pydantic-graph-studio 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydantic_graph_studio/__init__.py +55 -0
- pydantic_graph_studio/cli.py +201 -0
- pydantic_graph_studio/introspection.py +209 -0
- pydantic_graph_studio/runtime.py +433 -0
- pydantic_graph_studio/schemas.py +99 -0
- pydantic_graph_studio/server.py +175 -0
- pydantic_graph_studio/ui/__init__.py +1 -0
- pydantic_graph_studio/ui/_build/tailwind.input.css +11 -0
- pydantic_graph_studio/ui/assets/app.js +783 -0
- pydantic_graph_studio/ui/assets/dagre.min.js +3809 -0
- pydantic_graph_studio/ui/assets/react-dom.production.min.js +267 -0
- pydantic_graph_studio/ui/assets/react.production.min.js +31 -0
- pydantic_graph_studio/ui/assets/reactflow.css +406 -0
- pydantic_graph_studio/ui/assets/reactflow.min.js +10 -0
- pydantic_graph_studio/ui/assets/tailwind.css +1 -0
- pydantic_graph_studio/ui/assets/theme.css +145 -0
- pydantic_graph_studio/ui/index.html +19 -0
- pydantic_graph_studio-0.1.0.dist-info/METADATA +42 -0
- pydantic_graph_studio-0.1.0.dist-info/RECORD +21 -0
- pydantic_graph_studio-0.1.0.dist-info/WHEEL +4 -0
- pydantic_graph_studio-0.1.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const e = React.createElement;
|
|
3
|
+
const { useEffect, useMemo, useRef, useState } = React;
|
|
4
|
+
const RF = ReactFlow;
|
|
5
|
+
const {
|
|
6
|
+
ReactFlow: Flow,
|
|
7
|
+
Background,
|
|
8
|
+
Controls,
|
|
9
|
+
BaseEdge,
|
|
10
|
+
Handle,
|
|
11
|
+
MiniMap,
|
|
12
|
+
getSmoothStepPath,
|
|
13
|
+
useEdgesState,
|
|
14
|
+
useNodesState,
|
|
15
|
+
} = RF;
|
|
16
|
+
const dagreLib = window.dagre;
|
|
17
|
+
const hasDagre = dagreLib && dagreLib.graphlib && typeof dagreLib.layout === "function";
|
|
18
|
+
|
|
19
|
+
const statusClasses = {
|
|
20
|
+
idle: "studio-node--idle",
|
|
21
|
+
active: "studio-node--active",
|
|
22
|
+
done: "studio-node--done",
|
|
23
|
+
error: "studio-node--error",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const edgeBaseStyle = {
|
|
27
|
+
stroke: "#2563eb",
|
|
28
|
+
strokeWidth: 2.5,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const edgeActiveStyle = {
|
|
32
|
+
stroke: "#0ea5e9",
|
|
33
|
+
strokeWidth: 3.2,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const badgeBase = "studio-badge";
|
|
37
|
+
|
|
38
|
+
const nodeBase = "studio-node";
|
|
39
|
+
|
|
40
|
+
function StudioNode({ data }) {
|
|
41
|
+
const statusClass = statusClasses[data.status || "idle"] || statusClasses.idle;
|
|
42
|
+
const badges = [];
|
|
43
|
+
if (data.isEntry) {
|
|
44
|
+
badges.push(
|
|
45
|
+
e("span", { className: `${badgeBase} studio-badge--entry` }, "Entry"),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (data.isTerminal) {
|
|
49
|
+
badges.push(
|
|
50
|
+
e("span", { className: `${badgeBase} studio-badge--terminal` }, "Terminal"),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (data.isDynamic) {
|
|
54
|
+
badges.push(
|
|
55
|
+
e("span", { className: `${badgeBase} studio-badge--dynamic` }, "Dynamic"),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return e(
|
|
60
|
+
"div",
|
|
61
|
+
{ className: `${nodeBase} ${statusClass}` },
|
|
62
|
+
e(Handle, { type: "target", position: "top", style: { opacity: 0 } }),
|
|
63
|
+
e("div", { className: "text-sm font-semibold" }, data.label || data.id),
|
|
64
|
+
badges.length
|
|
65
|
+
? e(
|
|
66
|
+
"div",
|
|
67
|
+
{ className: "mt-2 flex flex-wrap gap-2" },
|
|
68
|
+
badges,
|
|
69
|
+
)
|
|
70
|
+
: null,
|
|
71
|
+
e(Handle, { type: "source", position: "bottom", style: { opacity: 0 } }),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const nodeTypes = { studio: StudioNode };
|
|
76
|
+
|
|
77
|
+
function StudioEdge({
|
|
78
|
+
id,
|
|
79
|
+
sourceX,
|
|
80
|
+
sourceY,
|
|
81
|
+
targetX,
|
|
82
|
+
targetY,
|
|
83
|
+
sourcePosition,
|
|
84
|
+
targetPosition,
|
|
85
|
+
style,
|
|
86
|
+
data,
|
|
87
|
+
}) {
|
|
88
|
+
const [edgePath] = getSmoothStepPath({
|
|
89
|
+
sourceX,
|
|
90
|
+
sourceY,
|
|
91
|
+
targetX,
|
|
92
|
+
targetY,
|
|
93
|
+
sourcePosition,
|
|
94
|
+
targetPosition,
|
|
95
|
+
borderRadius: 18,
|
|
96
|
+
centerX: typeof data?.routeX === "number" ? data.routeX : undefined,
|
|
97
|
+
});
|
|
98
|
+
const mergedStyle = {
|
|
99
|
+
...edgeBaseStyle,
|
|
100
|
+
...(data?.dynamic ? { strokeDasharray: "6 4" } : null),
|
|
101
|
+
...style,
|
|
102
|
+
};
|
|
103
|
+
return e(BaseEdge, { id, path: edgePath, style: mergedStyle });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const edgeTypes = { studio: StudioEdge };
|
|
107
|
+
|
|
108
|
+
function estimateNodeSize(data) {
|
|
109
|
+
const labelLength = (data.label || data.id || "").length;
|
|
110
|
+
const badgeCount =
|
|
111
|
+
(data.isEntry ? 1 : 0) + (data.isTerminal ? 1 : 0) + (data.isDynamic ? 1 : 0);
|
|
112
|
+
const width = Math.min(260, Math.max(160, labelLength * 7 + 90));
|
|
113
|
+
const height = 64 + badgeCount * 18;
|
|
114
|
+
return { width, height };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildFlowGraphDagre(graph) {
|
|
118
|
+
const nodes = [];
|
|
119
|
+
const edges = [];
|
|
120
|
+
const nodeData = new Map();
|
|
121
|
+
const sizeById = new Map();
|
|
122
|
+
const dynamicNodesBySource = new Map();
|
|
123
|
+
const entrySet = new Set(graph.entry_nodes || []);
|
|
124
|
+
const terminalSet = new Set(graph.terminal_nodes || []);
|
|
125
|
+
|
|
126
|
+
graph.nodes.forEach((node) => {
|
|
127
|
+
nodeData.set(node.node_id, {
|
|
128
|
+
id: node.node_id,
|
|
129
|
+
label: node.label || node.node_id,
|
|
130
|
+
isEntry: entrySet.has(node.node_id),
|
|
131
|
+
isTerminal: terminalSet.has(node.node_id),
|
|
132
|
+
isDynamic: false,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const edgesInput = [];
|
|
137
|
+
graph.edges.forEach((edge) => {
|
|
138
|
+
let target = edge.target_node_id;
|
|
139
|
+
if (!target) {
|
|
140
|
+
let dynamicId = dynamicNodesBySource.get(edge.source_node_id);
|
|
141
|
+
if (!dynamicId) {
|
|
142
|
+
dynamicId = `dynamic-${edge.source_node_id}`;
|
|
143
|
+
dynamicNodesBySource.set(edge.source_node_id, dynamicId);
|
|
144
|
+
nodeData.set(dynamicId, {
|
|
145
|
+
id: dynamicId,
|
|
146
|
+
label: "Dynamic target",
|
|
147
|
+
isEntry: false,
|
|
148
|
+
isTerminal: false,
|
|
149
|
+
isDynamic: true,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
target = dynamicId;
|
|
153
|
+
}
|
|
154
|
+
edgesInput.push({
|
|
155
|
+
source: edge.source_node_id,
|
|
156
|
+
target,
|
|
157
|
+
dynamic: edge.dynamic,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const dagreGraph = new dagreLib.graphlib.Graph();
|
|
162
|
+
dagreGraph.setGraph({
|
|
163
|
+
rankdir: "TB",
|
|
164
|
+
ranksep: 120,
|
|
165
|
+
nodesep: 70,
|
|
166
|
+
marginx: 40,
|
|
167
|
+
marginy: 40,
|
|
168
|
+
});
|
|
169
|
+
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
|
170
|
+
|
|
171
|
+
nodeData.forEach((data, nodeId) => {
|
|
172
|
+
const size = estimateNodeSize(data);
|
|
173
|
+
sizeById.set(nodeId, size);
|
|
174
|
+
dagreGraph.setNode(nodeId, size);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
edgesInput.forEach((edge) => {
|
|
178
|
+
dagreGraph.setEdge(edge.source, edge.target);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
dagreLib.layout(dagreGraph);
|
|
182
|
+
|
|
183
|
+
nodeData.forEach((data, nodeId) => {
|
|
184
|
+
const layout = dagreGraph.node(nodeId) || { x: 0, y: 0 };
|
|
185
|
+
const size = sizeById.get(nodeId) || { width: 180, height: 72 };
|
|
186
|
+
nodes.push({
|
|
187
|
+
id: nodeId,
|
|
188
|
+
type: "studio",
|
|
189
|
+
position: {
|
|
190
|
+
x: layout.x - size.width / 2,
|
|
191
|
+
y: layout.y - size.height / 2,
|
|
192
|
+
},
|
|
193
|
+
sourcePosition: "bottom",
|
|
194
|
+
targetPosition: "top",
|
|
195
|
+
data: {
|
|
196
|
+
id: nodeId,
|
|
197
|
+
label: data.label || nodeId,
|
|
198
|
+
status: "idle",
|
|
199
|
+
isEntry: data.isEntry,
|
|
200
|
+
isTerminal: data.isTerminal,
|
|
201
|
+
isDynamic: data.isDynamic,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
edgesInput.forEach((edge, index) => {
|
|
207
|
+
edges.push({
|
|
208
|
+
id: `e-${edge.source}-${edge.target}-${index}`,
|
|
209
|
+
source: edge.source,
|
|
210
|
+
target: edge.target,
|
|
211
|
+
type: "studio",
|
|
212
|
+
animated: false,
|
|
213
|
+
style: {
|
|
214
|
+
...edgeBaseStyle,
|
|
215
|
+
...(edge.dynamic ? { strokeDasharray: "4 3" } : null),
|
|
216
|
+
},
|
|
217
|
+
data: {
|
|
218
|
+
dynamic: edge.dynamic,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return { nodes, edges };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildFlowGraphHeuristic(graph) {
|
|
227
|
+
const nodes = [];
|
|
228
|
+
const edges = [];
|
|
229
|
+
const positions = new Map();
|
|
230
|
+
const dynamicNodesBySource = new Map();
|
|
231
|
+
const terminalSet = new Set(graph.terminal_nodes || []);
|
|
232
|
+
const gapX = 260;
|
|
233
|
+
const gapY = 190;
|
|
234
|
+
const nodeWidth = 160;
|
|
235
|
+
const nodeOrder = graph.nodes.map((node) => node.node_id);
|
|
236
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
|
237
|
+
const adjacency = new Map();
|
|
238
|
+
const incoming = new Map();
|
|
239
|
+
nodeOrder.forEach((nodeId) => adjacency.set(nodeId, []));
|
|
240
|
+
nodeOrder.forEach((nodeId) => incoming.set(nodeId, []));
|
|
241
|
+
graph.edges.forEach((edge) => {
|
|
242
|
+
if (!edge.target_node_id) return;
|
|
243
|
+
if (!adjacency.has(edge.source_node_id)) {
|
|
244
|
+
adjacency.set(edge.source_node_id, []);
|
|
245
|
+
}
|
|
246
|
+
if (!incoming.has(edge.target_node_id)) {
|
|
247
|
+
incoming.set(edge.target_node_id, []);
|
|
248
|
+
}
|
|
249
|
+
adjacency.get(edge.source_node_id).push(edge.target_node_id);
|
|
250
|
+
incoming.get(edge.target_node_id).push(edge.source_node_id);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const levels = new Map();
|
|
254
|
+
const entryNodes = graph.entry_nodes.length ? graph.entry_nodes : nodeOrder.slice(0, 1);
|
|
255
|
+
entryNodes.forEach((nodeId) => {
|
|
256
|
+
levels.set(nodeId, 0);
|
|
257
|
+
});
|
|
258
|
+
const maxIterations = Math.max(nodeOrder.length * 2, 1);
|
|
259
|
+
for (let iteration = 0; iteration < maxIterations; iteration += 1) {
|
|
260
|
+
let updated = false;
|
|
261
|
+
graph.edges.forEach((edge) => {
|
|
262
|
+
if (!edge.target_node_id) return;
|
|
263
|
+
const sourceLevel = levels.get(edge.source_node_id);
|
|
264
|
+
if (sourceLevel === undefined) return;
|
|
265
|
+
const candidate = sourceLevel + 1;
|
|
266
|
+
const current = levels.get(edge.target_node_id);
|
|
267
|
+
if (current === undefined || current < candidate) {
|
|
268
|
+
levels.set(edge.target_node_id, Math.min(candidate, nodeOrder.length));
|
|
269
|
+
updated = true;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
if (!updated) break;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let maxLevel = 0;
|
|
276
|
+
levels.forEach((value) => {
|
|
277
|
+
if (value > maxLevel) {
|
|
278
|
+
maxLevel = value;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
nodeOrder.forEach((nodeId) => {
|
|
282
|
+
if (!levels.has(nodeId)) {
|
|
283
|
+
levels.set(nodeId, maxLevel + 1);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
let maxNonTerminalLevel = -1;
|
|
287
|
+
nodeOrder.forEach((nodeId) => {
|
|
288
|
+
if (terminalSet.has(nodeId)) return;
|
|
289
|
+
const level = levels.get(nodeId) ?? 0;
|
|
290
|
+
if (level > maxNonTerminalLevel) {
|
|
291
|
+
maxNonTerminalLevel = level;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
if (maxNonTerminalLevel < 0) {
|
|
295
|
+
maxNonTerminalLevel = maxLevel;
|
|
296
|
+
}
|
|
297
|
+
terminalSet.forEach((nodeId) => {
|
|
298
|
+
levels.set(nodeId, maxNonTerminalLevel + 1);
|
|
299
|
+
});
|
|
300
|
+
if (maxNonTerminalLevel >= 0) {
|
|
301
|
+
nodeOrder.forEach((nodeId) => {
|
|
302
|
+
if (terminalSet.has(nodeId)) return;
|
|
303
|
+
const targets = adjacency.get(nodeId) || [];
|
|
304
|
+
if (targets.length === 0) return;
|
|
305
|
+
const allTerminal = targets.every((target) => terminalSet.has(target));
|
|
306
|
+
if (allTerminal) {
|
|
307
|
+
levels.set(nodeId, Math.max(levels.get(nodeId) ?? 0, maxNonTerminalLevel));
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const columns = new Map();
|
|
313
|
+
nodeOrder.forEach((nodeId) => {
|
|
314
|
+
const level = levels.get(nodeId) ?? 0;
|
|
315
|
+
if (!columns.has(level)) {
|
|
316
|
+
columns.set(level, []);
|
|
317
|
+
}
|
|
318
|
+
columns.get(level).push(nodeId);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const orderedLevels = Array.from(columns.keys()).sort((a, b) => a - b);
|
|
322
|
+
const labelFor = (nodeId) => nodeById.get(nodeId)?.label || nodeId;
|
|
323
|
+
const layers = orderedLevels.map((level) => columns.get(level).slice());
|
|
324
|
+
layers.forEach((layer) => layer.sort((a, b) => labelFor(a).localeCompare(labelFor(b))));
|
|
325
|
+
|
|
326
|
+
const nodeIndex = new Map();
|
|
327
|
+
const refreshIndex = () => {
|
|
328
|
+
layers.forEach((layer) => {
|
|
329
|
+
layer.forEach((nodeId, index) => {
|
|
330
|
+
nodeIndex.set(nodeId, index);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
};
|
|
334
|
+
const barycenterSort = (layerIndex, useIncoming) => {
|
|
335
|
+
const layer = layers[layerIndex];
|
|
336
|
+
const scores = new Map();
|
|
337
|
+
layer.forEach((nodeId, index) => {
|
|
338
|
+
const neighbors = useIncoming ? incoming.get(nodeId) : adjacency.get(nodeId);
|
|
339
|
+
const indices = (neighbors || [])
|
|
340
|
+
.map((neighbor) => nodeIndex.get(neighbor))
|
|
341
|
+
.filter((value) => value !== undefined);
|
|
342
|
+
const score = indices.length
|
|
343
|
+
? indices.reduce((sum, value) => sum + value, 0) / indices.length
|
|
344
|
+
: index;
|
|
345
|
+
scores.set(nodeId, score);
|
|
346
|
+
});
|
|
347
|
+
layer.sort((a, b) => {
|
|
348
|
+
const diff = (scores.get(a) ?? 0) - (scores.get(b) ?? 0);
|
|
349
|
+
if (Math.abs(diff) > 0.0001) {
|
|
350
|
+
return diff;
|
|
351
|
+
}
|
|
352
|
+
return labelFor(a).localeCompare(labelFor(b));
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
refreshIndex();
|
|
357
|
+
for (let pass = 0; pass < 3; pass += 1) {
|
|
358
|
+
for (let i = 1; i < layers.length; i += 1) {
|
|
359
|
+
barycenterSort(i, true);
|
|
360
|
+
refreshIndex();
|
|
361
|
+
}
|
|
362
|
+
for (let i = layers.length - 2; i >= 0; i -= 1) {
|
|
363
|
+
barycenterSort(i, false);
|
|
364
|
+
refreshIndex();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const maxRowSize = Math.max(...layers.map((list) => list.length), 1);
|
|
369
|
+
const rowBounds = new Map();
|
|
370
|
+
let globalMinX = Infinity;
|
|
371
|
+
let globalMaxX = -Infinity;
|
|
372
|
+
|
|
373
|
+
orderedLevels.forEach((level, rowIndex) => {
|
|
374
|
+
const rowNodes = layers[rowIndex] || columns.get(level).slice();
|
|
375
|
+
let minX = Infinity;
|
|
376
|
+
let maxX = -Infinity;
|
|
377
|
+
const rowWidth = (rowNodes.length - 1) * gapX;
|
|
378
|
+
const centerOffset = ((maxRowSize - 1) * gapX - rowWidth) / 2;
|
|
379
|
+
rowNodes.forEach((nodeId, columnIndex) => {
|
|
380
|
+
const position = {
|
|
381
|
+
x: columnIndex * gapX + centerOffset,
|
|
382
|
+
y: rowIndex * gapY,
|
|
383
|
+
};
|
|
384
|
+
positions.set(nodeId, position);
|
|
385
|
+
minX = Math.min(minX, position.x);
|
|
386
|
+
maxX = Math.max(maxX, position.x);
|
|
387
|
+
globalMinX = Math.min(globalMinX, position.x);
|
|
388
|
+
globalMaxX = Math.max(globalMaxX, position.x);
|
|
389
|
+
const node = nodeById.get(nodeId);
|
|
390
|
+
nodes.push({
|
|
391
|
+
id: nodeId,
|
|
392
|
+
type: "studio",
|
|
393
|
+
position,
|
|
394
|
+
sourcePosition: "bottom",
|
|
395
|
+
targetPosition: "top",
|
|
396
|
+
data: {
|
|
397
|
+
id: nodeId,
|
|
398
|
+
label: node?.label || nodeId,
|
|
399
|
+
status: "idle",
|
|
400
|
+
isEntry: graph.entry_nodes.includes(nodeId),
|
|
401
|
+
isTerminal: graph.terminal_nodes.includes(nodeId),
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
if (rowNodes.length) {
|
|
406
|
+
rowBounds.set(level, {
|
|
407
|
+
minX: minX - nodeWidth / 2,
|
|
408
|
+
maxX: maxX + nodeWidth / 2,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
if (!Number.isFinite(globalMinX) || !Number.isFinite(globalMaxX)) {
|
|
413
|
+
globalMinX = 0;
|
|
414
|
+
globalMaxX = 0;
|
|
415
|
+
}
|
|
416
|
+
const sideLaneOffset = gapX * 1.1;
|
|
417
|
+
const sideLaneSpacing = gapX * 0.35;
|
|
418
|
+
let leftLaneCount = 0;
|
|
419
|
+
let rightLaneCount = 0;
|
|
420
|
+
|
|
421
|
+
graph.edges.forEach((edge, index) => {
|
|
422
|
+
let target = edge.target_node_id;
|
|
423
|
+
if (!target) {
|
|
424
|
+
let dynamicId = dynamicNodesBySource.get(edge.source_node_id);
|
|
425
|
+
if (!dynamicId) {
|
|
426
|
+
dynamicId = `dynamic-${edge.source_node_id}`;
|
|
427
|
+
dynamicNodesBySource.set(edge.source_node_id, dynamicId);
|
|
428
|
+
const sourcePosition = positions.get(edge.source_node_id) || { x: 0, y: 0 };
|
|
429
|
+
nodes.push({
|
|
430
|
+
id: dynamicId,
|
|
431
|
+
type: "studio",
|
|
432
|
+
position: {
|
|
433
|
+
x: sourcePosition.x + gapX * 0.7,
|
|
434
|
+
y: sourcePosition.y + gapY * 0.4,
|
|
435
|
+
},
|
|
436
|
+
data: {
|
|
437
|
+
id: dynamicId,
|
|
438
|
+
label: "Dynamic target",
|
|
439
|
+
status: "idle",
|
|
440
|
+
isDynamic: true,
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
target = dynamicId;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let routeX;
|
|
448
|
+
const sourceLevel = levels.get(edge.source_node_id) ?? 0;
|
|
449
|
+
const targetLevel = levels.get(target) ?? 0;
|
|
450
|
+
if (Math.abs(targetLevel - sourceLevel) > 1) {
|
|
451
|
+
const sourcePos = positions.get(edge.source_node_id);
|
|
452
|
+
const targetPos = positions.get(target);
|
|
453
|
+
if (sourcePos && targetPos) {
|
|
454
|
+
const defaultCenter = (sourcePos.x + targetPos.x) / 2;
|
|
455
|
+
let needsDetour = false;
|
|
456
|
+
for (
|
|
457
|
+
let level = Math.min(sourceLevel, targetLevel) + 1;
|
|
458
|
+
level <= Math.max(sourceLevel, targetLevel) - 1;
|
|
459
|
+
level += 1
|
|
460
|
+
) {
|
|
461
|
+
const bounds = rowBounds.get(level);
|
|
462
|
+
if (!bounds) continue;
|
|
463
|
+
if (defaultCenter >= bounds.minX && defaultCenter <= bounds.maxX) {
|
|
464
|
+
needsDetour = true;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (needsDetour) {
|
|
469
|
+
const useRight = sourcePos.x >= targetPos.x;
|
|
470
|
+
if (useRight) {
|
|
471
|
+
rightLaneCount += 1;
|
|
472
|
+
routeX = globalMaxX + sideLaneOffset + (rightLaneCount - 1) * sideLaneSpacing;
|
|
473
|
+
} else {
|
|
474
|
+
leftLaneCount += 1;
|
|
475
|
+
routeX = globalMinX - sideLaneOffset - (leftLaneCount - 1) * sideLaneSpacing;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
edges.push({
|
|
482
|
+
id: `e-${edge.source_node_id}-${target}-${index}`,
|
|
483
|
+
source: edge.source_node_id,
|
|
484
|
+
target,
|
|
485
|
+
type: "studio",
|
|
486
|
+
animated: false,
|
|
487
|
+
style: {
|
|
488
|
+
...edgeBaseStyle,
|
|
489
|
+
...(edge.dynamic ? { strokeDasharray: "4 3" } : null),
|
|
490
|
+
},
|
|
491
|
+
data: {
|
|
492
|
+
dynamic: edge.dynamic,
|
|
493
|
+
routeX,
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
return { nodes, edges };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function buildFlowGraph(graph) {
|
|
502
|
+
if (hasDagre) {
|
|
503
|
+
return buildFlowGraphDagre(graph);
|
|
504
|
+
}
|
|
505
|
+
return buildFlowGraphHeuristic(graph);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function App() {
|
|
509
|
+
const [graph, setGraph] = useState(null);
|
|
510
|
+
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
511
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
512
|
+
const [status, setStatus] = useState({ phase: "idle", runId: null, error: null });
|
|
513
|
+
const eventSourceRef = useRef(null);
|
|
514
|
+
|
|
515
|
+
const statusLabel = useMemo(() => {
|
|
516
|
+
if (status.phase === "running") return `Running ${status.runId || ""}`.trim();
|
|
517
|
+
if (status.phase === "error") return "Error";
|
|
518
|
+
if (status.phase === "loading") return "Loading";
|
|
519
|
+
if (status.phase === "ready") return "Ready";
|
|
520
|
+
return "Idle";
|
|
521
|
+
}, [status]);
|
|
522
|
+
|
|
523
|
+
useEffect(() => {
|
|
524
|
+
let active = true;
|
|
525
|
+
|
|
526
|
+
const load = async () => {
|
|
527
|
+
setStatus((current) => ({ ...current, phase: "loading", error: null }));
|
|
528
|
+
try {
|
|
529
|
+
const response = await fetch("/api/graph");
|
|
530
|
+
if (!response.ok) {
|
|
531
|
+
throw new Error(`Failed to load graph (${response.status})`);
|
|
532
|
+
}
|
|
533
|
+
const data = await response.json();
|
|
534
|
+
if (!active) return;
|
|
535
|
+
const { nodes: nextNodes, edges: nextEdges } = buildFlowGraph(data);
|
|
536
|
+
setGraph(data);
|
|
537
|
+
setNodes(nextNodes);
|
|
538
|
+
setEdges(nextEdges);
|
|
539
|
+
setStatus((current) => ({ ...current, phase: "ready", error: null }));
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (!active) return;
|
|
542
|
+
setStatus({ phase: "error", runId: null, error: error.message });
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
load();
|
|
547
|
+
|
|
548
|
+
return () => {
|
|
549
|
+
active = false;
|
|
550
|
+
if (eventSourceRef.current) {
|
|
551
|
+
eventSourceRef.current.close();
|
|
552
|
+
eventSourceRef.current = null;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
}, [setNodes, setEdges]);
|
|
556
|
+
|
|
557
|
+
const resetRunVisuals = () => {
|
|
558
|
+
setNodes((current) =>
|
|
559
|
+
current.map((node) => ({
|
|
560
|
+
...node,
|
|
561
|
+
data: { ...node.data, status: "idle" },
|
|
562
|
+
})),
|
|
563
|
+
);
|
|
564
|
+
setEdges((current) =>
|
|
565
|
+
current.map((edge) => ({
|
|
566
|
+
...edge,
|
|
567
|
+
animated: false,
|
|
568
|
+
style: {
|
|
569
|
+
...edge.style,
|
|
570
|
+
...edgeBaseStyle,
|
|
571
|
+
...(edge.data?.dynamic ? { strokeDasharray: "4 3" } : null),
|
|
572
|
+
},
|
|
573
|
+
})),
|
|
574
|
+
);
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const handleEvent = (payload) => {
|
|
578
|
+
switch (payload.event_type) {
|
|
579
|
+
case "node_start":
|
|
580
|
+
setNodes((current) =>
|
|
581
|
+
current.map((node) =>
|
|
582
|
+
node.id === payload.node_id
|
|
583
|
+
? { ...node, data: { ...node.data, status: "active" } }
|
|
584
|
+
: node,
|
|
585
|
+
),
|
|
586
|
+
);
|
|
587
|
+
break;
|
|
588
|
+
case "node_end":
|
|
589
|
+
setNodes((current) =>
|
|
590
|
+
current.map((node) =>
|
|
591
|
+
node.id === payload.node_id
|
|
592
|
+
? { ...node, data: { ...node.data, status: "done" } }
|
|
593
|
+
: node,
|
|
594
|
+
),
|
|
595
|
+
);
|
|
596
|
+
break;
|
|
597
|
+
case "edge_taken":
|
|
598
|
+
setEdges((current) =>
|
|
599
|
+
current.map((edge) => {
|
|
600
|
+
if (edge.source !== payload.source_node_id) {
|
|
601
|
+
return edge;
|
|
602
|
+
}
|
|
603
|
+
if (payload.target_node_id && edge.target !== payload.target_node_id) {
|
|
604
|
+
return edge;
|
|
605
|
+
}
|
|
606
|
+
if (!payload.target_node_id && !edge.data?.dynamic) {
|
|
607
|
+
return edge;
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
...edge,
|
|
611
|
+
animated: true,
|
|
612
|
+
style: { ...edge.style, ...edgeActiveStyle },
|
|
613
|
+
};
|
|
614
|
+
}),
|
|
615
|
+
);
|
|
616
|
+
break;
|
|
617
|
+
case "run_end":
|
|
618
|
+
setStatus((current) => ({ ...current, phase: "ready" }));
|
|
619
|
+
if (eventSourceRef.current) {
|
|
620
|
+
eventSourceRef.current.close();
|
|
621
|
+
eventSourceRef.current = null;
|
|
622
|
+
}
|
|
623
|
+
break;
|
|
624
|
+
case "error":
|
|
625
|
+
if (payload.node_id) {
|
|
626
|
+
setNodes((current) =>
|
|
627
|
+
current.map((node) =>
|
|
628
|
+
node.id === payload.node_id
|
|
629
|
+
? { ...node, data: { ...node.data, status: "error" } }
|
|
630
|
+
: node,
|
|
631
|
+
),
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
setStatus((current) => ({
|
|
635
|
+
...current,
|
|
636
|
+
phase: "error",
|
|
637
|
+
error: payload.message || "Execution error",
|
|
638
|
+
}));
|
|
639
|
+
if (eventSourceRef.current) {
|
|
640
|
+
eventSourceRef.current.close();
|
|
641
|
+
eventSourceRef.current = null;
|
|
642
|
+
}
|
|
643
|
+
break;
|
|
644
|
+
default:
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const startRun = async () => {
|
|
650
|
+
if (!graph || status.phase === "loading") {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
resetRunVisuals();
|
|
654
|
+
setStatus((current) => ({ ...current, phase: "running", error: null }));
|
|
655
|
+
try {
|
|
656
|
+
const response = await fetch("/api/run", { method: "POST" });
|
|
657
|
+
if (!response.ok) {
|
|
658
|
+
throw new Error(`Failed to start run (${response.status})`);
|
|
659
|
+
}
|
|
660
|
+
const payload = await response.json();
|
|
661
|
+
const runId = payload.run_id;
|
|
662
|
+
setStatus((current) => ({ ...current, runId, phase: "running" }));
|
|
663
|
+
if (eventSourceRef.current) {
|
|
664
|
+
eventSourceRef.current.close();
|
|
665
|
+
}
|
|
666
|
+
const stream = new EventSource(`/api/events?run_id=${runId}`);
|
|
667
|
+
eventSourceRef.current = stream;
|
|
668
|
+
stream.onmessage = (event) => {
|
|
669
|
+
try {
|
|
670
|
+
const data = JSON.parse(event.data);
|
|
671
|
+
handleEvent(data);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
setStatus((current) => ({
|
|
674
|
+
...current,
|
|
675
|
+
phase: "error",
|
|
676
|
+
error: "Malformed event payload",
|
|
677
|
+
}));
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
stream.onerror = () => {
|
|
681
|
+
setStatus((current) => ({
|
|
682
|
+
...current,
|
|
683
|
+
phase: current.phase === "running" ? "error" : current.phase,
|
|
684
|
+
error: current.phase === "running" ? "Event stream disconnected" : current.error,
|
|
685
|
+
}));
|
|
686
|
+
stream.close();
|
|
687
|
+
eventSourceRef.current = null;
|
|
688
|
+
};
|
|
689
|
+
} catch (error) {
|
|
690
|
+
setStatus((current) => ({
|
|
691
|
+
...current,
|
|
692
|
+
phase: "error",
|
|
693
|
+
error: error.message,
|
|
694
|
+
}));
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const content = graph
|
|
699
|
+
? e(
|
|
700
|
+
Flow,
|
|
701
|
+
{
|
|
702
|
+
nodes,
|
|
703
|
+
edges,
|
|
704
|
+
onNodesChange,
|
|
705
|
+
onEdgesChange,
|
|
706
|
+
nodeTypes,
|
|
707
|
+
edgeTypes,
|
|
708
|
+
fitView: true,
|
|
709
|
+
fitViewOptions: { padding: 0.35 },
|
|
710
|
+
minZoom: 0.2,
|
|
711
|
+
},
|
|
712
|
+
e(Background, {
|
|
713
|
+
variant: RF.BackgroundVariant.Dots,
|
|
714
|
+
gap: 26,
|
|
715
|
+
size: 1.4,
|
|
716
|
+
color: "#cbd5f5",
|
|
717
|
+
}),
|
|
718
|
+
e(Controls, { position: "top-right" }),
|
|
719
|
+
e(MiniMap, {
|
|
720
|
+
position: "bottom-right",
|
|
721
|
+
nodeStrokeColor: "#cbd5f5",
|
|
722
|
+
nodeColor: "#e2e8f0",
|
|
723
|
+
maskColor: "rgba(248, 250, 252, 0.7)",
|
|
724
|
+
}),
|
|
725
|
+
)
|
|
726
|
+
: e(
|
|
727
|
+
"div",
|
|
728
|
+
{ className: "flex h-full items-center justify-center studio-empty" },
|
|
729
|
+
status.error || "Loading graph…",
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
return e(
|
|
733
|
+
"div",
|
|
734
|
+
{ className: "flex h-full flex-col studio-shell" },
|
|
735
|
+
e(
|
|
736
|
+
"header",
|
|
737
|
+
{ className: "flex items-center justify-between px-6 py-4 studio-header" },
|
|
738
|
+
e(
|
|
739
|
+
"div",
|
|
740
|
+
{ className: "space-y-1" },
|
|
741
|
+
e("h1", { className: "text-lg font-semibold studio-title" }, "Pydantic Graph Studio"),
|
|
742
|
+
e(
|
|
743
|
+
"p",
|
|
744
|
+
{ className: "text-xs uppercase tracking-[0.2em] studio-status" },
|
|
745
|
+
statusLabel,
|
|
746
|
+
),
|
|
747
|
+
),
|
|
748
|
+
e(
|
|
749
|
+
"div",
|
|
750
|
+
{ className: "flex items-center gap-3" },
|
|
751
|
+
status.error
|
|
752
|
+
? e(
|
|
753
|
+
"span",
|
|
754
|
+
{ className: "text-xs studio-error" },
|
|
755
|
+
status.error,
|
|
756
|
+
)
|
|
757
|
+
: null,
|
|
758
|
+
e(
|
|
759
|
+
"button",
|
|
760
|
+
{
|
|
761
|
+
className: "rounded-md px-4 py-2 text-sm font-semibold studio-button",
|
|
762
|
+
onClick: startRun,
|
|
763
|
+
disabled: status.phase === "loading" || status.phase === "running",
|
|
764
|
+
},
|
|
765
|
+
status.phase === "running" ? "Running…" : "Run",
|
|
766
|
+
),
|
|
767
|
+
),
|
|
768
|
+
),
|
|
769
|
+
e(
|
|
770
|
+
"main",
|
|
771
|
+
{ className: "flex-1" },
|
|
772
|
+
content,
|
|
773
|
+
),
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const rootEl = document.getElementById("root");
|
|
778
|
+
if (ReactDOM.createRoot) {
|
|
779
|
+
ReactDOM.createRoot(rootEl).render(e(App));
|
|
780
|
+
} else {
|
|
781
|
+
ReactDOM.render(e(App), rootEl);
|
|
782
|
+
}
|
|
783
|
+
})();
|