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.
@@ -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
+ })();