flow.ts 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import { MarkerType, Position, type Edge, type Node } from "@xyflow/react";
  2. import type { FlowNodeRecord, Phase, PhaseFrame, RunGraph } from "@/lib/types";
  3. export type CardData = {
  4. kind: "card";
  5. item: FlowNodeRecord;
  6. dimmed: boolean;
  7. } & Record<string, unknown>;
  8. export type FrameData = {
  9. kind: "frame";
  10. frame: PhaseFrame;
  11. } & Record<string, unknown>;
  12. export type CanvasData = CardData | FrameData;
  13. export const PHASE_LABELS: Record<Phase, string> = {
  14. "phase-1": "Phase 1",
  15. "phase-2": "Phase 2",
  16. "phase-3": "Phase 3",
  17. delivery: "Final Delivery",
  18. };
  19. const EDGE_COLOR = {
  20. control: "#68707f",
  21. artifact: "#4ca6d8",
  22. validation: "#b68cff",
  23. publication: "#5ec48d",
  24. };
  25. export function matchesNode(item: FlowNodeRecord, query: string): boolean {
  26. const value = query.trim().toLocaleLowerCase();
  27. if (!value) return true;
  28. return [
  29. item.title, item.subtitle, item.id, item.lane, item.status,
  30. item.task_kind ?? "", item.agent_role ?? "", item.record.model_name,
  31. ...item.tags,
  32. ].some((part) => part.toLocaleLowerCase().includes(value));
  33. }
  34. export function buildCanvas(
  35. graph: RunGraph,
  36. enabledPhases: Set<Phase>,
  37. query: string,
  38. ): { nodes: Node<CanvasData>[]; edges: Edge[] } {
  39. const visibleIds = new Set(
  40. graph.nodes.filter((item) => enabledPhases.has(item.phase)).map((item) => item.id),
  41. );
  42. const frameNodes: Node<CanvasData>[] = graph.frames.map((frame) => {
  43. const phase = frame.id.replace("frame-", "") as Phase;
  44. return {
  45. id: frame.id,
  46. type: "phaseFrame",
  47. position: { x: frame.x, y: frame.y },
  48. data: { kind: "frame", frame },
  49. style: { width: frame.width, height: frame.height },
  50. selectable: false,
  51. draggable: false,
  52. connectable: false,
  53. hidden: !enabledPhases.has(phase),
  54. zIndex: -10,
  55. };
  56. });
  57. const cardNodes: Node<CanvasData>[] = graph.nodes.map((item) => ({
  58. id: item.id,
  59. type: "flowCard",
  60. position: { x: item.x, y: item.y },
  61. data: { kind: "card", item, dimmed: !matchesNode(item, query) },
  62. sourcePosition: Position.Right,
  63. targetPosition: Position.Left,
  64. style: { width: item.width },
  65. hidden: !visibleIds.has(item.id),
  66. zIndex: 2,
  67. }));
  68. const edges: Edge[] = graph.edges.map((edge) => ({
  69. id: edge.id,
  70. source: edge.source,
  71. target: edge.target,
  72. label: edge.label ?? undefined,
  73. type: "smoothstep",
  74. animated: edge.animated,
  75. hidden: !visibleIds.has(edge.source) || !visibleIds.has(edge.target),
  76. markerEnd: { type: MarkerType.ArrowClosed, width: 16, height: 16, color: EDGE_COLOR[edge.kind] },
  77. style: { stroke: EDGE_COLOR[edge.kind], strokeWidth: edge.kind === "publication" ? 2.2 : 1.45 },
  78. labelStyle: { fill: "#9aa0ab", fontSize: 10, fontWeight: 600 },
  79. labelBgStyle: { fill: "#15171b", fillOpacity: 0.92 },
  80. labelBgPadding: [5, 3] as [number, number],
  81. labelBgBorderRadius: 5,
  82. }));
  83. return { nodes: [...frameNodes, ...cardNodes], edges };
  84. }