Skip to content

Commit 9901c85

Browse files
feat: add graph visualization component (#370)
Co-authored-by: bracesproul <[email protected]>
1 parent c844382 commit 9901c85

File tree

6 files changed

+2398
-2324
lines changed

6 files changed

+2398
-2324
lines changed

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@supabase/ssr": "^0.6.1",
4444
"@supabase/supabase-js": "^2.49.4",
4545
"@types/js-yaml": "^4.0.9",
46+
"@xyflow/react": "^12.8.4",
4647
"class-variance-authority": "^0.7.1",
4748
"clsx": "^2.1.1",
4849
"cmdk": "^1.1.1",

apps/web/src/app/api/settings/api-keys/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export async function POST(request: NextRequest) {
5353
{
5454
user_id: userId,
5555
api_keys: nonNullApiKeys,
56-
},
56+
} as any,
5757
{
5858
onConflict: "user_id",
5959
},
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
"use client";
2+
3+
import React, { useMemo } from "react";
4+
import {
5+
ReactFlow,
6+
Edge,
7+
Background,
8+
Controls,
9+
BackgroundVariant,
10+
} from "@xyflow/react";
11+
import "@xyflow/react/dist/style.css";
12+
import {
13+
StartEndNodeComponent,
14+
MainAgentNodeComponent,
15+
SubAgentNodeComponent,
16+
ToolNodeComponent,
17+
} from "./nodes";
18+
import { ConfigurableFieldSubAgentsMetadata } from "@/types/configurable";
19+
20+
interface AgentGraphVisualizationProps {
21+
configurable: Record<string, any>;
22+
name: string;
23+
}
24+
25+
interface SubAgentNode {
26+
id: string;
27+
type: "subagent";
28+
position: { x: number; y: number };
29+
data: { label: string; type: "subagent" };
30+
}
31+
32+
interface ToolNode {
33+
id: string;
34+
type: "tool";
35+
position: { x: number; y: number };
36+
data: { label: string; type: "tool" };
37+
}
38+
39+
interface StartEndNode {
40+
id: string;
41+
type: "startEnd";
42+
position: { x: number; y: number };
43+
data: { label: string; type: "start" | "end" };
44+
}
45+
46+
interface MainAgentNode {
47+
id: string;
48+
type: "mainAgent";
49+
position: { x: number; y: number };
50+
data: { label: string; type: "mainAgent" };
51+
}
52+
53+
type GraphNode = SubAgentNode | ToolNode | StartEndNode | MainAgentNode;
54+
55+
const nodeTypes = {
56+
startEnd: StartEndNodeComponent,
57+
mainAgent: MainAgentNodeComponent,
58+
subagent: SubAgentNodeComponent,
59+
tool: ToolNodeComponent,
60+
};
61+
62+
export function AgentGraphVisualization({
63+
configurable,
64+
name,
65+
}: AgentGraphVisualizationProps) {
66+
const { nodes, edges } = useMemo(() => {
67+
const nodes: GraphNode[] = [];
68+
const edges: Edge[] = [];
69+
70+
let currentY = 50;
71+
const centerX = 400;
72+
const nodeSpacing = 150;
73+
74+
nodes.push({
75+
id: "start",
76+
type: "startEnd",
77+
position: { x: centerX - 50, y: currentY },
78+
data: { label: "Start", type: "start" },
79+
});
80+
81+
currentY += nodeSpacing;
82+
83+
nodes.push({
84+
id: "main-agent",
85+
type: "mainAgent",
86+
position: { x: centerX - 80, y: currentY },
87+
data: { label: name, type: "mainAgent" },
88+
});
89+
90+
edges.push({
91+
id: "start-to-agent",
92+
source: "start",
93+
target: "main-agent",
94+
type: "smoothstep",
95+
sourceHandle: null,
96+
targetHandle: null,
97+
});
98+
99+
currentY += nodeSpacing;
100+
101+
// Get subagents from configurable
102+
const subagents: NonNullable<
103+
ConfigurableFieldSubAgentsMetadata["default"]
104+
> = configurable?.subagents || [];
105+
106+
// Layout subagents horizontally
107+
if (subagents.length > 0) {
108+
const subagentSpacing = 280;
109+
const subagentWidth = 140;
110+
const totalWidth = (subagents.length - 1) * subagentSpacing;
111+
const startX = centerX - totalWidth / 2 - subagentWidth / 2;
112+
113+
subagents.forEach((subagent, index) => {
114+
const nodeId = `subagent-${index}`;
115+
const subagentLabel =
116+
typeof subagent === "string"
117+
? subagent
118+
: subagent?.name || String(subagent);
119+
nodes.push({
120+
id: nodeId,
121+
type: "subagent",
122+
position: { x: startX + index * subagentSpacing, y: currentY },
123+
data: { label: subagentLabel, type: "subagent" },
124+
});
125+
126+
edges.push({
127+
id: `agent-to-${nodeId}`,
128+
source: "main-agent",
129+
target: nodeId,
130+
type: "smoothstep",
131+
});
132+
});
133+
134+
currentY += nodeSpacing;
135+
}
136+
137+
// Layout tools below subagents - each subagent gets its own tools
138+
if (subagents.length > 0) {
139+
let maxToolsCount = 0;
140+
141+
subagents.forEach((subagent) => {
142+
const subagentTools = subagent?.tools || [];
143+
maxToolsCount = Math.max(maxToolsCount, subagentTools.length);
144+
});
145+
146+
subagents.forEach((subagent, subagentIndex) => {
147+
const subagentSpacing = 280;
148+
const subagentWidth = 140;
149+
const totalWidth = (subagents.length - 1) * subagentSpacing;
150+
const startX = centerX - totalWidth / 2 - subagentWidth / 2;
151+
const subagentX = startX + subagentIndex * subagentSpacing;
152+
153+
// Get tools for this specific subagent
154+
const subagentTools = subagent?.tools || [];
155+
156+
// Create tools for this specific subagent
157+
subagentTools.forEach((tool, toolIndex) => {
158+
const nodeId = `tool-${subagentIndex}-${toolIndex}`;
159+
const toolLabel = tool;
160+
161+
const toolSpacing = 90;
162+
const totalToolsHeight = (maxToolsCount - 1) * toolSpacing;
163+
const thisSubagentHeight = (subagentTools.length - 1) * toolSpacing;
164+
const offset = (totalToolsHeight - thisSubagentHeight) / 2;
165+
166+
const toolY = currentY + offset + toolIndex * toolSpacing;
167+
168+
nodes.push({
169+
id: nodeId,
170+
type: "tool",
171+
position: { x: subagentX, y: toolY },
172+
data: { label: toolLabel, type: "tool" },
173+
});
174+
175+
// Only connect first tool to subagent, other tools are just stacked
176+
if (toolIndex === 0) {
177+
edges.push({
178+
id: `subagent-${subagentIndex}-to-${nodeId}`,
179+
source: `subagent-${subagentIndex}`,
180+
target: nodeId,
181+
type: "smoothstep",
182+
style: {
183+
strokeDasharray: "5,5",
184+
strokeWidth: 2,
185+
stroke: "#64748b",
186+
},
187+
});
188+
}
189+
});
190+
});
191+
192+
// Update currentY to account for the tallest column of tools
193+
if (maxToolsCount > 0) {
194+
currentY += nodeSpacing + (maxToolsCount - 1) * 90;
195+
}
196+
}
197+
198+
// End node - center it below the middle subagent or main agent
199+
const endX = subagents.length > 0 ? centerX - 50 : centerX - 50;
200+
201+
nodes.push({
202+
id: "end",
203+
type: "startEnd",
204+
position: { x: endX, y: currentY },
205+
data: { label: "End", type: "end" },
206+
});
207+
208+
// Connect terminal nodes to end - always from subagents to end
209+
if (subagents.length > 0) {
210+
// Subagents always connect to end (tools are intermediate)
211+
subagents.forEach((_, index) => {
212+
edges.push({
213+
id: `subagent-${index}-to-end`,
214+
source: `subagent-${index}`,
215+
target: "end",
216+
type: "smoothstep",
217+
});
218+
});
219+
} else {
220+
// If no subagents, main agent connects to end
221+
edges.push({
222+
id: "main-agent-to-end",
223+
source: "main-agent",
224+
target: "end",
225+
type: "smoothstep",
226+
});
227+
}
228+
229+
return { nodes, edges };
230+
}, [configurable, name]);
231+
232+
return (
233+
<div className="h-full min-h-[80vh] w-full rounded-lg border bg-gray-50">
234+
<ReactFlow
235+
nodes={nodes}
236+
edges={edges}
237+
nodeTypes={nodeTypes}
238+
fitView
239+
attributionPosition="bottom-left"
240+
proOptions={{ hideAttribution: true }}
241+
defaultEdgeOptions={{
242+
type: "smoothstep",
243+
style: { strokeWidth: 2, stroke: "#64748b" },
244+
markerEnd: {
245+
type: "arrowclosed",
246+
color: "#64748b",
247+
},
248+
}}
249+
>
250+
<Background
251+
variant={BackgroundVariant.Dots}
252+
gap={20}
253+
size={1}
254+
/>
255+
<Controls />
256+
</ReactFlow>
257+
</div>
258+
);
259+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { Handle, Position } from "@xyflow/react";
5+
import "@xyflow/react/dist/style.css";
6+
7+
interface StartEndNodeData {
8+
label: string;
9+
type: "start" | "end";
10+
}
11+
12+
export function StartEndNodeComponent({ data }: { data: StartEndNodeData }) {
13+
return (
14+
<div className="min-w-[100px] rounded-lg border-2 border-gray-800 bg-white px-6 py-3 text-center font-medium shadow-lg">
15+
{data.type === "start" && (
16+
<Handle
17+
type="source"
18+
position={Position.Bottom}
19+
className="!bg-gray-400"
20+
style={{ left: "50%", transform: "translateX(-50%)" }}
21+
/>
22+
)}
23+
<div className="flex items-center justify-center">{data.label}</div>
24+
{data.type === "end" && (
25+
<Handle
26+
type="target"
27+
position={Position.Top}
28+
className="!bg-gray-400"
29+
style={{ left: "50%", transform: "translateX(-50%)" }}
30+
/>
31+
)}
32+
</div>
33+
);
34+
}
35+
36+
interface MainAgentNodeData {
37+
label: string;
38+
type: "mainAgent";
39+
}
40+
41+
export function MainAgentNodeComponent({ data }: { data: MainAgentNodeData }) {
42+
return (
43+
<div className="min-w-[160px] rounded-lg border-2 border-yellow-500 bg-yellow-100 px-6 py-3 text-center font-medium shadow-lg">
44+
<Handle
45+
type="target"
46+
position={Position.Top}
47+
className="!bg-gray-400"
48+
style={{ left: "50%", transform: "translateX(-50%)" }}
49+
/>
50+
<div className="flex items-center justify-center">{data.label}</div>
51+
<Handle
52+
type="source"
53+
position={Position.Bottom}
54+
className="!bg-gray-400"
55+
style={{ left: "50%", transform: "translateX(-50%)" }}
56+
/>
57+
</div>
58+
);
59+
}
60+
61+
interface SubAgentNodeData {
62+
label: string;
63+
type: "subagent";
64+
}
65+
66+
export function SubAgentNodeComponent({ data }: { data: SubAgentNodeData }) {
67+
return (
68+
<div className="min-w-[140px] rounded-lg border-2 border-purple-500 bg-purple-100 px-4 py-4 text-center shadow-lg">
69+
<Handle
70+
type="target"
71+
position={Position.Top}
72+
className="!bg-gray-400"
73+
style={{ left: "50%", transform: "translateX(-50%)" }}
74+
/>
75+
<div className="flex flex-col items-center justify-center space-y-2">
76+
<div className="text-sm font-medium">{data.label}</div>
77+
<div className="rounded bg-purple-200 px-3 py-1 text-xs text-purple-800">
78+
subagent
79+
</div>
80+
</div>
81+
<Handle
82+
type="source"
83+
position={Position.Bottom}
84+
className="!bg-gray-400"
85+
style={{ left: "50%", transform: "translateX(-50%)" }}
86+
/>
87+
</div>
88+
);
89+
}
90+
91+
interface ToolNodeData {
92+
label: string;
93+
type: "tool";
94+
}
95+
96+
export function ToolNodeComponent({ data }: { data: ToolNodeData }) {
97+
return (
98+
<div className="min-w-[140px] rounded-lg border-2 border-green-500 bg-green-100 px-4 py-4 text-center shadow-lg">
99+
<Handle
100+
type="target"
101+
position={Position.Top}
102+
className="!bg-gray-400"
103+
style={{ left: "50%", transform: "translateX(-50%)" }}
104+
/>
105+
<div className="flex flex-col items-center justify-center space-y-2">
106+
<div className="text-sm font-medium">{data.label}</div>
107+
<div className="rounded bg-green-200 px-3 py-1 text-xs text-green-800">
108+
Tool
109+
</div>
110+
</div>
111+
<Handle
112+
type="source"
113+
position={Position.Bottom}
114+
className="!bg-gray-400"
115+
style={{ left: "50%", transform: "translateX(-50%)" }}
116+
/>
117+
</div>
118+
);
119+
}

0 commit comments

Comments
 (0)