| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- "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<Set<number>>(new Set());
- const [expandedBranches, setExpandedBranches] = useState<Set<string>>(new Set());
- const [expandedRetrievalAgents, setExpandedRetrievalAgents] = useState<Set<string>>(new Set());
- const [selected, setSelected] = useState<CanvasItem | null>(null);
- const [inspectorHistory, setInspectorHistory] = useState<CanvasItem[]>([]);
- const [artifactTarget, setArtifactTarget] = useState<ArtifactTarget | null>(null);
- const [promptRef, setPromptRef] = useState<string>();
- const [prompt, setPrompt] = useState<PromptProjection>();
- const [promptLoading, setPromptLoading] = useState(false);
- const [promptError, setPromptError] = useState<string>();
- const [inspectorMode, setInspectorMode] = useState<"business" | "lineage">("business");
- const [rawDetail, setRawDetail] = useState<unknown>();
- const [rawLoading, setRawLoading] = useState(false);
- const [detailError, setDetailError] = useState<string>();
- const [inspectorView, setInspectorView] = useState<InspectorWorkbenchProjection>();
- const [inspectorLoading, setInspectorLoading] = useState(false);
- const [inspectorError, setInspectorError] = useState<string>();
- const [mobileCanvas, setMobileCanvas] = useState(false);
- const [focusRound, setFocusRound] = useState<{ roundIndex: number; token: number }>();
- const selectedDetailRef = selected ? inspectorDetailRefFor(selected) : undefined;
- const inspectorTrigger = useRef<HTMLElement | null>(null);
- const inspectorTriggerKey = useRef<string | null>(null);
- const pendingFocusRestore = useRef<{ key: string | null; element: HTMLElement | null } | null>(null);
- const promptTrigger = useRef<HTMLElement | null>(null);
- const promptTriggerKey = useRef<string | null>(null);
- const pendingPromptFocusRestore = useRef<{ key: string | null; element: HTMLElement | null } | null>(null);
- const expansionPreference = useRef<"expanded" | "custom">("expanded");
- const detailCache = useRef(new Map<string, unknown>());
- const inspectorCache = useRef(new Map<string, InspectorWorkbenchProjection>());
- 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<HTMLElement>("[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<HTMLElement>("[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 <LoadingScreen />;
- if (!state.view) return <FatalState message={state.error || "无法读取真实构建数据"} />;
- const view = state.view;
- return <main className={`appFrame ${selected ? "withInspector" : ""} ${selected && inspectorMode === "lineage" ? "withLineageInspector" : ""}`}>
- <TopBar builds={state.builds} selectedId={state.selectedId} view={view} refreshing={state.refreshing} onSelect={state.selectBuild} expandedRounds={expandedRounds} allExpanded={allExpanded} onRound={navigateRound} onToggleAll={toggleAll} />
- {state.error ? <div className="errorBanner"><AlertTriangle size={15} /><span>{state.error}</span></div> : null}
- <div className="workspace">
- <MobileTimeline view={view} expandedRounds={expandedRounds} expandedBranches={expandedBranches} expandedRetrievalAgents={expandedRetrievalAgents} onToggleRound={toggleRound} onToggleBranch={toggleBranch} onToggleRetrievalAgent={toggleRetrievalAgent} onOpen={open} onCanvas={() => setMobileCanvas(true)} />
- <ReactFlowProvider><FlowCanvas view={view} expandedRounds={expandedRounds} expandedBranches={expandedBranches} expandedRetrievalAgents={expandedRetrievalAgents} onToggleRound={toggleRound} onToggleBranch={toggleBranch} onToggleRetrievalAgent={toggleRetrievalAgent} onOpen={open} onPrompt={openPrompt} selectedId={selected?.id} focusRound={focusRound} canvasFullscreen={mobileCanvas} onExitFullscreen={() => setMobileCanvas(false)} /></ReactFlowProvider>
- {selected ? <Inspector item={selected} mode={inspectorMode} onModeChange={setInspectorMode} onClose={closeInspector} onBack={inspectorHistory.length ? navigateInspectorBack : undefined} backLabel={inspectorHistory.length ? `返回${inspectorHistory[inspectorHistory.length - 1].title}` : undefined} onNavigate={navigateInspector} onPrompt={openPrompt} onArtifact={selected.itemType === "final-result" ? openArtifactFromInspector : undefined} detail={rawDetail} detailLoading={rawLoading} detailError={detailError} workbench={inspectorView} workbenchLoading={inspectorLoading} workbenchError={inspectorError} topOverlayOpen={Boolean(artifactTarget || promptRef)} /> : null}
- </div>
- {artifactTarget ? <ScriptArtifactViewer buildId={String(view.header.id)} snapshotRef={artifactTarget.snapshotRef} onClose={closeArtifact} /> : null}
- {promptRef ? <PromptDrawer prompt={prompt} loading={promptLoading} error={promptError} onClose={closePrompt} /> : null}
- </main>;
- }
- 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 <main className="loadingScreen"><h1>正在读取真实构建记录…</h1><div className="skeletonWide"><i /><i /><i /></div></main>; }
- function FatalState({ message }: { message: string }) { return <main className="fatalState"><AlertTriangle size={26} /><h1>真实运行数据暂时不可用</h1><p>{message}</p><p>请确认只读数据库连接和可视化后端已经启动。</p></main>; }
|