MobileTimeline.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import { ChevronDown, ChevronRight, FileText, Maximize2 } from "lucide-react";
  2. import { branchFlow, branchSummaryItem, finalResultItem, multipathDecisionItem, multipathReviewItem, queryAttemptItem, retrievalAgentItem, retrievalDirectItem, retrievalStageItem, roundSummaryItem, storyItem } from "@/lib/layout";
  3. import type { AgentDecisionProjection, BranchStory, CanvasItem, ExecutionViewV8, OpenTarget, RoundStoryV8, StoryNode } from "@/lib/types";
  4. import { runResultFallback } from "@/lib/status-copy";
  5. import { StatusBadge } from "@/components/ui/Badges";
  6. import { CardFields } from "@/components/flow/CardFields";
  7. import { authorityLabel, decisionTypeLabel } from "@/components/decision/AgentDecisionCard";
  8. interface Props { view: ExecutionViewV8; expandedRounds: Set<number>; expandedBranches: Set<string>; expandedRetrievalAgents: Set<string>; onToggleRound: (round: number) => void; onToggleBranch: (round: number, branchId: number) => void; onToggleRetrievalAgent: (agentId: string) => void; onOpen: (item: CanvasItem, target: OpenTarget) => void; onCanvas: () => void; }
  9. export function MobileTimeline({ view, expandedRounds, expandedBranches, expandedRetrievalAgents, onToggleRound, onToggleBranch, onToggleRetrievalAgent, onOpen, onCanvas }: Props) {
  10. const objective = storyItem(view.objective);
  11. const final = finalResultItem(view);
  12. const objectiveLabel = "创作目标";
  13. return <section className="mobileTimeline">
  14. <header><div><h2>这次构建经历了什么</h2><p>按时间从上到下阅读</p></div><button className="quietButton" type="button" onClick={onCanvas}><Maximize2 size={15} />横向画布</button></header>
  15. {view.planning ? <ol className="mobileSteps mobilePrelude"><StoryItem story={view.planning} onOpen={onOpen} /></ol> : null}
  16. <MobileDecision item={objective} decision={view.objective.decision} fallbackLabel={objectiveLabel} fallbackCard={view.objective.card} onOpen={onOpen} />
  17. <ol className="roundList">{view.rounds.map((round) => <RoundItem key={round.id} round={round} expanded={expandedRounds.has(round.roundIndex)} expandedBranches={expandedBranches} expandedRetrievalAgents={expandedRetrievalAgents} onToggle={() => onToggleRound(round.roundIndex)} onToggleBranch={onToggleBranch} onToggleRetrievalAgent={onToggleRetrievalAgent} onOpen={onOpen} />)}</ol>
  18. <article className="mobileStory mobileFinal"><button className="mobileFinalSummary" data-focus-return={`${final.id}:mobile`} type="button" onClick={() => onOpen(final, "business")}><span>最终结果</span><StatusBadge status={view.finalResult.status} /><strong>{view.finalResult.title}</strong><p>{view.finalResult.summary || view.finalResult.errorMessage || runResultFallback(view.finalResult.status, view.finalResult.terminal)}</p></button><button type="button" className="nodeAction" data-focus-return={`${final.id}:artifact-mobile`} onClick={() => onOpen(final, "artifact")}>查看完整主脚本</button></article>
  19. </section>;
  20. }
  21. function RoundItem({ round, expanded, expandedBranches, expandedRetrievalAgents, onToggle, onToggleBranch, onToggleRetrievalAgent, onOpen }: { round: RoundStoryV8; expanded: boolean; expandedBranches: Set<string>; expandedRetrievalAgents: Set<string>; onToggle: () => void; onToggleBranch: Props["onToggleBranch"]; onToggleRetrievalAgent: Props["onToggleRetrievalAgent"]; onOpen: Props["onOpen"] }) {
  22. const summary = roundSummaryItem(round);
  23. return <li>
  24. <div className="mobileRoundSummary">
  25. <button type="button" className="roundExpand" aria-label={`${expanded ? "折叠" : "展开"}第 ${round.roundIndex} 轮`} onClick={onToggle}>{expanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}</button>
  26. <button type="button" className="roundSummaryOpen" data-focus-return={`${summary.id}:mobile`} onClick={() => onOpen(summary, "business")}><span>第 {round.roundIndex} 轮</span><StatusBadge status={round.state} /><strong>{round.summary.title}</strong><SummarySequence round={round} /></button>
  27. <button type="button" className="candidateExpand artifactAction" data-focus-return={`${summary.id}:artifact-mobile`} onClick={() => onOpen(summary, "artifact")}><FileText size={14} />查看本轮产出脚本表</button>
  28. </div>
  29. {expanded ? <ol className="mobileSteps">
  30. <StoryItem story={round.goal} roundIndex={round.roundIndex} onOpen={onOpen} />
  31. <StoryItem story={round.plan.node} roundIndex={round.roundIndex} onOpen={onOpen} />
  32. {round.branches.map((branch) => {
  33. const key = `${round.roundIndex}:${branch.branchId}`;
  34. const item = branchSummaryItem(branch, round.roundIndex);
  35. return <li className="mobileCandidate" key={branch.id}><button type="button" data-focus-return={`${item.id}:mobile`} onClick={() => onOpen(item, "business")}><span>候选方案 {branch.branchId}</span><StatusBadge status={branch.status} /><strong>{branch.title}</strong><p>{branch.summary.task || branch.summary.output || "未记录方案摘要"}</p></button><div className="mobileCandidateActions"><button type="button" className="candidateExpand" onClick={() => onToggleBranch(round.roundIndex, branch.branchId)}>{expandedBranches.has(key) ? "收起方案过程" : "展开方案过程"}</button>{branch.output.type === "script-artifact" && branch.output.snapshotRef ? <button type="button" className="candidateExpand artifactAction" data-focus-return={`${item.id}:artifact-mobile`} onClick={() => onOpen(item, "artifact")}><FileText size={14} />查看完整候选脚本</button> : null}</div>{expandedBranches.has(key) ? <BranchProcess branch={branch} roundIndex={round.roundIndex} expandedRetrievalAgents={expandedRetrievalAgents} onToggleRetrievalAgent={onToggleRetrievalAgent} onOpen={onOpen} /> : null}</li>;
  36. })}
  37. {round.convergenceBatches.map((batch) => <li className="mobileConvergence" key={batch.id}>
  38. <ol className="mobileConvergenceSteps">
  39. {batch.review ? <li><MobileDecision item={multipathReviewItem(batch.review, round.roundIndex)} decision={batch.review.decisionProjection} fallbackLabel="多方案评审" fallbackCard={{ primary: { value: batch.review.conclusion || "查看评审结论" }, secondary: [] }} onOpen={onOpen} /></li> : batch.missingReview ? <StoryItem story={batch.missingReview} roundIndex={round.roundIndex} onOpen={onOpen} /> : null}
  40. {batch.decision ? <li><MobileDecision item={multipathDecisionItem(batch.decision, round.roundIndex)} decision={batch.decision.decisionProjection} fallbackLabel="主 Agent 多路决策" fallbackCard={{ primary: { value: batch.decision.decision }, secondary: [] }} onOpen={onOpen} /></li> : batch.missingDecision ? <StoryItem story={batch.missingDecision} roundIndex={round.roundIndex} onOpen={onOpen} /> : null}
  41. </ol>
  42. </li>)}
  43. <StoryItem story={round.overallEvaluation} roundIndex={round.roundIndex} onOpen={onOpen} />
  44. <StoryItem story={round.result} roundIndex={round.roundIndex} onOpen={onOpen} />
  45. </ol> : null}
  46. </li>;
  47. }
  48. function SummarySequence({ round }: { round: RoundStoryV8 }) {
  49. const fields = [
  50. ["目标", round.summary.goal.headline],
  51. ["尝试", `${round.summary.approach.mode || "方式未记录"} · ${round.summary.approach.candidateCount} 个实现方案`],
  52. ["决策", round.summary.decision.batchCount ? `${round.summary.decision.batchCount} 个决策批次` : round.summary.decision.summary],
  53. ["产出", round.summary.output.summary],
  54. ] as const;
  55. return <div className="mobileSummarySequence">{fields.map(([label, value]) => <p key={label}><b>{label}</b>{value || "未记录"}</p>)}</div>;
  56. }
  57. function BranchProcess({ branch, roundIndex, expandedRetrievalAgents, onToggleRetrievalAgent, onOpen }: { branch: BranchStory; roundIndex: number; expandedRetrievalAgents: Set<string>; onToggleRetrievalAgent: Props["onToggleRetrievalAgent"]; onOpen: Props["onOpen"] }) {
  58. const stage = retrievalStageItem(branch.retrievalStage, roundIndex, branch.branchId);
  59. const [task, ...tail] = branchFlow(branch);
  60. return <ol className="mobileBranchProcess">
  61. <StoryItem story={task} roundIndex={roundIndex} branchId={branch.branchId} onOpen={onOpen} />
  62. <li className="mobileRetrievalStage">
  63. <button type="button" data-focus-return={`${stage.id}:mobile`} onClick={() => onOpen(stage, "business")}><span>取数阶段</span><strong>{modeLabel(branch.retrievalStage.observedMode)}</strong><p>工具取数和 Agent 取数属于同一阶段</p></button>
  64. <ol>
  65. {branch.retrievalStage.directToolGroups.map((group) => { const item = retrievalDirectItem(group, roundIndex, branch.branchId); return <li key={item.id}><button type="button" onClick={() => onOpen(item, "business")}><span>工具取数</span><strong>{group.sources.map((source) => source.businessLabel).join("、") || "相关信息"}</strong><p>{group.callCount} 次读取</p></button></li>; })}
  66. {branch.retrievalStage.agentRuns.map((run) => {
  67. const item = retrievalAgentItem(run, roundIndex, branch.branchId);
  68. const expanded = expandedRetrievalAgents.has(run.id);
  69. const queryListId = `mobile-query-list-${run.eventId}`;
  70. return <li key={run.id} className="mobileRetrievalAgent"><button type="button" onClick={() => onOpen(item, "business")}><span>Agent 取数</span><strong>{run.businessLabel}</strong><p>{run.querySummary.total} 次查询 · {run.querySummary.hit} 次命中 · {run.querySummary.empty} 次无结果{run.querySummary.failure ? ` · ${run.querySummary.failure} 次失败` : ""}{run.querySummary.interrupted ? ` · ${run.querySummary.interrupted} 次中断` : ""}{run.querySummary.unknown ? ` · ${run.querySummary.unknown} 次结果待确认` : ""}</p><p>{run.screening.preview || "初筛整理未记录"}</p></button>{run.attempts.length ? <button type="button" className="candidateExpand" aria-expanded={expanded} aria-controls={queryListId} onClick={() => onToggleRetrievalAgent(run.id)}>{expanded ? "收起查询" : `展开 ${run.attempts.length} 次查询`}</button> : null}{expanded ? <ol id={queryListId}>{run.attempts.map((attempt, index) => { const query = queryAttemptItem(attempt, roundIndex, branch.branchId); return <li key={attempt.id}><button type="button" onClick={() => onOpen(query, "business")}><span>{String(index + 1).padStart(2, "0")}</span><strong>{attempt.queryLabel}</strong><p>{attempt.resultCount && attempt.resultCount > 0 ? `${attempt.resultCount} 条` : attempt.status === "empty" ? "无结果" : attempt.status === "failure" ? "失败" : attempt.status === "interrupted" ? "构建中断" : "查看结果"}</p></button></li>; })}</ol> : null}</li>;
  71. })}
  72. </ol>
  73. </li>
  74. {tail.map((story) => <StoryItem key={story.id} story={story} roundIndex={roundIndex} branchId={branch.branchId} onOpen={onOpen} />)}
  75. </ol>;
  76. }
  77. function StoryItem({ story, roundIndex, branchId, onOpen }: { story: StoryNode; roundIndex?: number; branchId?: number; onOpen: Props["onOpen"] }) {
  78. const item = storyItem(story, roundIndex, branchId);
  79. const card = story.card.primary || story.card.secondary.length ? story.card : { primary: { value: "查看详情" }, secondary: [] };
  80. const label = roleLabel(story.role);
  81. if (story.decision) return <li className="mobileDecisionItem"><MobileDecision item={item} decision={story.decision} fallbackLabel={label} fallbackCard={card} onOpen={onOpen} /></li>;
  82. return <li><button type="button" data-focus-return={`${story.id}:mobile`} onClick={() => onOpen(item, "business")}><span>{label}</span>{story.title !== label ? <strong>{story.title}</strong> : null}<CardFields card={card} /></button>{story.artifactRef ? <button type="button" className="candidateExpand artifactAction" data-focus-return={`${story.id}:artifact-mobile`} onClick={() => onOpen(item, "artifact")}><FileText size={14} />{story.role === "round-result" ? "查看本轮产出脚本表" : "查看完整候选脚本"}</button> : null}</li>;
  83. }
  84. function MobileDecision({ item, decision, fallbackLabel, fallbackCard, onOpen }: { item: CanvasItem; decision?: AgentDecisionProjection; fallbackLabel: string; fallbackCard: StoryNode["card"]; onOpen: Props["onOpen"] }) {
  85. const card = decision?.card || fallbackCard;
  86. const artifactRef = item.itemType === "story" ? item.artifactRef : undefined;
  87. return <div className="mobileDecision"><button type="button" data-focus-return={`${item.id}:mobile`} onClick={() => onOpen(item, "business")}><span>{decision ? `${decision.actor.label} · ${decisionTypeLabel(decision.decisionType)}` : fallbackLabel}</span>{decision ? <em>{authorityLabel(decision.authority)}</em> : null}<strong>{item.title}</strong><CardFields card={card} /></button>{artifactRef ? <button type="button" className="candidateExpand artifactAction" data-focus-return={`${item.id}:artifact-mobile`} onClick={() => onOpen(item, "artifact")}><FileText size={14} />查看完整候选脚本</button> : null}</div>;
  88. }
  89. function roleLabel(role: StoryNode["role"]) {
  90. return ({ "planning-analysis": "创作规划解析", goal: "本轮目标", plan: "实现规划", "multipath-review": "多方案评审", "multipath-decision": "主 Agent 多路决策", "round-evaluation": "主脚本整体评审", "round-result": "本轮产出", "branch-task": "实现任务", "data-decision": "数据取舍", "candidate-output": "候选脚本", "domain-output": "领域事实" } as Partial<Record<StoryNode["role"], string>>)[role] || "运行步骤";
  91. }
  92. function modeLabel(mode: BranchStory["retrievalStage"]["observedMode"]) { return ({ single: "单项取数", sequential: "依次取数", parallel: "并行取数", mixed: "混合执行", unknown: "执行先后未确认" } as const)[mode]; }