LegacyInspector.tsx 14 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. "use client";
  2. import { useEffect, useMemo, useRef, useState } from "react";
  3. import { ArrowLeft, BookOpen, ChevronRight, FileText, GitCompareArrows, X } from "lucide-react";
  4. import type { CanvasItem, DetailItem, DetailSection, InspectorDetailProjection, InspectorSemanticMeta, InspectorTab, RetrievalActivity } from "@/lib/types";
  5. import { SourceNoticeBadge, StatusBadge } from "@/components/ui/Badges";
  6. import { RichText } from "@/components/ui/RichText";
  7. import { ValueView } from "@/components/ui/ValueView";
  8. import { TabList } from "@/components/ui/TabList";
  9. import { DecisionDetailRenderer } from "@/components/decision/DecisionDetailRenderer";
  10. import { authorityLabel, decisionFor, decisionTypeLabel } from "@/components/decision/AgentDecisionCard";
  11. import { RetrievalDetailRenderer } from "@/components/inspector/RetrievalDetailRenderer";
  12. interface Props { item: CanvasItem; tab: InspectorTab; onTab: (tab: InspectorTab) => void; onClose: () => void; onBack?: () => void; backLabel?: string; onNavigate?: (activity: RetrievalActivity) => void; onPrompt?: (promptRef: string) => void; onArtifact?: () => void; onLineage?: () => void; detail?: unknown; detailLoading?: boolean; detailError?: string; }
  13. export function LegacyInspector({ item, tab, onTab, onClose, onBack, backLabel, onNavigate, onPrompt, onArtifact, onLineage, detail, detailLoading, detailError }: Props) {
  14. const dialog = useRef<HTMLElement>(null);
  15. const [modal, setModal] = useState(false);
  16. const projection = useMemo(() => normalizeProjection(item, detail), [item, detail]);
  17. const tabs = useMemo<Array<{ id: InspectorTab; label: string }>>(() => [
  18. { id: "business", label: "业务详情" },
  19. ...(projection.changes ? [{ id: "changes" as const, label: "产出与改动" }] : []),
  20. { id: "technical", label: "技术详情" },
  21. ], [projection.changes]);
  22. useEffect(() => {
  23. const query = window.matchMedia("(max-width: 1199px)");
  24. const update = () => setModal(query.matches);
  25. update(); query.addEventListener("change", update);
  26. return () => query.removeEventListener("change", update);
  27. }, []);
  28. useEffect(() => { if (!tabs.some((entry) => entry.id === tab)) onTab("business"); }, [onTab, tab, tabs]);
  29. const trapFocus = (event: React.KeyboardEvent<HTMLElement>) => {
  30. if (event.key !== "Tab" || !modal) return;
  31. const focusable = [...(dialog.current?.querySelectorAll<HTMLElement>('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])') || [])];
  32. if (!focusable.length) return;
  33. const first = focusable[0]; const last = focusable[focusable.length - 1];
  34. if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
  35. else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
  36. };
  37. return <aside ref={dialog} className="inspector" role="dialog" aria-modal={modal || undefined} aria-label="运行详情" onKeyDown={trapFocus} onWheelCapture={(event) => event.stopPropagation()}>
  38. <header className="inspectorHeader">{onBack ? <button className="iconButton inspectorBack" type="button" onClick={onBack} aria-label={backLabel || "返回上一级详情"}><ArrowLeft size={18} /></button> : null}<div><span>{itemLabel(item)}</span><h2>{item.title}</h2></div><div className="inspectorHeaderActions">{onLineage ? <button type="button" className="quietButton" aria-label="打开数据来源" onClick={onLineage}><GitCompareArrows size={14} />数据来源</button> : null}<button autoFocus className="iconButton" type="button" onClick={onClose} aria-label="关闭详情"><X size={18} /></button></div></header>
  39. <TabList className="inspectorTabs" tabs={tabs} active={tab} onChange={onTab} ariaLabel="详情分类" />
  40. <div className="inspectorBody">
  41. {detailLoading && tab === "business" ? <div className="skeletonLines" aria-label="正在加载完整业务记录"><i /><i /><i /></div> : null}
  42. {detailError && tab === "business" ? <Panel title="完整记录暂时不可用" missing><BusinessText value={detailError} /></Panel> : null}
  43. {tab === "business" ? <BusinessDetails item={item} projection={projection} onPrompt={onPrompt} onNavigate={onNavigate} onArtifact={onArtifact} /> : null}
  44. {tab === "changes" && projection.changes ? <ChangesDetails changes={projection.changes} /> : null}
  45. {tab === "technical" ? detailLoading ? <div className="skeletonLines" aria-label="正在加载技术详情"><i /><i /><i /></div> : detailError ? <Panel title="技术详情读取失败"><BusinessText className="diffWarning" value={detailError} /></Panel> : <TechnicalDetails value={projection.technical} /> : null}
  46. </div>
  47. </aside>;
  48. }
  49. function BusinessDetails({ item, projection, onPrompt, onNavigate, onArtifact }: { item: CanvasItem; projection: InspectorDetailProjection; onPrompt?: Props["onPrompt"]; onNavigate?: Props["onNavigate"]; onArtifact?: Props["onArtifact"] }) {
  50. const decision = decisionFor(item);
  51. const promptRef = projection.promptRef || decision?.promptRef;
  52. const actorRole = projection.actor?.role || decision?.actor.role;
  53. return <>
  54. <StoryBadges item={item} />
  55. {promptRef && onPrompt || item.itemType === "final-result" && onArtifact ? <div className="inspectorDecisionActions">
  56. {promptRef && onPrompt ? <button type="button" className="nodeAction promptAction" data-focus-return={`prompt:${item.id}:inspector`} onClick={() => onPrompt(promptRef)}><BookOpen size={14} />{promptActionLabel(item, actorRole)}</button> : null}
  57. {item.itemType === "final-result" && onArtifact ? <button type="button" className="nodeAction promptAction" data-focus-return={`artifact:${item.id}:inspector`} onClick={onArtifact}><FileText size={14} />查看当前主脚本</button> : null}
  58. </div> : null}
  59. {projection.detailKind?.startsWith("retrieval-") ? <RetrievalDetailRenderer projection={projection} onNavigate={onNavigate} /> : projection.detailKind === "agent-decision" ? <>
  60. <div className="decisionMetaRow">{projection.actor ? <span>{projection.actor.label}</span> : null}{projection.decisionType ? <span>{decisionTypeLabel(projection.decisionType)}</span> : null}{projection.authority ? <strong>{authorityLabel(projection.authority)}</strong> : null}</div>
  61. {projection.question ? <Panel title="这一步要回答什么" variant="summary" visualRole="key-result"><BusinessText value={projection.question} /></Panel> : null}
  62. <DecisionDetailRenderer blocks={projection.blocks || []} />
  63. </> : projection.businessSections?.length ? projection.businessSections.map((section) => <SectionPanel key={section.id} section={section} />) : <Panel title="业务记录" missing><BusinessText value="本次运行没有记录更多业务说明。" /></Panel>}
  64. </>;
  65. }
  66. function promptActionLabel(item: CanvasItem, actorRole?: string) {
  67. if (item.itemType === "story" && item.role === "data-decision") return "查看取数任务与提示词";
  68. if (actorRole === "main") return "查看主 Agent 提示词";
  69. if (actorRole === "multipath-evaluator") return "查看评审任务与提示词";
  70. if (actorRole === "overall-evaluator") return "查看整体评审提示词";
  71. return "查看实现任务与提示词";
  72. }
  73. function ChangesDetails({ changes }: { changes: NonNullable<InspectorDetailProjection["changes"]> }) { return <>{changes.summary ? <Panel title="改动摘要"><BusinessText value={changes.summary} /></Panel> : null}{changes.sections.map((section) => <SectionPanel key={section.id} section={section} />)}<p className="changeExactness">{exactnessLabel(changes.exactness)}</p></>; }
  74. function SectionPanel({ section }: { section: DetailSection }) { const content = section.id === "plan" && section.content ? insertExecutionPlanBreaks(section.content) : section.content; return <Panel title={section.title} variant={section.variant} visualRole={section.visualRole} presentation={section.presentation} collapsible={section.collapsible} defaultOpen={section.defaultOpen}>{section.items?.length ? <DetailItems items={section.items} /> : content ? <BusinessText value={content} /> : <BusinessText value="本次运行没有记录更多业务说明。" />}</Panel>; }
  75. function insertExecutionPlanBreaks(value: string) { const pattern = /(^|[ \t\u3000]+)(\d+)([.)、])(?=[ \t\u3000]*\S)/gm; const markers = [...value.matchAll(pattern)]; if (markers.length < 2) return value; const numbers = markers.map((match) => Number(match[2])); if (numbers[0] !== 1 || numbers.some((number, index) => index > 0 && number !== numbers[index - 1] + 1)) return value; let markerIndex = 0; return value.replace(pattern, (match, prefix: string, number: string, punctuation: string, offset: number) => { const first = markerIndex++ === 0; if (first && offset === 0) return `${number}${punctuation}`; return `${first ? "\n\n" : " \n"}${number}${punctuation}`; }); }
  76. function DetailItems({ items }: { items: DetailItem[] }) { return <dl className="inspectorDetailItems">{items.map((item, index) => <div key={`${item.label}-${index}`} className="inspectorDetailItem" data-detail-ref={item.detailRef || undefined}><dt>{item.label}</dt><dd><BusinessText value={item.value} />{item.sourceNotice && item.sourceNotice !== "runtime-associated" ? <SourceNoticeBadge notice={item.sourceNotice} /> : null}</dd></div>)}</dl>; }
  77. function StoryBadges({ 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 ? <div className="inspectorBadgeRow"><StatusBadge status={status} /></div> : null; }
  78. function normalizeProjection(item: CanvasItem, detail: unknown): InspectorDetailProjection { const record = asRecord(detail); if (record && Object.prototype.hasOwnProperty.call(record, "technical") && (Array.isArray(record.businessSections) || typeof record.detailKind === "string")) return record as unknown as InspectorDetailProjection; return { detailKind: "activity", businessSections: [], technical: detail ?? item }; }
  79. function TechnicalDetails({ value }: { value: unknown }) { const record = asRecord(value); if (!record) return <Panel title="原始技术数据"><ValueView value={value} /></Panel>; const groups = [["source", "数据来源与定位"], ["association", "关联方式与完整性"], ["input", "原始输入"], ["output", "原始输出"], ["cost", "耗时与成本"]] as const; const shown = new Set<string>(); return <>{groups.map(([key, title]) => { if (!Object.prototype.hasOwnProperty.call(record, key)) return null; shown.add(key); return <Panel key={key} title={title}><ValueView value={record[key]} /></Panel>; })}<Panel title="原始记录"><ValueView value={Object.fromEntries(Object.entries(record).filter(([key]) => !shown.has(key)))} /></Panel></>; }
  80. function exactnessLabel(value: NonNullable<InspectorDetailProjection["changes"]>["exactness"]) { return ({ exact: "这是可确认的精确记录。", "current-only": "这里只能确认当前主脚本,无法还原每轮结束时的完整版本。", "historical-snapshot": "这里展示的是处置前保存的候选快照。", unknown: "当前记录不足以还原精确的前后差异。" } as const)[value]; }
  81. function Panel({ title, children, missing = false, variant = "default", visualRole, presentation, collapsible = false, defaultOpen = false }: { title: string; children: React.ReactNode; missing?: boolean; variant?: DetailSection["variant"] } & InspectorSemanticMeta) { const role = visualRole || (variant === "summary" ? "key-result" : "section"); const panelProps = { className: `inspectorPanel ${missing ? "isMissing" : ""} is-${variant} is-role-${role} ${collapsible ? "decisionDisclosurePanel" : ""}`, "data-visual-role": role, "data-presentation": presentation || "prose" }; if (collapsible) return <section {...panelProps}><details className="decisionDisclosure" open={defaultOpen || undefined}><summary><span className="decisionDisclosureLabel"><ChevronRight aria-hidden="true" size={16} strokeWidth={2.25} /><h3>{title}</h3></span></summary><div className="decisionDisclosureBody">{children}</div></details></section>; return <section {...panelProps}><h3>{title}</h3>{children}</section>; }
  82. function BusinessText({ value, className = "" }: { value: string; className?: string }) { return <RichText value={value} className={className} />; }
  83. function asRecord(value: unknown): Record<string, unknown> | undefined { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined; }
  84. 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 决策" : "最终结果"; }