RunResultCard.tsx 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940
  1. import { ArrowRight, CheckCircle2, FileCheck2 } from "lucide-react";
  2. import { Handle, Position, type NodeProps } from "@xyflow/react";
  3. import type { CanvasNodeData } from "@/lib/layout";
  4. import { runResultFallback } from "@/lib/status-copy";
  5. import { SourceNoticeBadge, StatusBadge } from "@/components/ui/Badges";
  6. import { SourceComparisonButton } from "@/components/flow/SourceComparisonButton";
  7. export function RunResultCard({ data }: NodeProps) {
  8. const { item, onOpen } = data as unknown as CanvasNodeData;
  9. if (!item || item.itemType !== "final-result") return null;
  10. const result = item.result;
  11. const artifact = result.artifact;
  12. const summary = result.summary || result.errorMessage || runResultFallback(result.status, result.terminal);
  13. const summaryPreview = resultSummaryPreview(summary);
  14. const adoptedCount = result.branchCounts.merged ?? 0;
  15. return <article className="flowCard runResultCard" data-focus-return={`${item.id}:card`} aria-label="最终结果" tabIndex={0} onKeyDown={(event) => { if (event.target === event.currentTarget && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); onOpen(item, "business"); } }}>
  16. <Handle type="target" position={Position.Left} className="flowHandle" />
  17. <header className="cardHeader"><span className="finalLabel"><FileCheck2 size={14} />最终结果</span><StatusBadge status={result.status} /></header>
  18. {result.title !== "最终结果" ? <h3>{result.title}</h3> : null}
  19. <div className="finalOutcome"><span><CheckCircle2 size={18} /></span><div><b>{result.terminal ? "当前主脚本已经形成" : "脚本构建仍在推进"}</b><p>{result.roundCount} 轮构建 · {result.branchCounts.total} 个候选方案</p></div></div>
  20. {summaryPreview ? <section className="finalSummaryPreview"><b>构建摘要</b><p>{summaryPreview}</p></section> : null}
  21. <div className="artifactStats"><span><b>{artifact.paragraphCount ?? 0}</b>段落</span><span><b>{artifact.elementCount ?? 0}</b>元素</span><span><b>{artifact.linkCount ?? 0}</b>关联</span></div>
  22. <div className="finalRunFacts"><span><b>{adoptedCount}</b> 个方案已采用</span><span><b>{result.branchCounts.discarded ?? 0}</b> 个未采用</span></div>
  23. <footer className="cardFooter"><button type="button" className="nodeAction nodrag nopan" data-focus-return={`${item.id}:business`} onClick={(event) => { event.stopPropagation(); onOpen(item, "business"); }}>查看结果详情</button><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"); }}>查看完整主脚本<ArrowRight size={14} /></button></footer>
  24. <SourceNoticeBadge notice={result.sourceNotice} />
  25. </article>;
  26. }
  27. function resultSummaryPreview(value: string) {
  28. const lines = value
  29. .replace(/\\n/g, "\n")
  30. .split(/\n+/)
  31. .map((line) => line.replace(/^\s*(?:#{1,6}\s*|[-*+]\s+|\d+[.、)]\s*)/, "").replace(/[`*_]/g, "").trim())
  32. .filter(Boolean);
  33. const text = lines.find((line) => line.length >= 18) || lines[0] || "";
  34. if (text.length <= 150) return text;
  35. const candidate = text.slice(0, 151);
  36. const punctuation = Math.max(...["。", "!", "?", ";"].map((mark) => candidate.lastIndexOf(mark)));
  37. return punctuation >= 72 ? candidate.slice(0, punctuation + 1) : `${text.slice(0, 149).trim()}…`;
  38. }