import { useEffect, useRef, useState } from 'react'; import { Loader2, X, Activity } from 'lucide-react'; import { usePluginContext } from './client'; import type { AnalysisJob } from './types'; const API_BASE = '/api/v1/plugin/data_analytics'; interface Props { jobId: string; onClose: () => void; } interface StreamEvent { type?: string; ts?: number; payload?: unknown; raw?: string; } export function JobDetail({ jobId, onClose }: Props) { const { client, token, apiBase } = usePluginContext(); const [job, setJob] = useState(null); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const eventBoxRef = useRef(null); // 初次加载 + 后台轮询 useEffect(() => { let cancelled = false; const fetchJob = async () => { try { const resp = await client.get(`${API_BASE}/jobs/${jobId}`); if (!cancelled) setJob(resp.data); } catch (e) { console.error('fetch job failed', e); } finally { if (!cancelled) setLoading(false); } }; fetchJob(); const t = setInterval(fetchJob, 4000); return () => { cancelled = true; clearInterval(t); }; }, [client, jobId]); // SSE 事件流(用 fetch + ReadableStream,因为 EventSource 不支持自定义 header) useEffect(() => { const controller = new AbortController(); const run = async () => { try { const url = `${apiBase || ''}${API_BASE}/jobs/${jobId}/stream`; const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` }, signal: controller.signal, }); if (!resp.body) return; const reader = resp.body.getReader(); const decoder = new TextDecoder('utf-8'); let buf = ''; // eslint-disable-next-line no-constant-condition while (true) { const { value, done } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const parts = buf.split('\n\n'); buf = parts.pop() || ''; for (const part of parts) { const line = part.split('\n').find((l) => l.startsWith('data:')); if (!line) continue; const payload = line.slice(5).trim(); try { setEvents((prev) => [...prev, JSON.parse(payload)]); } catch { setEvents((prev) => [...prev, { raw: payload }]); } } } } catch (e) { if ((e as Error).name !== 'AbortError') console.error('SSE error', e); } }; run(); return () => controller.abort(); }, [apiBase, jobId, token]); // 自动滚动到底部 useEffect(() => { if (eventBoxRef.current) { eventBoxRef.current.scrollTop = eventBoxRef.current.scrollHeight; } }, [events]); return (

任务详情

{jobId}
{loading ? (
) : job ? ( <>
{job.task_error && }
{job.task_result !== undefined && job.task_result !== null && (
执行结果
                    {typeof job.task_result === 'string'
                      ? job.task_result
                      : JSON.stringify(job.task_result, null, 2)}
                  
)}
事件流(SSE)
{events.length === 0 ? ( (等待事件…) ) : ( events.map((e, i) => (
{e.type || 'event'}{' '} {e.payload !== undefined ? ( {JSON.stringify(e.payload)} ) : e.raw ? ( {e.raw} ) : null}
)) )}
) : (
任务不存在或已被删除
)}
); } function Field({ label, value, danger }: { label: string; value: string; danger?: boolean }) { return (
{label}
{value}
); }