RoundSummaryCard.tsx 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243
  1. import { ArrowRight, ChevronDown, FileText } from "lucide-react";
  2. import { Handle, Position, type NodeProps } from "@xyflow/react";
  3. import type { CanvasNodeData } from "@/lib/layout";
  4. import { StatusBadge } from "@/components/ui/Badges";
  5. import { SourceComparisonButton } from "@/components/flow/SourceComparisonButton";
  6. export function RoundSummaryCard({ data }: NodeProps) {
  7. const { item, onOpen, onToggleRound } = data as unknown as CanvasNodeData;
  8. if (!item || item.itemType !== "round-summary") return null;
  9. const summary = item.round.summary;
  10. const approach = `${summary.approach.mode || "方式未记录"} · ${summary.approach.candidateCount} 个实现方案`;
  11. const decisionMeta = summary.decision.batchCount
  12. ? `${summary.decision.batchCount} 个决策批次${summary.decision.branchIds.length ? ` · 涉及方案 ${summary.decision.branchIds.join("、")}` : ""}`
  13. : summary.decision.summary;
  14. const outputs = [
  15. summary.output.mergedContentCount ? summary.output.scriptSummary : null,
  16. summary.output.domainFactCount ? summary.output.domainSummary : null,
  17. ].filter((value): value is string => Boolean(value));
  18. if (!outputs.length) outputs.push(summary.output.summary);
  19. return <article className="flowCard roundSummaryCard" data-focus-return={`${item.id}:card`} aria-label={`第 ${item.roundIndex} 轮摘要`} tabIndex={0} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onOpen(item, "business"); } }}>
  20. <Handle type="target" position={Position.Left} className="flowHandle" /><Handle type="source" position={Position.Right} className="flowHandle" />
  21. <header className="cardHeader"><span className="roundNumber">第 {item.roundIndex} 轮</span><StatusBadge status={item.round.state} /></header>
  22. <div className="roundSummaryContent">
  23. <section className="roundGoalSummary">
  24. <b>本轮目标</b>
  25. <p>{summary.goal.headline}</p>
  26. {summary.goal.itemCount ? <small>{summary.goal.itemCount} 项具体任务 · 完整内容见详情</small> : summary.goal.truncated ? <small>完整内容见详情</small> : null}
  27. </section>
  28. <div className="roundSummaryFacts">
  29. <SummaryBlock label="尝试方案" value={approach} />
  30. <SummaryBlock label="主 Agent 决策" value={decisionMeta} detail={summary.decision.batchCount ? summary.decision.summary : undefined} tone={summary.decision.batchCount ? "decision" : "warning"} />
  31. <SummaryBlock label="本轮产出" values={outputs} tone={summary.output.mergedContentCount || summary.output.domainFactCount ? "success" : "normal"} />
  32. </div>
  33. </div>
  34. <footer className="cardFooter"><button type="button" className="nodeAction nodrag nopan" onClick={(event) => { event.stopPropagation(); onToggleRound(item.roundIndex); }}><ChevronDown size={14} />展开本轮</button>{item.detailRef ? <button type="button" className="nodeAction nodrag nopan" data-focus-return={`${item.id}:business`} onClick={(event) => { event.stopPropagation(); onOpen(item, "business"); }}>查看详情<ArrowRight size={14} /></button> : null}<SourceComparisonButton item={item} onOpen={onOpen} /><button type="button" className="nodeAction nodrag nopan artifactAction" data-focus-return={`${item.id}:artifact`} onClick={(event) => { event.stopPropagation(); onOpen(item, "artifact"); }}><FileText size={14} />查看本轮产出脚本表</button></footer>
  35. </article>;
  36. }
  37. function SummaryBlock({ label, value, values, detail, tone = "normal" }: { label: string; value?: string; values?: string[]; detail?: string; tone?: "normal" | "decision" | "success" | "warning" }) {
  38. const lines = values || (value ? [value] : []);
  39. return <section className={`roundSummaryBlock summaryTone-${tone}`}><b>{label}</b><div>{lines.map((line) => <p key={line}>{line}</p>)}{detail && detail !== value ? <small>{detail}</small> : null}</div></section>;
  40. }