| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- "use client";
- import { useEffect, useRef, useState } from "react";
- import { ArrowLeft, BookOpen, FileText, GitCompareArrows, X } from "lucide-react";
- import type { CanvasItem, CardBusinessData, InspectorDetailProjection, InspectorTab, InspectorWorkbenchProjection, RetrievalActivity } from "@/lib/types";
- import { StatusBadge } from "@/components/ui/Badges";
- import { InspectorComparison } from "@/components/inspector/InspectorComparison";
- import { LegacyInspector } from "@/components/inspector/LegacyInspector";
- interface Props {
- item: CanvasItem;
- mode?: "business" | "lineage";
- onModeChange?: (mode: "business" | "lineage") => void;
- onClose: () => void;
- onBack?: () => void;
- backLabel?: string;
- onNavigate?: (activity: RetrievalActivity) => void;
- onPrompt?: (promptRef: string) => void;
- onArtifact?: () => void;
- workbench?: InspectorWorkbenchProjection;
- workbenchLoading?: boolean;
- workbenchError?: string;
- topOverlayOpen?: boolean;
- // Kept only so old isolated callers still compile while the formal path uses workbench.
- tab?: InspectorTab;
- onTab?: (tab: InspectorTab) => void;
- detail?: unknown;
- detailLoading?: boolean;
- detailError?: string;
- cardData?: CardBusinessData;
- cardDataLoading?: boolean;
- cardDataError?: string;
- cardDataMissingRef?: boolean;
- }
- export function Inspector(props: Props) {
- const [internalTab, setInternalTab] = useState<InspectorTab>("business");
- useEffect(() => setInternalTab("business"), [props.item.id]);
- const tab = props.tab || internalTab;
- const onTab = props.onTab || setInternalTab;
- const mode = props.mode || (props.tab && props.onTab ? "business" : "lineage");
- if (mode === "business") return <LegacyInspector {...props} tab={tab} onTab={onTab} onLineage={props.onModeChange ? () => props.onModeChange?.("lineage") : undefined} />;
- return <ComparisonInspector {...props} onBusiness={props.onModeChange ? () => props.onModeChange?.("business") : undefined} />;
- }
- function ComparisonInspector({ item, onClose, onBack, backLabel, onNavigate, onPrompt, onArtifact, onBusiness, workbench, workbenchLoading, workbenchError, detailLoading, detailError, topOverlayOpen = false }: Props & { onBusiness?: () => void }) {
- const dialog = useRef<HTMLElement>(null);
- const topOverlayOpenRef = useRef(topOverlayOpen);
- topOverlayOpenRef.current = topOverlayOpen;
- const [activeBindingId, setActiveBindingId] = useState<string>();
- const [clearActiveSignal, setClearActiveSignal] = useState(0);
- const promptRef = promptRefFor(item);
- useEffect(() => setActiveBindingId(undefined), [item.id]);
- useEffect(() => {
- const keepFocusInside = (event: FocusEvent) => {
- const panel = dialog.current;
- if (topOverlayOpenRef.current || !panel || panel.contains(event.target as Node)) return;
- panel.querySelector<HTMLElement>('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), summary, [tabindex]:not([tabindex="-1"])')?.focus();
- };
- document.addEventListener("focusin", keepFocusInside);
- return () => document.removeEventListener("focusin", keepFocusInside);
- }, []);
- const trapFocus = (event: React.KeyboardEvent<HTMLElement>) => {
- if (topOverlayOpen) return;
- if (event.key === "Escape") {
- if (activeBindingId) { event.preventDefault(); event.stopPropagation(); setActiveBindingId(undefined); setClearActiveSignal((value) => value + 1); return; }
- event.preventDefault(); event.stopPropagation(); onClose(); return;
- }
- if (event.key !== "Tab") return;
- const focusable = [...(dialog.current?.querySelectorAll<HTMLElement>('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), summary, [tabindex]:not([tabindex="-1"])') || [])];
- if (!focusable.length) return;
- const first = focusable[0]; const last = focusable[focusable.length - 1];
- if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
- else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
- };
- const effectiveError = workbenchError || (!workbench && !workbenchLoading && detailError ? detailError : undefined);
- const effectiveLoading = workbenchLoading || (!workbench && !workbenchError && detailLoading);
- return <><div className="inspectorWorkbenchBackdrop" aria-hidden="true" /><aside ref={dialog} className="inspector inspectorWorkbench" role="dialog" aria-modal="true" aria-label={`${item.title}三列来源详情`} onKeyDown={trapFocus} onWheelCapture={(event) => event.stopPropagation()}>
- <header className="inspectorHeader comparisonInspectorHeader">
- {onBack ? <button className="iconButton inspectorBack" type="button" onClick={onBack} aria-label={backLabel || "返回上一级详情"}><ArrowLeft size={18} /></button> : null}
- <div className="comparisonInspectorTitle"><span>{itemLabel(item)}</span><h2>{item.title}</h2><div className="comparisonInspectorMeta"><StoryStatus item={item} /></div></div>
- <div className="comparisonInspectorActions">
- {onBusiness ? <button type="button" className="quietButton" onClick={onBusiness}><GitCompareArrows size={14} />返回业务详情</button> : null}
- {promptRef && onPrompt ? <button type="button" className="quietButton" data-focus-return={`prompt:${item.id}:inspector`} onClick={() => onPrompt(promptRef)}><BookOpen size={14} />{promptActionLabel(item)}</button> : null}
- {item.itemType === "final-result" && onArtifact ? <button type="button" className="quietButton" data-focus-return={`artifact:${item.id}:inspector`} onClick={onArtifact}><FileText size={14} />查看当前主脚本</button> : null}
- <button autoFocus className="iconButton" type="button" onClick={onClose} aria-label="关闭详情"><X size={18} /></button>
- </div>
- </header>
- <div className="inspectorBody comparisonInspectorBody">
- <InspectorComparison data={workbench} loading={effectiveLoading} error={effectiveError} onNavigate={onNavigate} onActiveBindingChange={setActiveBindingId} clearActiveSignal={clearActiveSignal} />
- </div>
- </aside></>;
- }
- function promptRefFor(item: CanvasItem) {
- if (item.itemType === "story") return item.decision?.promptRef || undefined;
- if (item.itemType === "multipath-review") return item.review.promptRef;
- if (item.itemType === "multipath-decision") return item.decision.decisionProjection?.promptRef || undefined;
- return undefined;
- }
- function promptActionLabel(item: CanvasItem) {
- if (item.itemType === "story" && item.role === "data-decision") return "查看取数任务与提示词";
- const actorRole = item.itemType === "story" ? item.decision?.actor.role : item.itemType === "multipath-review" ? "multipath-evaluator" : item.itemType === "multipath-decision" ? item.decision.decisionProjection?.actor.role : undefined;
- if (actorRole === "main") return "查看主 Agent 提示词";
- if (actorRole === "multipath-evaluator") return "查看评审任务与提示词";
- if (actorRole === "overall-evaluator") return "查看整体评审提示词";
- return "查看实现任务与提示词";
- }
- function StoryStatus({ item }: { item: CanvasItem }) {
- const status = item.itemType === "story" ? item.status
- : item.itemType === "branch-summary" ? item.branch.status
- : item.itemType === "round-summary" ? item.round.state
- : item.itemType === "final-result" ? item.result.status
- : item.itemType === "retrieval-stage" ? item.stage.status
- : item.itemType === "retrieval-agent" ? item.run.status
- : item.itemType === "retrieval-direct" ? item.group.failureCount || item.group.interruptedCount ? "partial" : item.group.runningCount ? "running" : "completed"
- : item.itemType === "query-attempt" ? item.attempt.status
- : item.itemType === "retrieval-activity" ? item.activity.status
- : item.itemType === "multipath-review" ? item.review.status
- : undefined;
- return status ? <StatusBadge status={status} /> : null;
- }
- function itemLabel(item: CanvasItem) { return item.itemType === "story" ? item.role === "planning-analysis" ? "构建开始前的规划" : item.kind === "decision" ? "这一步的决定" : item.kind === "result" ? "这一步的结果" : "这一步做了什么" : item.itemType === "branch-summary" ? "候选方案" : item.itemType === "round-summary" ? "轮次摘要" : item.itemType === "retrieval-stage" ? "取数阶段" : item.itemType === "retrieval-direct" ? "工具取数" : item.itemType === "retrieval-agent" ? "Agent 取数" : item.itemType === "query-attempt" ? "单次查询" : item.itemType === "retrieval-activity" ? item.activity.kind === "tool" ? "单次工具调用" : "单次查询" : item.itemType === "multipath-review" ? "候选方案评审" : item.itemType === "multipath-decision" ? "主 Agent 决策" : "最终结果"; }
|