StoryCard.tsx 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637
  1. import { ArrowRight, FileText } from "lucide-react";
  2. import { Handle, Position, type NodeProps } from "@xyflow/react";
  3. import type { CanvasNodeData } from "@/lib/layout";
  4. import { SourceNoticeBadge, StatusBadge } from "@/components/ui/Badges";
  5. import { CardFields } from "@/components/flow/CardFields";
  6. import { SourceComparisonButton } from "@/components/flow/SourceComparisonButton";
  7. export function StoryCard({ data }: NodeProps) {
  8. const { item, onOpen } = data as unknown as CanvasNodeData;
  9. if (!item || item.itemType !== "story") return null;
  10. const role = roleLabel(item.role);
  11. const actor = actorLabel(item.role);
  12. const hasFields = Boolean(item.card.primary || item.card.secondary.length);
  13. return <article className={`flowCard storyCard story-${item.kind} role-${item.role}`} data-focus-return={`${item.id}:card`} aria-label={`${item.title}`} tabIndex={0} onKeyDown={(event) => { if (event.target !== event.currentTarget) return; if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onOpen(item, "business"); } }}>
  14. <Handle type="target" position={Position.Left} className="flowHandle" />
  15. <Handle type="source" position={Position.Right} className="flowHandle" />
  16. <header className="cardHeader"><div className="cardIdentity"><span className={`actorBadge actor-${actor.tone}`}>{actor.label}</span>{role !== item.title ? <span className="storyRole">{role}</span> : null}</div>{item.status ? <StatusBadge status={item.status} /> : null}</header>
  17. <h3>{item.title}</h3>
  18. <CardFields card={item.card} />
  19. <footer className="cardFooter">
  20. {item.detailRef || hasFields ? <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}
  21. <SourceComparisonButton item={item} onOpen={onOpen} />
  22. {item.artifactRef ? <button type="button" className="nodeAction nodrag nopan artifactAction" data-focus-return={`${item.id}:artifact`} onClick={(event) => { event.stopPropagation(); onOpen(item, "artifact"); }}><FileText size={14} />{item.role === "round-result" ? "查看本轮产出脚本表" : "查看完整候选脚本"}</button> : null}
  23. </footer>
  24. <SourceNoticeBadge notice={item.sourceNotice} />
  25. </article>;
  26. }
  27. function roleLabel(role: string) {
  28. return ({ "planning-analysis": "规划解析", objective: "创作目标", goal: "本轮目标", plan: "实现规划", "branch-task": "实现任务", "data-decision": "数据取舍", "candidate-output": "候选脚本", "domain-output": "领域事实", "multipath-review": "多方案评审", "multipath-decision": "多路决策", "round-evaluation": "主脚本整体评审", "round-result": "本轮产出", "final-result": "最终结果" } as Record<string, string>)[role] || "运行步骤";
  29. }
  30. function actorLabel(role: string) {
  31. if (role === "multipath-review" || role === "round-evaluation") return { label: role === "multipath-review" ? "多方案评审 Agent" : "整体评审 Agent", tone: "evaluator" };
  32. if (["branch-task", "data-decision", "candidate-output", "domain-output"].includes(role)) return { label: "实现 Agent", tone: "implementer" };
  33. return { label: "主 Agent", tone: "main" };
  34. }