-
Notifications
You must be signed in to change notification settings - Fork 0
Add Graph Auto-Layout engine and Execution Metrics Summary banner #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| $ vite --host 0.0.0.0 --port 3000 | ||
|
|
||
| VITE v5.4.21 ready in 312 ms | ||
| VITE v5.4.21 ready in 372 ms | ||
|
|
||
| ➜ Local: http://localhost:3000/ | ||
| ➜ Network: http://192.168.0.2:3000/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| import type { Node, Edge } from "reactflow"; | ||
| import type { AgentNodeData } from "./types"; | ||
|
|
||
| export type LayoutDirection = "TB" | "LR"; | ||
|
|
||
| /** | ||
| * Computes an automatic hierarchical layout for workflow nodes. | ||
| * Supports Top-to-Bottom (TB) and Left-to-Right (LR) flow directions. | ||
| * | ||
| * @param nodes Array of ReactFlow nodes to layout. | ||
| * @param edges Array of ReactFlow edges defining node connections. | ||
| * @param direction "TB" (Top-to-Bottom) or "LR" (Left-to-Right). | ||
| * @returns A new array of nodes with updated position coordinates. | ||
| */ | ||
| export function autoLayoutGraph( | ||
| nodes: Node<AgentNodeData>[], | ||
| edges: Edge[], | ||
| direction: LayoutDirection = "TB" | ||
| ): Node<AgentNodeData>[] { | ||
| if (nodes.length === 0) return []; | ||
|
|
||
| const nodeMap = new Map<string, Node<AgentNodeData>>(nodes.map((n) => [n.id, n])); | ||
| const inDegree = new Map<string, number>(); | ||
| const outEdges = new Map<string, string[]>(); | ||
|
|
||
| nodes.forEach((n) => { | ||
| inDegree.set(n.id, 0); | ||
| outEdges.set(n.id, []); | ||
| }); | ||
|
|
||
| edges.forEach((e) => { | ||
| if (e.source !== e.target && nodeMap.has(e.source) && nodeMap.has(e.target)) { | ||
| inDegree.set(e.target, (inDegree.get(e.target) || 0) + 1); | ||
| const list = outEdges.get(e.source) || []; | ||
| list.push(e.target); | ||
| outEdges.set(e.source, list); | ||
| } | ||
| }); | ||
|
|
||
| // Calculate ranks | ||
| const ranks = new Map<string, number>(); | ||
|
|
||
| // Entry nodes (inDegree === 0 or marked as entry) get rank 0 | ||
| const queue: string[] = []; | ||
| nodes.forEach((n) => { | ||
| if ((inDegree.get(n.id) || 0) === 0 || n.data?.isEntry || n.data?.kind === "trigger") { | ||
| ranks.set(n.id, 0); | ||
| queue.push(n.id); | ||
| } | ||
| }); | ||
|
|
||
| // Fallback if graph is completely cyclic or has no explicit entry points | ||
| if (queue.length === 0) { | ||
| nodes.forEach((n) => { | ||
| ranks.set(n.id, 0); | ||
| queue.push(n.id); | ||
| }); | ||
| } | ||
|
|
||
| // Topological / BFS rank propagation with iteration cap to prevent infinite loops on cycles | ||
| let maxPasses = nodes.length * 2; | ||
| while (queue.length > 0 && maxPasses > 0) { | ||
| maxPasses--; | ||
| const currId = queue.shift()!; | ||
| const currRank = ranks.get(currId) || 0; | ||
|
|
||
| const targets = outEdges.get(currId) || []; | ||
| targets.forEach((targetId) => { | ||
| const existingRank = ranks.get(targetId); | ||
| const nextRank = currRank + 1; | ||
| if (existingRank === undefined || nextRank > existingRank) { | ||
| ranks.set(targetId, nextRank); | ||
| queue.push(targetId); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Ensure every node has a rank | ||
| nodes.forEach((n) => { | ||
| if (!ranks.has(n.id)) { | ||
| ranks.set(n.id, 0); | ||
| } | ||
| }); | ||
|
|
||
| // Group nodes by rank | ||
| const rankGroups = new Map<number, Node<AgentNodeData>[]>(); | ||
| nodes.forEach((n) => { | ||
| const r = ranks.get(n.id) || 0; | ||
| const group = rankGroups.get(r) || []; | ||
| group.push(n); | ||
| rankGroups.set(r, group); | ||
| }); | ||
|
|
||
| const isTB = direction === "TB"; | ||
| // Dimension constants for layout calculation | ||
| const colSpacing = isTB ? 260 : 300; | ||
| const rowSpacing = isTB ? 160 : 150; | ||
|
|
||
| const updatedNodes: Node<AgentNodeData>[] = []; | ||
|
|
||
| rankGroups.forEach((group, rank) => { | ||
| const count = group.length; | ||
| // Offset to center smaller ranks relative to maxBreadth | ||
| const groupWidth = count * colSpacing; | ||
|
|
||
| group.forEach((node, index) => { | ||
| let x = 0; | ||
| let y = 0; | ||
|
|
||
| if (isTB) { | ||
| // Top-to-Bottom: ranks are vertical Y levels, nodes in rank are spaced along X | ||
| x = index * colSpacing - groupWidth / 2 + colSpacing / 2 + 400; | ||
| y = rank * rowSpacing + 80; | ||
| } else { | ||
| // Left-to-Right: ranks are horizontal X levels, nodes in rank are spaced along Y | ||
| x = rank * colSpacing + 80; | ||
| y = index * rowSpacing - (count * rowSpacing) / 2 + rowSpacing / 2 + 300; | ||
| } | ||
|
|
||
| updatedNodes.push({ | ||
| ...node, | ||
| position: { | ||
| x: Math.round(x), | ||
| y: Math.round(y), | ||
| }, | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| return updatedNodes; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -64,6 +64,7 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| StatePreset, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cryptoId as presetCryptoId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@/flow/statePresets"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { autoLayoutGraph } from "@/flow/graphLayout"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const nodeTypes = { agent: AgentNode, note: NoteNode }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1434,6 +1435,34 @@ function Canvas() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="hidden md:inline font-mono text-[10px] text-[hsl(var(--ink-faint))]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {nodes.length} nodes · {edges.length} edges | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Auto Layout Controls */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center border border-dashed border-[hsl(var(--ink))]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| snapshot(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const relaid = autoLayoutGraph(nodes, edges, "TB"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setNodes(relaid); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toast.success("Auto-layout applied (Top to Bottom)"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title="Auto-layout graph top-to-bottom" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="font-mono text-[10px] sm:text-[11px] px-1.5 py-1 hover:bg-[hsl(var(--ink))] hover:text-[hsl(var(--paper))] transition-colors border-r border-dashed border-[hsl(var(--ink))]" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ⚡ layout TB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| snapshot(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const relaid = autoLayoutGraph(nodes, edges, "LR"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setNodes(relaid); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toast.success("Auto-layout applied (Left to Right)"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title="Auto-layout graph left-to-right" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="font-mono text-[10px] sm:text-[11px] px-1.5 py-1 hover:bg-[hsl(var(--ink))] hover:text-[hsl(var(--paper))] transition-colors" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LR | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Canvas Theme Selector */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <select | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={canvasTheme} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2223,6 +2252,61 @@ function Canvas() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Execution Metrics Summary Banner */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {runLogs && runLogs.length > 0 && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="border border-dashed p-3 space-y-2 mb-2 transition-all" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| borderColor: runLogs.some((l) => l.error) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "hsl(var(--issue))" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : "hsl(var(--edge-selected))", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: runLogs.some((l) => l.error) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "hsl(var(--issue)/0.04)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : "hsl(var(--edge-selected)/0.04)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-between"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="font-mono text-[10px] uppercase tracking-[0.15em] font-bold text-[hsl(var(--ink))]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Execution Metrics Summary | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={`font-mono text-[9px] uppercase tracking-wider font-bold px-2 py-0.5 border border-dashed ${ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runLogs.some((l) => l.error) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "bg-[hsl(var(--issue))] text-[hsl(var(--paper))] border-transparent" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : "bg-[hsl(var(--edge-selected))] text-[hsl(var(--paper))] border-transparent" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {runLogs.some((l) => l.error) ? "⚠ FAILED" : "✓ PASSED"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2256
to
+2279
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Do not mark an active run as passed.
Proposed fix- {runLogs && runLogs.length > 0 && (
+ {!running && runLogs && runLogs.length > 0 && (📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="grid grid-cols-3 gap-2 pt-1 font-mono text-[10px]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="p-1.5 border border-dashed border-[hsl(var(--grid-line))] bg-[hsl(var(--paper))] text-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="text-[9px] text-[hsl(var(--ink-faint))] uppercase">Total Duration</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="font-bold text-[hsl(var(--ink))] mt-0.5"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {runLogs.reduce((sum, l) => sum + (l.ms || 0), 0) >= 1000 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? `${(runLogs.reduce((sum, l) => sum + (l.ms || 0), 0) / 1000).toFixed(2)}s` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : `${runLogs.reduce((sum, l) => sum + (l.ms || 0), 0)}ms`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="p-1.5 border border-dashed border-[hsl(var(--grid-line))] bg-[hsl(var(--paper))] text-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="text-[9px] text-[hsl(var(--ink-faint))] uppercase">Step Count</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="font-bold text-[hsl(var(--ink))] mt-0.5"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {runLogs.length} step{runLogs.length === 1 ? "" : "s"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="p-1.5 border border-dashed border-[hsl(var(--grid-line))] bg-[hsl(var(--paper))] text-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="text-[9px] text-[hsl(var(--ink-faint))] uppercase">Node Types</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="font-bold text-[hsl(var(--ink))] mt-0.5"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {new Set(runLogs.map((l) => l.kind).filter((k) => k !== "runtime")).size} unique | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Run logs management header/tools */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {runLogs && runLogs.length > 0 && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="border border-dashed border-[hsl(var(--grid-line))] p-3 space-y-2 mb-2 bg-[hsl(var(--ink)/0.01)]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { autoLayoutGraph } from "../flow/graphLayout"; | ||
| import type { Node, Edge } from "reactflow"; | ||
| import type { AgentNodeData } from "../flow/types"; | ||
|
|
||
| describe("autoLayoutGraph", () => { | ||
| const sampleNodes: Node<AgentNodeData>[] = [ | ||
| { | ||
| id: "n1", | ||
| type: "agent", | ||
| position: { x: 0, y: 0 }, | ||
| data: { kind: "trigger", name: "Trigger Node", config: {}, isEntry: true }, | ||
| }, | ||
| { | ||
| id: "n2", | ||
| type: "agent", | ||
| position: { x: 0, y: 0 }, | ||
| data: { kind: "llm", name: "LLM Node", config: {} }, | ||
| }, | ||
| { | ||
| id: "n3", | ||
| type: "agent", | ||
| position: { x: 0, y: 0 }, | ||
| data: { kind: "sink", name: "Sink Node", config: {}, isTerminal: true }, | ||
| }, | ||
| ]; | ||
|
|
||
| const sampleEdges: Edge[] = [ | ||
| { id: "e1-2", source: "n1", target: "n2" }, | ||
| { id: "e2-3", source: "n2", target: "n3" }, | ||
| ]; | ||
|
|
||
| it("arranges nodes hierarchically in Top-to-Bottom (TB) direction", () => { | ||
| const relaid = autoLayoutGraph(sampleNodes, sampleEdges, "TB"); | ||
|
|
||
| expect(relaid).toHaveLength(3); | ||
|
|
||
| const n1 = relaid.find((n) => n.id === "n1")!; | ||
| const n2 = relaid.find((n) => n.id === "n2")!; | ||
| const n3 = relaid.find((n) => n.id === "n3")!; | ||
|
|
||
| // In TB mode, Y coordinate increases with rank level | ||
| expect(n1.position.y).toBeLessThan(n2.position.y); | ||
| expect(n2.position.y).toBeLessThan(n3.position.y); | ||
|
|
||
| // Node data and properties should be preserved | ||
| expect(n1.data.kind).toBe("trigger"); | ||
| expect(n3.data.isTerminal).toBe(true); | ||
| }); | ||
|
|
||
| it("arranges nodes hierarchically in Left-to-Right (LR) direction", () => { | ||
| const relaid = autoLayoutGraph(sampleNodes, sampleEdges, "LR"); | ||
|
|
||
| expect(relaid).toHaveLength(3); | ||
|
|
||
| const n1 = relaid.find((n) => n.id === "n1")!; | ||
| const n2 = relaid.find((n) => n.id === "n2")!; | ||
| const n3 = relaid.find((n) => n.id === "n3")!; | ||
|
|
||
| // In LR mode, X coordinate increases with rank level | ||
| expect(n1.position.x).toBeLessThan(n2.position.x); | ||
| expect(n2.position.x).toBeLessThan(n3.position.x); | ||
| }); | ||
|
|
||
| it("handles disconnected nodes and multiple components gracefully", () => { | ||
| const disconnectedNodes: Node<AgentNodeData>[] = [ | ||
| ...sampleNodes, | ||
| { | ||
| id: "orphan", | ||
| type: "note", | ||
| position: { x: 10, y: 10 }, | ||
| data: { kind: "note", name: "Sticky Note", config: { content: "Doc" } }, | ||
| }, | ||
| ]; | ||
|
|
||
| const relaid = autoLayoutGraph(disconnectedNodes, sampleEdges, "TB"); | ||
| expect(relaid).toHaveLength(4); | ||
|
|
||
| const orphan = relaid.find((n) => n.id === "orphan"); | ||
| expect(orphan).toBeDefined(); | ||
| expect(orphan?.position).toBeDefined(); | ||
| }); | ||
|
|
||
| it("handles cyclic graph structures without infinite loop", () => { | ||
| const cyclicEdges: Edge[] = [ | ||
| { id: "e1-2", source: "n1", target: "n2" }, | ||
| { id: "e2-3", source: "n2", target: "n3" }, | ||
| { id: "e3-1", source: "n3", target: "n1" }, // cycle back | ||
| ]; | ||
|
|
||
| const relaid = autoLayoutGraph(sampleNodes, cyclicEdges, "TB"); | ||
| expect(relaid).toHaveLength(3); | ||
| relaid.forEach((n) => { | ||
| expect(typeof n.position.x).toBe("number"); | ||
| expect(typeof n.position.y).toBe("number"); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Stop rank propagation through cyclic components.
Lines 52-75 increase ranks on every cycle traversal until
maxPassesexpires. A cycle reachable from an entry node can therefore receive different positions when an unrelated disconnected node is added, because that node increasesmaxPasses.Condense strongly connected components before rank propagation. Assign ranks on the resulting acyclic component graph. Add a regression test for an entry node that reaches a cycle plus an unrelated node.
🤖 Prompt for AI Agents