import { useCallback, useEffect, useRef, useState } from "react"; import { Archive, BarChart3, ChevronRight, LoaderCircle, RefreshCw } from "lucide-react"; import type { WorkbenchAccess } from "../api/request-context"; import { Button } from "@/shared/ui/button"; import { Disclosure } from "@/shared/ui/disclosure"; import { StatusBadge, type StatusTone } from "@/shared/ui/status"; import { fetchWorkbenchAnalysisRun } from "../api/workbench-runtime-client"; import { fetchWorkbenchAnalysisHistory, type WorkbenchAnalysisHistoryItem } from "../api/workbench-analysis-history-client"; import type { AnalysisArtifact } from "../workspace/workspace-model"; import { WorkbenchEmptyState, WorkbenchNotice, WorkbenchSection } from "./workbench-panel-ui"; const HISTORY_PAGE_SIZE = 10; export function AnalysisHistorySection({ getAccessToken, onOpenArtifact }: { getAccessToken?: WorkbenchAccess; onOpenArtifact: (artifact: AnalysisArtifact) => void; }) { const [items, setItems] = useState([]); const [nextOffset, setNextOffset] = useState(null); const [loading, setLoading] = useState(false); const [openingRunId, setOpeningRunId] = useState(null); const [error, setError] = useState(null); const loadedRef = useRef(false); const listControllerRef = useRef(null); const artifactControllerRef = useRef(null); useEffect(() => () => { listControllerRef.current?.abort(); artifactControllerRef.current?.abort(); }, []); const load = useCallback(async (more = false) => { if (loading) return; const offset = more ? nextOffset : 0; if (more && offset === null) return; listControllerRef.current?.abort(); const controller = new AbortController(); listControllerRef.current = controller; setLoading(true); setError(null); try { const page = await fetchWorkbenchAnalysisHistory({ limit: HISTORY_PAGE_SIZE, offset: offset ?? 0, getAccessToken, signal: controller.signal }); if (controller.signal.aborted) return; setItems((current) => more ? [...current, ...page.items.filter( (item) => !current.some((existing) => existing.runId === item.runId) )] : page.items); setNextOffset(page.nextOffset); loadedRef.current = true; } catch (nextError) { if (!controller.signal.aborted) { setError(errorMessage(nextError)); } } finally { if (!controller.signal.aborted) setLoading(false); } }, [getAccessToken, loading, nextOffset]); const openArtifact = async (item: WorkbenchAnalysisHistoryItem) => { if (openingRunId) return; artifactControllerRef.current?.abort(); const controller = new AbortController(); artifactControllerRef.current = controller; setOpeningRunId(item.runId); setError(null); try { const projection = await fetchWorkbenchAnalysisRun( item.runId, getAccessToken, controller.signal ); if (!controller.signal.aborted) onOpenArtifact(projection.artifact); } catch (nextError) { if (!controller.signal.aborted) setError(errorMessage(nextError)); } finally { if (!controller.signal.aborted) setOpeningRunId(null); } }; return ( } tone="info" meta={loadedRef.current ? ( 已加载 {items.length} 项 ) : null} > { if (event.currentTarget.open && !loadedRef.current && !loading) void load(); }} >
{error ? {error} : null} {loading && !items.length ? (
正在读取分析成果历史
) : null} {!loading && loadedRef.current && !items.length ? ( } title="暂无持久化分析成果" description="新的分析运行会在保存到当前项目 BizDB 后出现在这里。" /> ) : null} {items.length ? (
    {items.map((item) => (
  1. ))}
) : null}
{error && !items.length ? ( ) : null} {nextOffset !== null ? ( ) : null}
); } function statusTone(status: string): StatusTone { if (["completed", "succeeded"].includes(status)) return "success"; if (["failed", "error"].includes(status)) return "danger"; if (["running"].includes(status)) return "info"; if (["queued", "pending"].includes(status)) return "warning"; return "neutral"; } function formatHistoryTime(value: string) { return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(value)); } function errorMessage(error: unknown) { return error instanceof Error ? error.message : "分析成果历史请求失败"; }