"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { AlertTriangle } from "lucide-react"; import { ReactFlowProvider } from "@xyflow/react"; import { errorMessage, getActivityDetail, getDecisionPrompt, getInspectorView, getRoundDetail } from "@/lib/api"; import type { CanvasItem, ExecutionViewV8, InspectorWorkbenchProjection, OpenTarget, PromptProjection, RetrievalActivity, StoryNode } from "@/lib/types"; import { branchFlow, branchSummaryItem, finalResultItem, multipathDecisionItem, multipathReviewItem, queryAttemptItem, retrievalActivityItem, retrievalAgentItem, retrievalDirectItem, retrievalStageItem, roundSummaryItem, storyItem } from "@/lib/layout"; import { useExecutionView } from "@/hooks/useExecutionView"; import { TopBar } from "@/components/workbench/TopBar"; import { FlowCanvas } from "@/components/workbench/FlowCanvas"; import { MobileTimeline } from "@/components/workbench/MobileTimeline"; import { Inspector } from "@/components/inspector/Inspector"; import { ScriptArtifactViewer } from "@/components/artifact/ScriptArtifactViewer"; import { PromptDrawer } from "@/components/decision/PromptDrawer"; import { expandedStateFor, isFullyExpanded } from "@/lib/expansion"; interface ArtifactTarget { snapshotRef: string } export function ExecutionWorkbench() { const state = useExecutionView(); const [expandedRounds, setExpandedRounds] = useState>(new Set()); const [expandedBranches, setExpandedBranches] = useState>(new Set()); const [expandedRetrievalAgents, setExpandedRetrievalAgents] = useState>(new Set()); const [selected, setSelected] = useState(null); const [inspectorHistory, setInspectorHistory] = useState([]); const [artifactTarget, setArtifactTarget] = useState(null); const [promptRef, setPromptRef] = useState(); const [prompt, setPrompt] = useState(); const [promptLoading, setPromptLoading] = useState(false); const [promptError, setPromptError] = useState(); const [inspectorMode, setInspectorMode] = useState<"business" | "lineage">("business"); const [rawDetail, setRawDetail] = useState(); const [rawLoading, setRawLoading] = useState(false); const [detailError, setDetailError] = useState(); const [inspectorView, setInspectorView] = useState(); const [inspectorLoading, setInspectorLoading] = useState(false); const [inspectorError, setInspectorError] = useState(); const [mobileCanvas, setMobileCanvas] = useState(false); const [focusRound, setFocusRound] = useState<{ roundIndex: number; token: number }>(); const selectedDetailRef = selected ? inspectorDetailRefFor(selected) : undefined; const inspectorTrigger = useRef(null); const inspectorTriggerKey = useRef(null); const pendingFocusRestore = useRef<{ key: string | null; element: HTMLElement | null } | null>(null); const promptTrigger = useRef(null); const promptTriggerKey = useRef(null); const pendingPromptFocusRestore = useRef<{ key: string | null; element: HTMLElement | null } | null>(null); const expansionPreference = useRef<"expanded" | "custom">("expanded"); const detailCache = useRef(new Map()); const inspectorCache = useRef(new Map()); useEffect(() => { if (!state.view) return; expansionPreference.current = "expanded"; const expanded = expandedStateFor(state.view); setExpandedRounds(expanded.rounds); setExpandedBranches(expanded.branches); setExpandedRetrievalAgents(expanded.retrievalAgents); setSelected(null); setInspectorHistory([]); setInspectorMode("business"); detailCache.current.clear(); inspectorCache.current.clear(); setRawDetail(undefined); setDetailError(undefined); setInspectorView(undefined); setInspectorError(undefined); setArtifactTarget(null); setPromptRef(undefined); }, [state.view?.header.id]); useEffect(() => { if (!state.view || expansionPreference.current !== "expanded") return; const expanded = expandedStateFor(state.view); setExpandedRounds(expanded.rounds); setExpandedBranches(expanded.branches); setExpandedRetrievalAgents(expanded.retrievalAgents); }, [state.view?.capturedAt]); useEffect(() => { if (inspectorMode !== "business" || !selected || !state.view) return; const detailRef = selectedDetailRef; if (!detailRef) { setRawDetail(selected); setRawLoading(false); setDetailError(undefined); return; } const cacheKey = `${state.view.header.id}:${detailRef}:${state.view.capturedAt}`; const cached = detailCache.current.get(cacheKey); if (cached !== undefined && !isTransientDetail(cached)) { setRawDetail(cached); setRawLoading(false); setDetailError(undefined); return; } const controller = new AbortController(); setRawDetail(undefined); setRawLoading(true); setDetailError(undefined); const request = selected.itemType === "round-summary" ? getRoundDetail(String(state.view.header.id), selected.roundIndex, controller.signal) : selected.itemType === "story" && selected.roundIndex && detailRef === `round:${selected.roundIndex}` ? getRoundDetail(String(state.view.header.id), selected.roundIndex, controller.signal) : getActivityDetail(String(state.view.header.id), detailRef, controller.signal); void request .then((detail) => { if (!isTransientDetail(detail)) detailCache.current.set(cacheKey, detail); setRawDetail(detail); }) .catch((error) => { if (!controller.signal.aborted) { setRawDetail(undefined); setDetailError(errorMessage(error)); } }) .finally(() => { if (!controller.signal.aborted) setRawLoading(false); }); return () => controller.abort(); }, [inspectorMode, selected?.id, selectedDetailRef, state.view?.header.id, state.view?.capturedAt]); useEffect(() => { if (!selected || !state.view) return; const updated = findItem(state.view, selected.id); if (updated) setSelected(updated); }, [state.view?.capturedAt]); useEffect(() => { if (inspectorMode !== "lineage") return; if (!selected || !state.view) return; const detailRef = selectedDetailRef; if (!detailRef) { setInspectorView(undefined); setInspectorLoading(false); setInspectorError("当前卡片没有可用的详情引用。"); return; } const cacheKey = `${state.view.header.id}:${detailRef}:${state.view.capturedAt}`; const cached = inspectorCache.current.get(cacheKey); if (cached) { setInspectorView(cached); setInspectorLoading(false); setInspectorError(undefined); return; } const controller = new AbortController(); setInspectorView(undefined); setInspectorLoading(true); setInspectorError(undefined); void getInspectorView(String(state.view.header.id), detailRef, controller.signal) .then((payload) => { const prefix = `${state.view!.header.id}:${detailRef}:`; for (const key of inspectorCache.current.keys()) { if (key.startsWith(prefix)) inspectorCache.current.delete(key); } inspectorCache.current.set(cacheKey, payload); while (inspectorCache.current.size > 32) { const oldest = inspectorCache.current.keys().next().value; if (!oldest) break; inspectorCache.current.delete(oldest); } setInspectorView(payload); }) .catch((error) => { if (!controller.signal.aborted) { setInspectorView(undefined); setInspectorError(errorMessage(error)); } }) .finally(() => { if (!controller.signal.aborted) setInspectorLoading(false); }); return () => controller.abort(); }, [inspectorMode, selected?.id, selectedDetailRef, state.view?.header.id, state.view?.capturedAt]); useEffect(() => { if (!promptRef || !state.view) return; const controller = new AbortController(); setPrompt(undefined); setPromptError(undefined); setPromptLoading(true); void getDecisionPrompt(String(state.view.header.id), promptRef, controller.signal) .then(setPrompt) .catch((error) => { if (!controller.signal.aborted) setPromptError(errorMessage(error)); }) .finally(() => { if (!controller.signal.aborted) setPromptLoading(false); }); return () => controller.abort(); }, [promptRef, state.view?.header.id]); const closeInspector = useCallback(() => { pendingFocusRestore.current = { key: inspectorTriggerKey.current, element: inspectorTrigger.current }; setSelected(null); setInspectorHistory([]); setInspectorMode("business"); }, []); const closeArtifact = useCallback(() => { pendingFocusRestore.current = { key: inspectorTriggerKey.current, element: inspectorTrigger.current }; setArtifactTarget(null); }, []); const openPrompt = useCallback((nextPromptRef: string) => { promptTrigger.current = document.activeElement instanceof HTMLElement ? document.activeElement : null; promptTriggerKey.current = promptTrigger.current?.dataset.focusReturn || null; setPromptRef(nextPromptRef); }, []); const openArtifactFromInspector = useCallback(() => { if (!selected) return; const snapshotRef = artifactRefFor(selected); if (!snapshotRef) return; inspectorTrigger.current = document.activeElement instanceof HTMLElement ? document.activeElement : null; inspectorTriggerKey.current = inspectorTrigger.current?.dataset.focusReturn || null; setArtifactTarget({ snapshotRef }); }, [selected]); const closePrompt = useCallback(() => { pendingPromptFocusRestore.current = { key: promptTriggerKey.current, element: promptTrigger.current, }; setPromptRef(undefined); }, []); useEffect(() => { const pending = pendingFocusRestore.current; if (artifactTarget || !pending) return; pendingFocusRestore.current = null; let secondFrame = 0; const firstFrame = requestAnimationFrame(() => { secondFrame = requestAnimationFrame(() => { const replacement = pending.key ? [...document.querySelectorAll("[data-focus-return]")] .find((element) => element.dataset.focusReturn === pending.key) : null; const target = replacement || (pending.element?.isConnected ? pending.element : null); target?.focus({ preventScroll: true }); }); }); return () => { cancelAnimationFrame(firstFrame); if (secondFrame) cancelAnimationFrame(secondFrame); }; }, [selected, artifactTarget]); useEffect(() => { const pending = pendingPromptFocusRestore.current; if (promptRef || !pending) return; pendingPromptFocusRestore.current = null; let secondFrame = 0; const firstFrame = requestAnimationFrame(() => { secondFrame = requestAnimationFrame(() => { const replacement = pending.key ? [...document.querySelectorAll("[data-focus-return]")] .find((element) => element.dataset.focusReturn === pending.key) : null; const target = replacement || (pending.element?.isConnected ? pending.element : null); target?.focus({ preventScroll: true }); }); }); return () => { cancelAnimationFrame(firstFrame); if (secondFrame) cancelAnimationFrame(secondFrame); }; }, [promptRef]); useEffect(() => { const close = (event: KeyboardEvent) => { if (event.key === "Escape" && !artifactTarget && !promptRef) { closeInspector(); setMobileCanvas(false); } }; window.addEventListener("keydown", close); return () => window.removeEventListener("keydown", close); }, [artifactTarget, closeInspector, promptRef]); const open = useCallback((item: CanvasItem, target: OpenTarget) => { inspectorTrigger.current = document.activeElement instanceof HTMLElement ? document.activeElement : null; inspectorTriggerKey.current = inspectorTrigger.current?.dataset.focusReturn || null; if (target === "artifact") { const snapshotRef = artifactRefFor(item); if (!snapshotRef) return; setSelected(null); setInspectorHistory([]); setArtifactTarget({ snapshotRef }); return; } setInspectorHistory([]); setInspectorMode(target === "lineage" ? "lineage" : "business"); setSelected(item); setRawDetail(undefined); setDetailError(undefined); setInspectorView(undefined); setInspectorError(undefined); }, []); const navigateInspector = useCallback((activity: RetrievalActivity) => { if (!selected || !activity.detailRef) return; const roundIndex = "roundIndex" in selected ? selected.roundIndex : 0; const branchId = "branchId" in selected ? selected.branchId : 0; setInspectorHistory((current) => [...current, selected]); setSelected(retrievalActivityItem(activity, roundIndex || 0, branchId || 0)); setInspectorView(undefined); setInspectorError(undefined); }, [selected]); const navigateInspectorBack = useCallback(() => { setInspectorHistory((current) => { const parent = current[current.length - 1]; if (!parent) return current; const refreshed = state.view ? findItem(state.view, parent.id) : undefined; setSelected(refreshed || parent); setInspectorView(undefined); setInspectorError(undefined); return current.slice(0, -1); }); }, [state.view]); const toggleRound = useCallback((roundIndex: number) => { expansionPreference.current = "custom"; setExpandedRounds((current) => { const next = new Set(current); next.has(roundIndex) ? next.delete(roundIndex) : next.add(roundIndex); return next; }); }, []); const toggleBranch = useCallback((roundIndex: number, branchId: number) => setExpandedBranches((current) => { expansionPreference.current = "custom"; const next = new Set(current); const key = `${roundIndex}:${branchId}`; next.has(key) ? next.delete(key) : next.add(key); return next; }), []); const toggleRetrievalAgent = useCallback((agentId: string) => setExpandedRetrievalAgents((current) => { expansionPreference.current = "custom"; const next = new Set(current); next.has(agentId) ? next.delete(agentId) : next.add(agentId); return next; }), []); const navigateRound = useCallback((roundIndex: number) => { expansionPreference.current = "custom"; setExpandedRounds((current) => new Set(current).add(roundIndex)); setFocusRound((current) => ({ roundIndex, token: (current?.token || 0) + 1 })); }, []); const allExpanded = state.view ? isFullyExpanded(state.view, expandedRounds, expandedBranches, expandedRetrievalAgents) : false; const toggleAll = useCallback(() => { if (!state.view) return; if (isFullyExpanded(state.view, expandedRounds, expandedBranches, expandedRetrievalAgents)) { expansionPreference.current = "custom"; setExpandedRounds(new Set()); setExpandedBranches(new Set()); setExpandedRetrievalAgents(new Set()); return; } expansionPreference.current = "expanded"; const expanded = expandedStateFor(state.view); setExpandedRounds(expanded.rounds); setExpandedBranches(expanded.branches); setExpandedRetrievalAgents(expanded.retrievalAgents); }, [state.view, expandedRounds, expandedBranches, expandedRetrievalAgents]); if (state.loading && !state.view) return ; if (!state.view) return ; const view = state.view; return
{state.error ?
{state.error}
: null}
setMobileCanvas(true)} /> setMobileCanvas(false)} /> {selected ? : null}
{artifactTarget ? : null} {promptRef ? : null}
; } function artifactRefFor(item: CanvasItem): string | undefined { if (item.itemType === "final-result") return item.result.artifact.snapshotRef || item.detailRef || "artifact:base:current"; if (item.itemType === "round-summary") return item.round.result.artifactRef || `artifact:round:${item.roundIndex}`; if (item.itemType === "branch-summary" && item.branch.output.type === "script-artifact") return item.branch.output.snapshotRef; if (item.itemType === "story") return item.artifactRef; return undefined; } function inspectorDetailRefFor(item: CanvasItem): string | undefined { if (item.itemType === "final-result") return "run:final-result"; return "detailRef" in item ? item.detailRef : undefined; } function findItem(view: ExecutionViewV8, id: string): CanvasItem | undefined { if (id === view.planning?.id) return storyItem(view.planning); if (id === view.objective.id) return storyItem(view.objective); if (id === view.finalResult.id) return finalResultItem(view); for (const round of view.rounds) { if (id === `round:${round.roundIndex}:summary`) return roundSummaryItem(round); const main: StoryNode[] = [round.goal, round.plan.node, ...round.convergenceBatches.flatMap((batch) => [batch.missingReview, batch.missingDecision].filter((node): node is StoryNode => Boolean(node))), round.overallEvaluation, round.result]; const mainStory = main.find((story) => story.id === id); if (mainStory) return storyItem(mainStory, round.roundIndex); for (const batch of round.convergenceBatches) { if (batch.review?.id === id) return multipathReviewItem(batch.review, round.roundIndex); if (batch.decision?.id === id) return multipathDecisionItem(batch.decision, round.roundIndex); } for (const branch of round.branches) { if (id === `round:${round.roundIndex}:branch:${branch.branchId}:summary`) return branchSummaryItem(branch, round.roundIndex); const branchStory = branchFlow(branch).find((story) => story.id === id); if (branchStory) return storyItem(branchStory, round.roundIndex, branch.branchId); if (branch.retrievalStage.id === id) return retrievalStageItem(branch.retrievalStage, round.roundIndex, branch.branchId); const direct = branch.retrievalStage.directToolGroups.find((group) => group.id === id); if (direct) return retrievalDirectItem(direct, round.roundIndex, branch.branchId); for (const group of branch.retrievalStage.directToolGroups) { const call = group.calls.find((item) => item.id === id || item.detailRef === id); if (call) return retrievalActivityItem({ id: call.id, kind: "tool", label: call.businessLabel, status: call.status, summary: call.resultSummary, detailRef: call.detailRef }, round.roundIndex, branch.branchId); } const agent = branch.retrievalStage.agentRuns.find((run) => run.id === id); if (agent) return retrievalAgentItem(agent, round.roundIndex, branch.branchId); for (const run of branch.retrievalStage.agentRuns) { const attempt = run.attempts.find((item) => item.id === id); if (attempt) return queryAttemptItem(attempt, round.roundIndex, branch.branchId); } } } return undefined; } function isTransientDetail(detail: unknown) { if (!detail || typeof detail !== "object" || Array.isArray(detail)) return false; const record = detail as { outcome?: { state?: string }; activities?: Array<{ status?: string }> }; return record.outcome?.state === "running" || record.activities?.some((item) => item.status === "running") === true; } function LoadingScreen() { return

正在读取真实构建记录…

; } function FatalState({ message }: { message: string }) { return

真实运行数据暂时不可用

{message}

请确认只读数据库连接和可视化后端已经启动。

; }