AgentDecisionCard.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
  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 type { AgentDecisionProjection, CanvasItem } from "@/lib/types";
  5. import { CardFields } from "@/components/flow/CardFields";
  6. import { SourceComparisonButton } from "@/components/flow/SourceComparisonButton";
  7. export function AgentDecisionCard({ data }: NodeProps) {
  8. const node = data as unknown as CanvasNodeData;
  9. const item = node.item;
  10. if (!item) return null;
  11. const decision = decisionFor(item);
  12. if (!decision) return null;
  13. return <article
  14. className={`flowCard agentDecisionCard decision-${decision.decisionType}`}
  15. data-focus-return={`${decision.id}:card`}
  16. tabIndex={0}
  17. aria-label={`${decision.actor.label}:${item.title}`}
  18. onKeyDown={(event) => {
  19. if (event.target !== event.currentTarget) return;
  20. if (event.key === "Enter" || event.key === " ") {
  21. event.preventDefault();
  22. node.onOpen(item, "business");
  23. }
  24. }}
  25. >
  26. <Handle type="target" position={Position.Left} className="flowHandle" />
  27. <Handle type="source" position={Position.Right} className="flowHandle" />
  28. <header className="cardHeader">
  29. <div className="cardIdentity">
  30. <span className={`actorBadge actor-${actorTone(decision)}`}>{decision.actor.label}</span>
  31. <span className="storyRole">{decisionTypeLabel(decision.decisionType)}</span>
  32. </div>
  33. <span className={`authorityBadge authority-${decision.authority}`}>{authorityLabel(decision.authority)}</span>
  34. </header>
  35. <h3>{item.title}</h3>
  36. <CardFields card={decision.card} />
  37. <footer className="cardFooter">
  38. <button type="button" className="nodeAction nodrag nopan" onClick={(event) => { event.stopPropagation(); node.onOpen(item, "business"); }}>{item.itemType === "story" && item.role === "objective" ? "查看详情" : "查看决策"}<ArrowRight size={14} /></button>
  39. <SourceComparisonButton item={item} onOpen={node.onOpen} />
  40. {item.itemType === "story" && item.artifactRef ? <button type="button" className="nodeAction nodrag nopan artifactAction" data-focus-return={`${item.id}:artifact`} onClick={(event) => { event.stopPropagation(); node.onOpen(item, "artifact"); }}><FileText size={14} />查看完整候选脚本</button> : null}
  41. </footer>
  42. </article>;
  43. }
  44. export function decisionFor(item: CanvasItem): AgentDecisionProjection | undefined {
  45. if (item.itemType === "story") return item.decision;
  46. if (item.itemType === "multipath-review") return item.review.decisionProjection;
  47. if (item.itemType === "multipath-decision") return item.decision.decisionProjection;
  48. return undefined;
  49. }
  50. function actorTone(decision: AgentDecisionProjection) {
  51. if (decision.actor.role === "main") return "main";
  52. if (decision.actor.role === "implementer") return "implementer";
  53. return "evaluator";
  54. }
  55. export const authorityLabel = (authority: AgentDecisionProjection["authority"]) => ({ final: "最终决定", recommendation: "评审建议", "implementation-scope": "本方案内有效" } as const)[authority];
  56. export const decisionTypeLabel = (type: AgentDecisionProjection["decisionType"]) => ({ direction: "方向决定", tradeoff: "取舍决定", evaluation: "评审判断", creative: "创作处理" } as const)[type];