@bian-womp/spark-workbench 0.1.0

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.
Files changed (89) hide show
  1. package/lib/cjs/index.cjs +1748 -0
  2. package/lib/cjs/index.cjs.map +1 -0
  3. package/lib/cjs/src/adapters/cli/index.d.ts +22 -0
  4. package/lib/cjs/src/adapters/cli/index.d.ts.map +1 -0
  5. package/lib/cjs/src/adapters/react-flow/index.d.ts +31 -0
  6. package/lib/cjs/src/adapters/react-flow/index.d.ts.map +1 -0
  7. package/lib/cjs/src/core/AbstractWorkbench.d.ts +35 -0
  8. package/lib/cjs/src/core/AbstractWorkbench.d.ts.map +1 -0
  9. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +54 -0
  10. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -0
  11. package/lib/cjs/src/core/contracts.d.ts +107 -0
  12. package/lib/cjs/src/core/contracts.d.ts.map +1 -0
  13. package/lib/cjs/src/core/ui-extensions.d.ts +59 -0
  14. package/lib/cjs/src/core/ui-extensions.d.ts.map +1 -0
  15. package/lib/cjs/src/examples/cli.d.ts +2 -0
  16. package/lib/cjs/src/examples/cli.d.ts.map +1 -0
  17. package/lib/cjs/src/examples/reactflow/App.d.ts +2 -0
  18. package/lib/cjs/src/examples/reactflow/App.d.ts.map +1 -0
  19. package/lib/cjs/src/examples/reactflow/WorkbenchStudio.d.ts +21 -0
  20. package/lib/cjs/src/examples/reactflow/WorkbenchStudio.d.ts.map +1 -0
  21. package/lib/cjs/src/index.d.ts +9 -0
  22. package/lib/cjs/src/index.d.ts.map +1 -0
  23. package/lib/cjs/src/misc/DebugEvents.d.ts +7 -0
  24. package/lib/cjs/src/misc/DebugEvents.d.ts.map +1 -0
  25. package/lib/cjs/src/misc/DefaultContextMenu.d.ts +13 -0
  26. package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +1 -0
  27. package/lib/cjs/src/misc/DefaultNode.d.ts +4 -0
  28. package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -0
  29. package/lib/cjs/src/misc/Inspector.d.ts +10 -0
  30. package/lib/cjs/src/misc/Inspector.d.ts.map +1 -0
  31. package/lib/cjs/src/misc/IssueBadge.d.ts +7 -0
  32. package/lib/cjs/src/misc/IssueBadge.d.ts.map +1 -0
  33. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts +6 -0
  34. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -0
  35. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +45 -0
  36. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -0
  37. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts +12 -0
  38. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -0
  39. package/lib/cjs/src/misc/hooks.d.ts +17 -0
  40. package/lib/cjs/src/misc/hooks.d.ts.map +1 -0
  41. package/lib/cjs/src/misc/mapping.d.ts +47 -0
  42. package/lib/cjs/src/misc/mapping.d.ts.map +1 -0
  43. package/lib/cjs/src/runtime/GraphRunner.d.ts +61 -0
  44. package/lib/cjs/src/runtime/GraphRunner.d.ts.map +1 -0
  45. package/lib/esm/index.js +1740 -0
  46. package/lib/esm/index.js.map +1 -0
  47. package/lib/esm/src/adapters/cli/index.d.ts +22 -0
  48. package/lib/esm/src/adapters/cli/index.d.ts.map +1 -0
  49. package/lib/esm/src/adapters/react-flow/index.d.ts +31 -0
  50. package/lib/esm/src/adapters/react-flow/index.d.ts.map +1 -0
  51. package/lib/esm/src/core/AbstractWorkbench.d.ts +35 -0
  52. package/lib/esm/src/core/AbstractWorkbench.d.ts.map +1 -0
  53. package/lib/esm/src/core/InMemoryWorkbench.d.ts +54 -0
  54. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -0
  55. package/lib/esm/src/core/contracts.d.ts +107 -0
  56. package/lib/esm/src/core/contracts.d.ts.map +1 -0
  57. package/lib/esm/src/core/ui-extensions.d.ts +59 -0
  58. package/lib/esm/src/core/ui-extensions.d.ts.map +1 -0
  59. package/lib/esm/src/examples/cli.d.ts +2 -0
  60. package/lib/esm/src/examples/cli.d.ts.map +1 -0
  61. package/lib/esm/src/examples/reactflow/App.d.ts +2 -0
  62. package/lib/esm/src/examples/reactflow/App.d.ts.map +1 -0
  63. package/lib/esm/src/examples/reactflow/WorkbenchStudio.d.ts +21 -0
  64. package/lib/esm/src/examples/reactflow/WorkbenchStudio.d.ts.map +1 -0
  65. package/lib/esm/src/index.d.ts +9 -0
  66. package/lib/esm/src/index.d.ts.map +1 -0
  67. package/lib/esm/src/misc/DebugEvents.d.ts +7 -0
  68. package/lib/esm/src/misc/DebugEvents.d.ts.map +1 -0
  69. package/lib/esm/src/misc/DefaultContextMenu.d.ts +13 -0
  70. package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +1 -0
  71. package/lib/esm/src/misc/DefaultNode.d.ts +4 -0
  72. package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -0
  73. package/lib/esm/src/misc/Inspector.d.ts +10 -0
  74. package/lib/esm/src/misc/Inspector.d.ts.map +1 -0
  75. package/lib/esm/src/misc/IssueBadge.d.ts +7 -0
  76. package/lib/esm/src/misc/IssueBadge.d.ts.map +1 -0
  77. package/lib/esm/src/misc/WorkbenchCanvas.d.ts +6 -0
  78. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -0
  79. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +45 -0
  80. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -0
  81. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts +12 -0
  82. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -0
  83. package/lib/esm/src/misc/hooks.d.ts +17 -0
  84. package/lib/esm/src/misc/hooks.d.ts.map +1 -0
  85. package/lib/esm/src/misc/mapping.d.ts +47 -0
  86. package/lib/esm/src/misc/mapping.d.ts.map +1 -0
  87. package/lib/esm/src/runtime/GraphRunner.d.ts +61 -0
  88. package/lib/esm/src/runtime/GraphRunner.d.ts.map +1 -0
  89. package/package.json +65 -0
@@ -0,0 +1,1740 @@
1
+ import { GraphBuilder, StepEngine, HybridEngine, PullEngine, BatchedEngine, PushEngine, createSimpleGraphRegistry, createSimpleGraphDef, createValidationGraphRegistry, createValidationGraphDef, createProgressGraphRegistry, createProgressGraphDef, createAsyncGraphRegistry, createAsyncGraphDef } from '@bian-womp/spark-graph';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import React, { createContext, useContext, useMemo, useState, useCallback, useEffect, useRef } from 'react';
4
+ import ReactFlow, { Handle, Position, useReactFlow, Background, MiniMap, Controls } from 'reactflow';
5
+ import 'reactflow/dist/style.css';
6
+ import cx from 'classnames';
7
+ import { XCircleIcon, WarningCircleIcon } from '@phosphor-icons/react';
8
+ import { HttpPollingTransport, WebSocketTransport, RemoteRunner } from '@bian-womp/spark-remote';
9
+
10
+ class DefaultUIExtensionRegistry {
11
+ constructor() {
12
+ this.nodeRenderers = new Map();
13
+ this.portRenderers = new Map();
14
+ this.edgeRenderers = new Map();
15
+ }
16
+ registerNodeRenderer(nodeTypeId, renderer) {
17
+ this.nodeRenderers.set(nodeTypeId, renderer);
18
+ return this;
19
+ }
20
+ getNodeRenderer(nodeTypeId) {
21
+ return this.nodeRenderers.get(nodeTypeId);
22
+ }
23
+ registerPortRenderer(dataTypeId, renderer) {
24
+ this.portRenderers.set(dataTypeId, renderer);
25
+ return this;
26
+ }
27
+ getPortRenderer(dataTypeId) {
28
+ return this.portRenderers.get(dataTypeId);
29
+ }
30
+ registerEdgeRenderer(typeId, renderer) {
31
+ this.edgeRenderers.set(typeId, renderer);
32
+ return this;
33
+ }
34
+ getEdgeRenderer(typeId) {
35
+ return this.edgeRenderers.get(typeId);
36
+ }
37
+ setInspector(renderer) {
38
+ this.inspector = renderer;
39
+ return this;
40
+ }
41
+ getInspector() {
42
+ return this.inspector;
43
+ }
44
+ setPalette(renderer) {
45
+ this.palette = renderer;
46
+ return this;
47
+ }
48
+ getPalette() {
49
+ return this.palette;
50
+ }
51
+ setToolbar(renderer) {
52
+ this.toolbar = renderer;
53
+ return this;
54
+ }
55
+ getToolbar() {
56
+ return this.toolbar;
57
+ }
58
+ setContextMenu(renderer) {
59
+ this.contextMenu = renderer;
60
+ return this;
61
+ }
62
+ getContextMenu() {
63
+ return this.contextMenu;
64
+ }
65
+ setMiniMap(renderer) {
66
+ this.miniMap = renderer;
67
+ return this;
68
+ }
69
+ getMiniMap() {
70
+ return this.miniMap;
71
+ }
72
+ setIconProvider(provider) {
73
+ this.iconProvider = provider;
74
+ return this;
75
+ }
76
+ getIconProvider() {
77
+ return this.iconProvider;
78
+ }
79
+ }
80
+
81
+ class AbstractWorkbench {
82
+ constructor(args) {
83
+ this.ui = args.ui;
84
+ this.layout = args.layout;
85
+ this.storage = args.storage;
86
+ this.serializer = args.serializer;
87
+ }
88
+ // Expose UI registry to adapters (React Flow, CLI) to allow overrides
89
+ getUI() {
90
+ return this.ui;
91
+ }
92
+ }
93
+
94
+ class InMemoryWorkbench extends AbstractWorkbench {
95
+ constructor() {
96
+ super(...arguments);
97
+ this.def = { nodes: [], edges: [] };
98
+ this.positions = {};
99
+ this.listeners = new Map();
100
+ this.selection = {
101
+ nodes: [],
102
+ edges: [],
103
+ };
104
+ }
105
+ setRegistry(registry) {
106
+ this.registry = registry;
107
+ }
108
+ async load(def) {
109
+ this.def = { nodes: [...def.nodes], edges: [...def.edges] };
110
+ if (this.layout) {
111
+ const { positions } = await this.layout.layout(this.def);
112
+ this.positions = positions;
113
+ }
114
+ this.emit("graphChanged", { def: this.def });
115
+ this.refreshValidation();
116
+ }
117
+ export() {
118
+ return this.def;
119
+ }
120
+ refreshValidation() {
121
+ this.emit("validationChanged", this.validate());
122
+ }
123
+ validate() {
124
+ if (this.registry) {
125
+ const builder = new GraphBuilder(this.registry);
126
+ const report = builder.validate(this.def);
127
+ return report;
128
+ }
129
+ const issues = [];
130
+ const nodeIds = new Set();
131
+ for (const n of this.def.nodes) {
132
+ if (nodeIds.has(n.nodeId)) {
133
+ issues.push({
134
+ level: "error",
135
+ code: "NODE_ID_DUP",
136
+ message: `Duplicate nodeId ${n.nodeId}`,
137
+ });
138
+ }
139
+ else
140
+ nodeIds.add(n.nodeId);
141
+ }
142
+ const edgeIds = new Set();
143
+ for (const e of this.def.edges) {
144
+ if (edgeIds.has(e.id)) {
145
+ issues.push({
146
+ level: "error",
147
+ code: "EDGE_ID_DUP",
148
+ message: `Duplicate edge id ${e.id}`,
149
+ });
150
+ }
151
+ else
152
+ edgeIds.add(e.id);
153
+ }
154
+ return { ok: issues.every((i) => i.level !== "error"), issues };
155
+ }
156
+ addNode(node) {
157
+ const id = node.nodeId ?? this.generateId("n");
158
+ this.def.nodes.push({
159
+ nodeId: id,
160
+ typeId: node.typeId,
161
+ params: node.params,
162
+ });
163
+ if (node.position)
164
+ this.positions[id] = node.position;
165
+ this.emit("graphChanged", {
166
+ def: this.def,
167
+ change: { type: "addNode", nodeId: id },
168
+ });
169
+ this.refreshValidation();
170
+ }
171
+ removeNode(nodeId) {
172
+ this.def.nodes = this.def.nodes.filter((n) => n.nodeId !== nodeId);
173
+ this.def.edges = this.def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
174
+ delete this.positions[nodeId];
175
+ this.emit("graphChanged", {
176
+ def: this.def,
177
+ change: { type: "removeNode", nodeId },
178
+ });
179
+ this.refreshValidation();
180
+ }
181
+ connect(edge) {
182
+ const id = edge.id ?? this.generateId("e");
183
+ this.def.edges.push({
184
+ id,
185
+ source: { ...edge.source },
186
+ target: { ...edge.target },
187
+ typeId: edge.typeId,
188
+ });
189
+ this.emit("graphChanged", {
190
+ def: this.def,
191
+ change: { type: "connect", edgeId: id },
192
+ });
193
+ this.refreshValidation();
194
+ }
195
+ disconnect(edgeId) {
196
+ this.def.edges = this.def.edges.filter((e) => e.id !== edgeId);
197
+ this.emit("graphChanged", {
198
+ def: this.def,
199
+ change: { type: "disconnect", edgeId },
200
+ });
201
+ this.emit("validationChanged", this.validate());
202
+ }
203
+ updateParams(nodeId, params) {
204
+ const n = this.def.nodes.find((n) => n.nodeId === nodeId);
205
+ if (!n)
206
+ return;
207
+ n.params = { ...(n.params ?? {}), ...params };
208
+ this.emit("graphChanged", {
209
+ def: this.def,
210
+ change: { type: "updateParams", nodeId },
211
+ });
212
+ }
213
+ // Position and selection APIs for React Flow bridge
214
+ setPosition(nodeId, pos) {
215
+ this.positions[nodeId] = pos;
216
+ this.emit("graphUiChanged", {
217
+ def: this.def,
218
+ change: { type: "moveNode", nodeId, pos },
219
+ });
220
+ }
221
+ setPositions(map) {
222
+ this.positions = { ...map };
223
+ this.emit("graphUiChanged", {
224
+ def: this.def,
225
+ change: { type: "moveNodes" },
226
+ });
227
+ }
228
+ getPositions() {
229
+ return { ...this.positions };
230
+ }
231
+ setSelection(sel) {
232
+ this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
233
+ this.emit("selectionChanged", this.selection);
234
+ }
235
+ getSelection() {
236
+ return {
237
+ nodes: [...this.selection.nodes],
238
+ edges: [...this.selection.edges],
239
+ };
240
+ }
241
+ toggleNodeSelection(nodeId) {
242
+ this.selection.nodes = this.selection.nodes.includes(nodeId)
243
+ ? this.selection.nodes.filter((id) => id !== nodeId)
244
+ : [...this.selection.nodes, nodeId];
245
+ this.emit("selectionChanged", this.selection);
246
+ }
247
+ toggleEdgeSelection(edgeId) {
248
+ this.selection.edges = this.selection.edges.includes(edgeId)
249
+ ? this.selection.edges.filter((id) => id !== edgeId)
250
+ : [...this.selection.edges, edgeId];
251
+ this.emit("selectionChanged", this.selection);
252
+ }
253
+ on(event, handler) {
254
+ if (!this.listeners.has(event))
255
+ this.listeners.set(event, new Set());
256
+ const set = this.listeners.get(event);
257
+ set.add(handler);
258
+ return () => set.delete(handler);
259
+ }
260
+ emit(event, payload) {
261
+ const set = this.listeners.get(event);
262
+ if (set)
263
+ for (const h of Array.from(set))
264
+ h(payload);
265
+ }
266
+ generateId(prefix) {
267
+ return `${prefix}${Math.random().toString(36).slice(2, 8)}`;
268
+ }
269
+ }
270
+
271
+ class CLIWorkbench {
272
+ constructor(wb, deps = {}) {
273
+ this.wb = wb;
274
+ this.deps = deps;
275
+ }
276
+ async load(def) {
277
+ await this.wb.load(def);
278
+ }
279
+ print(def, options) {
280
+ const d = def ?? this.wb.export();
281
+ const detail = !!options?.detail;
282
+ const lines = [];
283
+ lines.push(`Nodes (${d.nodes.length})`);
284
+ for (const n of d.nodes) {
285
+ if (!detail) {
286
+ lines.push(` - ${n.nodeId}: ${n.typeId}`);
287
+ }
288
+ else {
289
+ const reg = this.wb["registry"];
290
+ const desc = reg?.nodes?.get(n.typeId);
291
+ const inputs = Object.entries(desc?.inputs ?? {})
292
+ .map(([h, t]) => `${h}:${t}`)
293
+ .join(", ");
294
+ const outputs = Object.entries(desc?.outputs ?? {})
295
+ .map(([h, t]) => `${h}:${t}`)
296
+ .join(", ");
297
+ const params = n.params ? JSON.stringify(n.params) : "{}";
298
+ lines.push(` - ${n.nodeId}: ${n.typeId}`);
299
+ lines.push(` inputs: ${inputs || "-"}`);
300
+ lines.push(` outputs: ${outputs || "-"}`);
301
+ lines.push(` params: ${params}`);
302
+ const inVals = options?.values?.inputs?.[n.nodeId];
303
+ const outVals = options?.values?.outputs?.[n.nodeId];
304
+ if (inVals && Object.keys(inVals).length > 0)
305
+ lines.push(` inputValues: ${JSON.stringify(inVals)}`);
306
+ if (outVals && Object.keys(outVals).length > 0)
307
+ lines.push(` outputValues: ${JSON.stringify(outVals)}`);
308
+ }
309
+ }
310
+ lines.push(`Edges (${d.edges.length})`);
311
+ for (const e of d.edges) {
312
+ lines.push(` - ${e.id}: ${e.source.nodeId}.${e.source.handle} -> ${e.target.nodeId}.${e.target.handle} (${e.typeId})`);
313
+ }
314
+ return lines.join("\n");
315
+ }
316
+ get actions() {
317
+ return this.wb;
318
+ }
319
+ }
320
+
321
+ function toReactFlow$1(def, positions = {}) {
322
+ const nodes = def.nodes.map((n) => ({
323
+ id: n.nodeId,
324
+ data: { typeId: n.typeId, params: n.params },
325
+ position: positions[n.nodeId] ?? { x: 0, y: 0 },
326
+ }));
327
+ const edges = def.edges.map((e) => ({
328
+ id: e.id,
329
+ source: e.source.nodeId,
330
+ target: e.target.nodeId,
331
+ sourceHandle: e.source.handle,
332
+ targetHandle: e.target.handle,
333
+ }));
334
+ return { nodes, edges };
335
+ }
336
+ class ReactFlowWorkbench {
337
+ constructor(wb) {
338
+ this.wb = wb;
339
+ }
340
+ get actions() {
341
+ return this.wb;
342
+ }
343
+ async load(def) {
344
+ await this.wb.load(def);
345
+ }
346
+ export(def) {
347
+ const d = def ?? this.wb.export();
348
+ return toReactFlow$1(d);
349
+ }
350
+ }
351
+
352
+ const WorkbenchContext = createContext(null);
353
+ function useWorkbenchContext() {
354
+ const ctx = useContext(WorkbenchContext);
355
+ if (!ctx)
356
+ throw new Error("useWorkbenchContext must be used within WorkbenchProvider");
357
+ return ctx;
358
+ }
359
+
360
+ function useWorkbenchBridge(wb) {
361
+ const onConnect = useCallback((params) => {
362
+ if (!params.source || !params.target)
363
+ return;
364
+ if (!params.sourceHandle || !params.targetHandle)
365
+ return;
366
+ wb.connect({
367
+ source: { nodeId: params.source, handle: params.sourceHandle },
368
+ target: { nodeId: params.target, handle: params.targetHandle },
369
+ });
370
+ }, [wb]);
371
+ const onNodesChange = useCallback((changes) => {
372
+ changes.forEach((c) => {
373
+ if (c.type === "position" && c.position)
374
+ wb.setPosition(c.id, c.position);
375
+ if (c.type === "remove")
376
+ wb.removeNode(c.id);
377
+ if (c.type === "select")
378
+ wb.toggleNodeSelection(c.id);
379
+ });
380
+ }, [wb]);
381
+ const onEdgesDelete = useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
382
+ const onEdgesChange = useCallback((changes) => {
383
+ changes.forEach((c) => {
384
+ if (c.type === "remove")
385
+ wb.disconnect(c.id);
386
+ else if (c.type === "select")
387
+ wb.toggleEdgeSelection(c.id);
388
+ });
389
+ }, [wb]);
390
+ const onNodesDelete = useCallback((nodes) => {
391
+ for (const n of nodes)
392
+ wb.removeNode(n.id);
393
+ }, [wb]);
394
+ const onSelectionChange = useCallback((sel) => {
395
+ const next = {
396
+ nodes: sel.nodes.map((n) => n.id),
397
+ edges: sel.edges.map((e) => e.id),
398
+ };
399
+ const cur = wb.getSelection();
400
+ const sameLen = cur.nodes.length === next.nodes.length &&
401
+ cur.edges.length === next.edges.length;
402
+ const same = sameLen &&
403
+ cur.nodes.every((id, i) => id === next.nodes[i]) &&
404
+ cur.edges.every((id, i) => id === next.edges[i]);
405
+ if (!same)
406
+ wb.setSelection(next);
407
+ }, [wb]);
408
+ return {
409
+ onConnect,
410
+ onNodesChange,
411
+ onEdgesChange,
412
+ onEdgesDelete,
413
+ onNodesDelete,
414
+ onSelectionChange,
415
+ };
416
+ }
417
+ function useWorkbenchGraphTick(wb) {
418
+ const [tick, setTick] = useState(0);
419
+ useEffect(() => {
420
+ const bump = () => setTick((t) => t + 1);
421
+ const off = wb.on("graphChanged", bump);
422
+ return () => off();
423
+ }, [wb]);
424
+ return tick;
425
+ }
426
+ function useWorkbenchGraphUiTick(wb) {
427
+ const [tick, setTick] = useState(0);
428
+ useEffect(() => {
429
+ const bump = () => setTick((t) => t + 1);
430
+ const off = wb.on("graphUiChanged", bump);
431
+ return () => off();
432
+ }, [wb]);
433
+ return tick;
434
+ }
435
+ function useWorkbenchVersionTick(runner) {
436
+ const [version, setVersion] = useState(0);
437
+ useEffect(() => {
438
+ const bump = () => setVersion((v) => v + 1);
439
+ const u1 = runner.on("value", bump);
440
+ const u2 = runner.on("error", bump);
441
+ const u3 = runner.on("invalidate", bump);
442
+ const u4 = runner.on("status", bump);
443
+ const u5 = runner.on("stats", bump);
444
+ return () => {
445
+ u1();
446
+ u2();
447
+ u3();
448
+ u4();
449
+ u5();
450
+ };
451
+ }, [runner]);
452
+ return version;
453
+ }
454
+ // Query param helpers
455
+ function setSearchParam(key, val) {
456
+ if (typeof window === "undefined")
457
+ return;
458
+ const url = new URL(window.location.href);
459
+ if (val === undefined || val === "")
460
+ url.searchParams.delete(key);
461
+ else
462
+ url.searchParams.set(key, val);
463
+ window.history.replaceState({}, "", url.toString());
464
+ }
465
+ function useQueryParamBoolean(key, defaultValue) {
466
+ const initial = useMemo(() => {
467
+ if (typeof window === "undefined")
468
+ return defaultValue;
469
+ const sp = new URLSearchParams(window.location.search);
470
+ const v = sp.get(key);
471
+ if (v === null)
472
+ return defaultValue;
473
+ return v === "1" || v === "true";
474
+ }, [key, defaultValue]);
475
+ const [val, setVal] = useState(initial);
476
+ const set = useCallback((v) => {
477
+ setVal(v);
478
+ setSearchParam(key, v ? "1" : undefined);
479
+ }, [key]);
480
+ useEffect(() => {
481
+ const onPop = () => {
482
+ const sp = new URLSearchParams(window.location.search);
483
+ const v = sp.get(key);
484
+ setVal(v === "1" || v === "true");
485
+ };
486
+ window.addEventListener("popstate", onPop);
487
+ return () => window.removeEventListener("popstate", onPop);
488
+ }, [key]);
489
+ return [val, set];
490
+ }
491
+ function useQueryParamString(key, defaultValue) {
492
+ const initial = useMemo(() => {
493
+ if (typeof window === "undefined")
494
+ return defaultValue;
495
+ const sp = new URLSearchParams(window.location.search);
496
+ const v = sp.get(key);
497
+ return v ?? defaultValue;
498
+ }, [key, defaultValue]);
499
+ const [val, setVal] = useState(initial);
500
+ const set = useCallback((v) => {
501
+ setVal(v);
502
+ setSearchParam(key, v);
503
+ }, [key]);
504
+ useEffect(() => {
505
+ const onPop = () => {
506
+ const sp = new URLSearchParams(window.location.search);
507
+ const v = sp.get(key) ?? undefined;
508
+ setVal(v);
509
+ };
510
+ window.addEventListener("popstate", onPop);
511
+ return () => window.removeEventListener("popstate", onPop);
512
+ }, [key]);
513
+ return [val, set];
514
+ }
515
+
516
+ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
517
+ const [nodeStatus, setNodeStatus] = useState({});
518
+ const [edgeStatus, setEdgeStatus] = useState({});
519
+ const [events, setEvents] = useState([]);
520
+ const clearEvents = useCallback(() => setEvents([]), []);
521
+ // Validation
522
+ const [validation, setValidation] = useState(undefined);
523
+ // Selection (mirror workbench selectionChanged)
524
+ const [selectedNodeId, setSelectedNodeId] = useState();
525
+ const [selectedEdgeId, setSelectedEdgeId] = useState();
526
+ const setSelection = useCallback((sel) => wb.setSelection(sel), [wb]);
527
+ // Ticks
528
+ const graphTick = useWorkbenchGraphTick(wb);
529
+ const graphUiTick = useWorkbenchGraphUiTick(wb);
530
+ const versionTick = useWorkbenchVersionTick(runner);
531
+ const valuesTick = versionTick + graphTick + graphUiTick;
532
+ // Def and IO values
533
+ const def = wb.export();
534
+ const inputsMap = useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
535
+ const outputsMap = useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
536
+ // Auto layout (simple layered layout)
537
+ const runAutoLayout = useCallback(() => {
538
+ const cur = wb.export();
539
+ const indegree = {};
540
+ const adj = {};
541
+ for (const n of cur.nodes) {
542
+ indegree[n.nodeId] = 0;
543
+ adj[n.nodeId] = [];
544
+ }
545
+ for (const e of cur.edges) {
546
+ indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
547
+ adj[e.source.nodeId].push(e.target.nodeId);
548
+ }
549
+ const q = Object.keys(indegree).filter((k) => indegree[k] === 0);
550
+ const layers = [];
551
+ while (q.length) {
552
+ const layer = [];
553
+ const next = [];
554
+ for (const id of q) {
555
+ layer.push(id);
556
+ for (const nb of adj[id]) {
557
+ indegree[nb] -= 1;
558
+ if (indegree[nb] === 0)
559
+ next.push(nb);
560
+ }
561
+ }
562
+ layers.push(layer);
563
+ q.splice(0, q.length, ...next);
564
+ }
565
+ const X = 360;
566
+ const Y = 180;
567
+ const pos = {};
568
+ layers.forEach((layer, layerIndex) => {
569
+ layer.forEach((id, itemIndex) => {
570
+ pos[id] = { x: layerIndex * X, y: itemIndex * Y };
571
+ });
572
+ });
573
+ wb.setPositions(pos);
574
+ }, [wb]);
575
+ // Subscribe to runner/workbench events
576
+ useEffect(() => {
577
+ const add = (source, type) => (payload) => setEvents((prev) => {
578
+ if (source === "workbench" &&
579
+ (type === "graphChanged" || type === "graphUiChanged")) {
580
+ const changeType = payload?.change?.type;
581
+ if (changeType === "moveNode" || changeType === "moveNodes")
582
+ return prev;
583
+ }
584
+ const next = [
585
+ { at: Date.now(), source, type, payload: structuredClone(payload) },
586
+ ...prev,
587
+ ];
588
+ return next.length > 200 ? next.slice(0, 200) : next;
589
+ });
590
+ const off1 = runner.on("value", (e) => {
591
+ if (e?.io === "input") {
592
+ const nodeId = e?.nodeId;
593
+ setNodeStatus((s) => ({
594
+ ...s,
595
+ [nodeId]: { ...(s[nodeId] ?? {}), invalidated: true },
596
+ }));
597
+ }
598
+ return add("runner", "value")(e);
599
+ });
600
+ const off2 = runner.on("error", (e) => {
601
+ const edgeError = e;
602
+ const nodeError = e;
603
+ if (edgeError?.kind === "edge-convert") {
604
+ const edgeId = edgeError.edgeId;
605
+ setEdgeStatus((s) => ({
606
+ ...s,
607
+ [edgeId]: { ...(s[edgeId] ?? {}), lastError: edgeError.err },
608
+ }));
609
+ }
610
+ else if (nodeError?.nodeId) {
611
+ const nodeId = nodeError?.nodeId;
612
+ setNodeStatus((s) => ({
613
+ ...s,
614
+ [nodeId]: {
615
+ ...(s[nodeId] ?? {}),
616
+ lastError: nodeError?.err,
617
+ },
618
+ }));
619
+ }
620
+ return add("runner", "error")(e);
621
+ });
622
+ const off3 = runner.on("invalidate", (e) => {
623
+ if (e?.reason === "graph-updated") {
624
+ setNodeStatus((s) => {
625
+ const next = {};
626
+ for (const n of wb.export().nodes) {
627
+ next[n.nodeId] = { ...(s[n.nodeId] ?? {}), invalidated: true };
628
+ }
629
+ return next;
630
+ });
631
+ }
632
+ return add("runner", "invalidate")(e);
633
+ });
634
+ const off3b = runner.on("stats", (s) => {
635
+ if (!s)
636
+ return;
637
+ if (s.kind === "node-start") {
638
+ const id = s.nodeId;
639
+ setNodeStatus((prev) => ({
640
+ ...prev,
641
+ [id]: {
642
+ ...(prev[id] ?? {}),
643
+ running: true,
644
+ progress: 0,
645
+ invalidated: false,
646
+ },
647
+ }));
648
+ }
649
+ else if (s.kind === "node-progress") {
650
+ const id = s.nodeId;
651
+ setNodeStatus((prev) => ({
652
+ ...prev,
653
+ [id]: {
654
+ ...(prev[id] ?? {}),
655
+ running: true,
656
+ progress: Number(s.progress) || 0,
657
+ },
658
+ }));
659
+ }
660
+ else if (s.kind === "node-done") {
661
+ const id = s.nodeId;
662
+ setNodeStatus((prev) => ({
663
+ ...prev,
664
+ [id]: { ...(prev[id] ?? {}), running: false },
665
+ }));
666
+ }
667
+ else if (s.kind === "edge-start") {
668
+ const id = s.edgeId;
669
+ setEdgeStatus((prev) => ({
670
+ ...prev,
671
+ [id]: { ...(prev[id] ?? {}), running: true },
672
+ }));
673
+ }
674
+ else if (s.kind === "edge-done") {
675
+ const id = s.edgeId;
676
+ setEdgeStatus((prev) => ({
677
+ ...prev,
678
+ [id]: { ...(prev[id] ?? {}), running: false },
679
+ }));
680
+ }
681
+ return add("runner", "stats")(s);
682
+ });
683
+ const off4 = wb.on("graphChanged", add("workbench", "graphChanged"));
684
+ const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
685
+ const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
686
+ const off5b = wb.on("validationChanged", (r) => setValidation(r));
687
+ const off6 = wb.on("selectionChanged", (sel) => {
688
+ setSelectedNodeId(sel.nodes?.[0]);
689
+ setSelectedEdgeId(sel.edges?.[0]);
690
+ });
691
+ const off7 = wb.on("error", add("workbench", "error"));
692
+ wb.refreshValidation();
693
+ return () => {
694
+ off1();
695
+ off2();
696
+ off3();
697
+ off3b();
698
+ off4();
699
+ off4b();
700
+ off5();
701
+ off5b();
702
+ off6();
703
+ off7();
704
+ };
705
+ }, [runner, wb]);
706
+ // Push incremental updates into running engine without full reload
707
+ useEffect(() => {
708
+ if (runner.isRunning()) {
709
+ try {
710
+ runner.update(def);
711
+ }
712
+ catch { }
713
+ }
714
+ }, [runner, def, graphTick]);
715
+ const validationByNode = useMemo(() => {
716
+ const inputs = {};
717
+ const outputs = {};
718
+ const issues = {};
719
+ if (!validation)
720
+ return { inputs, outputs, issues };
721
+ for (const is of validation.issues ?? []) {
722
+ const d = is?.data;
723
+ const level = is?.level;
724
+ const code = String(is?.code ?? "");
725
+ const message = String(is?.message ?? code);
726
+ if (!d)
727
+ continue;
728
+ if (d.nodeId) {
729
+ if (d.input) {
730
+ const arr = inputs[d.nodeId] ?? (inputs[d.nodeId] = []);
731
+ arr.push({ handle: String(d.input), level, message, code });
732
+ const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
733
+ nodeArr.push({ level, message, code });
734
+ }
735
+ if (d.output) {
736
+ const arr = outputs[d.nodeId] ?? (outputs[d.nodeId] = []);
737
+ arr.push({ handle: String(d.output), level, message, code });
738
+ const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
739
+ nodeArr.push({ level, message, code });
740
+ }
741
+ if (!d.input && !d.output) {
742
+ const arr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
743
+ arr.push({ level, message, code });
744
+ }
745
+ }
746
+ }
747
+ return { inputs, outputs, issues };
748
+ }, [validation]);
749
+ const validationGlobal = useMemo(() => {
750
+ const list = [];
751
+ if (!validation)
752
+ return list;
753
+ for (const is of validation.issues ?? []) {
754
+ const d = is?.data;
755
+ const level = is?.level;
756
+ const code = String(is?.code ?? "");
757
+ const message = String(is?.message ?? code);
758
+ if (!d || (!d.nodeId && !d.edgeId)) {
759
+ list.push({ level, code, message });
760
+ }
761
+ }
762
+ return list;
763
+ }, [validation]);
764
+ const validationByEdge = useMemo(() => {
765
+ const errors = {};
766
+ const issues = {};
767
+ if (!validation)
768
+ return { errors, issues };
769
+ for (const is of validation.issues ?? []) {
770
+ const d = is?.data;
771
+ const level = is?.level;
772
+ const code = String(is?.code ?? "");
773
+ const message = String(is?.message ?? code);
774
+ if (d?.edgeId) {
775
+ if (level === "error")
776
+ errors[d.edgeId] = true;
777
+ const arr = issues[d.edgeId] ?? (issues[d.edgeId] = []);
778
+ arr.push({ level, message, code });
779
+ }
780
+ }
781
+ return { errors, issues };
782
+ }, [validation]);
783
+ const isRunning = useCallback(() => runner.isRunning(), [runner]);
784
+ const engineKind = useCallback(() => runner.getRunningEngine(), [runner]);
785
+ const start = useCallback((engine) => {
786
+ try {
787
+ runner.launch(wb.export(), { engine });
788
+ }
789
+ catch { }
790
+ }, [runner, wb]);
791
+ const stop = useCallback(() => runner.dispose(), [runner]);
792
+ const step = useCallback(() => runner.step(), [runner]);
793
+ const flush = useCallback(() => runner.flush(), [runner]);
794
+ const value = useMemo(() => ({
795
+ wb,
796
+ runner,
797
+ registry,
798
+ setRegistry,
799
+ def,
800
+ selectedNodeId,
801
+ selectedEdgeId,
802
+ setSelection,
803
+ nodeStatus,
804
+ edgeStatus,
805
+ valuesTick,
806
+ inputsMap,
807
+ outputsMap,
808
+ validationByNode,
809
+ validationByEdge,
810
+ validationGlobal,
811
+ events,
812
+ clearEvents,
813
+ isRunning,
814
+ engineKind,
815
+ start,
816
+ stop,
817
+ step,
818
+ flush,
819
+ runAutoLayout,
820
+ }), [
821
+ wb,
822
+ runner,
823
+ registry,
824
+ setRegistry,
825
+ def,
826
+ selectedNodeId,
827
+ selectedEdgeId,
828
+ setSelection,
829
+ nodeStatus,
830
+ edgeStatus,
831
+ valuesTick,
832
+ inputsMap,
833
+ outputsMap,
834
+ validationByNode,
835
+ validationByEdge,
836
+ validationGlobal,
837
+ events,
838
+ clearEvents,
839
+ isRunning,
840
+ engineKind,
841
+ start,
842
+ stop,
843
+ step,
844
+ flush,
845
+ runAutoLayout,
846
+ ]);
847
+ return (jsx(WorkbenchContext.Provider, { value: value, children: children }));
848
+ }
849
+
850
+ function toReactFlow(def, positions, registry, selectedNodeIds, selectedEdgeIds, opts) {
851
+ const nodeHandleMap = {};
852
+ const nodes = def.nodes.map((n) => {
853
+ const desc = registry.nodes.get(n.typeId);
854
+ const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
855
+ const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
856
+ nodeHandleMap[n.nodeId] = {
857
+ inputs: new Set(inputHandles.map((h) => h.id)),
858
+ outputs: new Set(outputHandles.map((h) => h.id)),
859
+ };
860
+ return {
861
+ id: n.nodeId,
862
+ data: {
863
+ typeId: n.typeId,
864
+ params: n.params,
865
+ inputHandles,
866
+ outputHandles,
867
+ showValues: opts?.showValues,
868
+ inputValues: opts?.inputs?.[n.nodeId],
869
+ outputValues: opts?.outputs?.[n.nodeId],
870
+ status: opts?.nodeStatus?.[n.nodeId],
871
+ validation: {
872
+ inputs: opts?.nodeValidation?.inputs?.[n.nodeId] ?? [],
873
+ outputs: opts?.nodeValidation?.outputs?.[n.nodeId] ?? [],
874
+ issues: opts?.nodeValidation?.issues?.[n.nodeId] ?? [],
875
+ },
876
+ toDisplay: opts?.toDisplay,
877
+ },
878
+ position: positions[n.nodeId] ?? { x: 0, y: 0 },
879
+ type: opts?.resolveNodeType?.(n.typeId) ?? "@bian-womp/spark:default",
880
+ selected: selectedNodeIds ? selectedNodeIds.has(n.nodeId) : undefined,
881
+ };
882
+ });
883
+ const edges = def.edges
884
+ .filter((e) => {
885
+ const src = nodeHandleMap[e.source.nodeId];
886
+ const dst = nodeHandleMap[e.target.nodeId];
887
+ if (!src || !dst)
888
+ return false;
889
+ return src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle);
890
+ })
891
+ .map((e) => {
892
+ const st = opts?.edgeStatus?.[e.id];
893
+ const isRunning = !!st?.running;
894
+ const hasError = !!st?.lastError;
895
+ const isInvalidEdge = !!opts?.edgeValidation?.[e.id];
896
+ const style = hasError || isInvalidEdge
897
+ ? { stroke: "#ef4444", strokeWidth: 2 }
898
+ : isRunning
899
+ ? { stroke: "#3b82f6" }
900
+ : undefined;
901
+ return {
902
+ id: e.id,
903
+ source: e.source.nodeId,
904
+ target: e.target.nodeId,
905
+ sourceHandle: e.source.handle,
906
+ targetHandle: e.target.handle,
907
+ selected: selectedEdgeIds ? selectedEdgeIds.has(e.id) : undefined,
908
+ animated: isRunning,
909
+ style,
910
+ };
911
+ });
912
+ return { nodes, edges };
913
+ }
914
+
915
+ function IssueBadge({ level, title, size = 12, className, }) {
916
+ const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
917
+ return (jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsx(XCircleIcon, { size: size, weight: "fill" })) : (jsx(WarningCircleIcon, { size: size, weight: "fill" })) }));
918
+ }
919
+
920
+ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
921
+ const typeId = data.typeId;
922
+ const inputEntries = data.inputHandles ?? [];
923
+ const outputEntries = data.outputHandles ?? [];
924
+ const status = data.status ?? {};
925
+ const validation = data.validation ?? {
926
+ inputs: [],
927
+ outputs: [],
928
+ issues: [],
929
+ };
930
+ const HEADER_SIZE = 24;
931
+ const ROW_SIZE = 22;
932
+ const maxRows = Math.max(inputEntries.length, outputEntries.length);
933
+ const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
934
+ const minWidth = data.showValues ? 320 : 160;
935
+ const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
936
+ const hasError = !!status.lastError;
937
+ const isRunning = !!status.running;
938
+ const isInvalid = !!status.invalidated && !isRunning && !hasError;
939
+ const borderClasses = selected
940
+ ? "border-2 border-gray-900 dark:border-gray-100"
941
+ : hasError
942
+ ? "border-2 border-red-500"
943
+ : isRunning
944
+ ? "border-2 border-blue-500 ring-2 ring-blue-200 dark:ring-blue-900"
945
+ : isInvalid
946
+ ? "border-2 border-amber-500 border-dashed"
947
+ : "border border-gray-500 dark:border-gray-400";
948
+ const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
949
+ return (jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900 border-solid", borderClasses), style: { position: "relative", minHeight: minHeight, minWidth }, children: [jsxs("div", { className: "flex h-6 items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", children: [jsx("strong", { className: "flex-1 h-full leading-6 text-xs", children: typeId }), jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsx(XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
950
+ ? "error"
951
+ : "warning", size: 12, className: "w-3 h-3", title: validation.issues
952
+ .map((v) => `${v.code}: ${v.message}`)
953
+ .join("; ") })), jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }), (isRunning || pct > 0) && (jsx("div", { className: "h-1 bg-blue-200 dark:bg-blue-900", children: jsx("div", { className: "h-1 bg-blue-500 transition-all", style: { width: `${pct}%` } }) })), inputEntries.map((entry, i) => {
954
+ const vIssues = validation.inputs.filter((v) => v.handle === entry.id);
955
+ const hasAny = vIssues.length > 0;
956
+ const hasErr = vIssues.some((v) => v.level === "error");
957
+ const title = vIssues
958
+ .map((v) => `${v.code}: ${v.message}`)
959
+ .join("; ");
960
+ return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "target", position: Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8 }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), data.showValues && (jsx("span", { className: "ml-1 opacity-60", children: data.toDisplay
961
+ ? data.toDisplay(entry.typeId, data.inputValues?.[entry.id])
962
+ : String(data.inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
963
+ }), outputEntries.map((entry, i) => {
964
+ const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
965
+ const hasAny = vIssues.length > 0;
966
+ const hasErr = vIssues.some((v) => v.level === "error");
967
+ const title = vIssues
968
+ .map((v) => `${v.code}: ${v.message}`)
969
+ .join("; ");
970
+ return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "source", position: Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8, textAlign: "right" }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), data.showValues && (jsx("span", { className: "ml-1 opacity-60", children: data.toDisplay
971
+ ? data.toDisplay(entry.typeId, data.outputValues?.[entry.id])
972
+ : String(data.outputValues?.[entry.id]) }))] })] }, `out-${entry.id}`));
973
+ })] }));
974
+ });
975
+ DefaultNode.displayName = "DefaultNode";
976
+
977
+ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
978
+ const { registry } = useWorkbenchContext();
979
+ const rf = useReactFlow();
980
+ if (!open || !clientPos)
981
+ return null;
982
+ const items = Array.from(registry.nodes.keys());
983
+ const handleClick = (typeId) => {
984
+ const p = rf.project({ x: clientPos.x, y: clientPos.y });
985
+ onAdd(typeId, p);
986
+ onClose();
987
+ };
988
+ return (jsxs("div", { className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: clientPos.x, top: clientPos.y }, onMouseLeave: onClose, children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Add Node" }), jsx("div", { className: "max-h-60 overflow-auto", children: items.map((id) => (jsx("button", { onClick: () => handleClick(id), className: "block w-full text-left px-2 py-1 hover:bg-gray-100 cursor-pointer", children: id }, id))) })] }));
989
+ }
990
+
991
+ function WorkbenchCanvas({ showValues, toDisplay, }) {
992
+ const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
993
+ const ioValues = { inputs: inputsMap, outputs: outputsMap };
994
+ const nodeValidation = validationByNode;
995
+ const edgeValidation = validationByEdge.errors;
996
+ const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
997
+ const { nodeTypes, resolveNodeType } = useMemo(() => {
998
+ // Build nodeTypes map using UI extension registry
999
+ const ui = wb.getUI();
1000
+ const custom = new Map();
1001
+ for (const typeId of Array.from(registry.nodes.keys())) {
1002
+ const renderer = ui.getNodeRenderer(typeId);
1003
+ if (renderer)
1004
+ custom.set(typeId, renderer);
1005
+ }
1006
+ const types = { "@bian-womp/spark:default": DefaultNode };
1007
+ for (const [typeId, comp] of custom.entries()) {
1008
+ types[`spark:${typeId}`] = comp;
1009
+ }
1010
+ const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark:${nodeTypeId}` : "@bian-womp/spark:default";
1011
+ return { nodeTypes: types, resolveNodeType: resolver };
1012
+ // registry is stable; ui renderers expected to be set up before mount
1013
+ }, [wb, registry]);
1014
+ const { nodes, edges } = useMemo(() => {
1015
+ const def = wb.export();
1016
+ const sel = wb.getSelection();
1017
+ return toReactFlow(def, wb.getPositions(), registry, new Set(sel.nodes), new Set(sel.edges), {
1018
+ showValues,
1019
+ inputs: ioValues.inputs,
1020
+ outputs: ioValues.outputs,
1021
+ resolveNodeType,
1022
+ toDisplay,
1023
+ nodeStatus,
1024
+ edgeStatus,
1025
+ nodeValidation,
1026
+ edgeValidation,
1027
+ });
1028
+ }, [
1029
+ showValues,
1030
+ ioValues,
1031
+ valuesTick,
1032
+ toDisplay,
1033
+ nodeStatus,
1034
+ edgeStatus,
1035
+ nodeValidation,
1036
+ edgeValidation,
1037
+ ]);
1038
+ const [menuOpen, setMenuOpen] = useState(false);
1039
+ const [menuPos, setMenuPos] = useState(null);
1040
+ const onContextMenu = (e) => {
1041
+ e.preventDefault();
1042
+ setMenuPos({ x: e.clientX, y: e.clientY });
1043
+ setMenuOpen(true);
1044
+ };
1045
+ const addNodeAt = (typeId, pos) => {
1046
+ wb.addNode({ typeId, position: pos });
1047
+ };
1048
+ return (jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, selectionOnDrag: true, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onSelectionChange: onSelectionChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, children: [jsx(Background, {}), jsx(MiniMap, {}), jsx(Controls, {}), jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) })] }) }));
1049
+ }
1050
+
1051
+ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWorkbenchChange, }) {
1052
+ const { events, clearEvents } = useWorkbenchContext();
1053
+ const scrollRef = useRef(null);
1054
+ const rows = useMemo(() => {
1055
+ const filtered = hideWorkbench
1056
+ ? events.filter((e) => e.source !== "workbench")
1057
+ : events;
1058
+ return filtered.slice().reverse();
1059
+ }, [events, hideWorkbench]);
1060
+ useEffect(() => {
1061
+ if (!autoScroll)
1062
+ return;
1063
+ const el = scrollRef.current;
1064
+ if (!el)
1065
+ return;
1066
+ el.scrollTop = el.scrollHeight;
1067
+ }, [rows, autoScroll]);
1068
+ const renderPayload = (v) => {
1069
+ try {
1070
+ return JSON.stringify(v, null, 0);
1071
+ }
1072
+ catch {
1073
+ return String(v);
1074
+ }
1075
+ };
1076
+ return (jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsx("div", { className: "font-semibold", children: "Events" }), jsxs("div", { className: "flex items-center gap-2", children: [jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsx("span", { children: "Hide workbench" })] }), jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsx("span", { children: "Auto scroll" })] }), jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev, idx) => (jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxs("div", { className: "flex items-baseline gap-2", children: [jsx("span", { className: "w-8 shrink-0 text-right text-gray-500 select-none", children: idx + 1 }), jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsx("pre", { className: "m-0 whitespace-pre-wrap ml-10", children: renderPayload(ev.payload) })] }, `${ev.at}:${idx}`))) })] }));
1077
+ }
1078
+
1079
+ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toDisplay, setInput, }) {
1080
+ const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
1081
+ const nodeValidationIssues = validationByNode.issues;
1082
+ const edgeValidationIssues = validationByEdge.issues;
1083
+ const nodeValidationHandles = validationByNode;
1084
+ const globalValidationIssues = validationGlobal;
1085
+ const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
1086
+ const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
1087
+ const selectedDesc = selectedNode
1088
+ ? registry.nodes.get(selectedNode.typeId)
1089
+ : undefined;
1090
+ const inputHandles = Object.keys(selectedDesc?.inputs ?? {});
1091
+ const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
1092
+ const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
1093
+ const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
1094
+ const selectedNodeStatus = selectedNodeId
1095
+ ? nodeStatus?.[selectedNodeId]
1096
+ : undefined;
1097
+ const selectedNodeValidation = selectedNodeId
1098
+ ? nodeValidationIssues?.[selectedNodeId] ?? []
1099
+ : [];
1100
+ const selectedEdgeValidation = selectedEdge
1101
+ ? edgeValidationIssues?.[selectedEdge.id] ?? []
1102
+ : [];
1103
+ const selectedNodeHandleValidation = selectedNodeId
1104
+ ? {
1105
+ inputs: nodeValidationHandles?.inputs?.[selectedNodeId] ?? [],
1106
+ outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
1107
+ }
1108
+ : { inputs: [], outputs: [] };
1109
+ // Local drafts and originals for commit-on-blur/enter behavior
1110
+ const [drafts, setDrafts] = useState({});
1111
+ const [originals, setOriginals] = useState({});
1112
+ // Initialize drafts from current inputs whenever selection or valuesTick change,
1113
+ // but do not clobber fields currently being edited (dirty drafts)
1114
+ useEffect(() => {
1115
+ const shallowEqual = (a, b) => {
1116
+ const ak = Object.keys(a);
1117
+ const bk = Object.keys(b);
1118
+ if (ak.length !== bk.length)
1119
+ return false;
1120
+ for (const k of ak)
1121
+ if (a[k] !== b[k])
1122
+ return false;
1123
+ return true;
1124
+ };
1125
+ if (!selectedNodeId) {
1126
+ if (Object.keys(drafts).length || Object.keys(originals).length) {
1127
+ setDrafts({});
1128
+ setOriginals({});
1129
+ }
1130
+ return;
1131
+ }
1132
+ const desc = selectedDesc;
1133
+ const handles = Object.keys(desc?.inputs ?? {});
1134
+ const nextDrafts = { ...drafts };
1135
+ const nextOriginals = { ...originals };
1136
+ for (const h of handles) {
1137
+ const typeId = desc?.inputs?.[h];
1138
+ const current = nodeInputs[h];
1139
+ const display = toDisplay(typeId, current);
1140
+ const wasOriginal = originals[h];
1141
+ const isDirty = drafts[h] !== undefined &&
1142
+ wasOriginal !== undefined &&
1143
+ drafts[h] !== wasOriginal;
1144
+ if (!isDirty) {
1145
+ nextDrafts[h] = display;
1146
+ nextOriginals[h] = display;
1147
+ }
1148
+ }
1149
+ if (!shallowEqual(drafts, nextDrafts))
1150
+ setDrafts(nextDrafts);
1151
+ if (!shallowEqual(originals, nextOriginals))
1152
+ setOriginals(nextOriginals);
1153
+ }, [selectedNodeId, selectedDesc, valuesTick]);
1154
+ const widthClass = debug ? "w-[480px]" : "w-[320px]";
1155
+ return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxs("div", { children: [jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxs("div", { children: [jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxs("div", { children: [selectedNode && (jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
1156
+ selectedNodeStatus.lastError) }))] })), jsxs("div", { className: "mb-2", children: [jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
1157
+ const typeId = (selectedDesc?.inputs ?? {})[h];
1158
+ const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === h);
1159
+ const commonProps = {
1160
+ style: { flex: 1 },
1161
+ disabled: isLinked,
1162
+ };
1163
+ const current = nodeInputs[h];
1164
+ const value = drafts[h] ?? toDisplay(typeId, current);
1165
+ const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
1166
+ const commit = () => {
1167
+ const draft = drafts[h];
1168
+ if (draft === undefined)
1169
+ return;
1170
+ setInput(h, draft);
1171
+ setOriginals((o) => ({ ...o, [h]: draft }));
1172
+ };
1173
+ const revert = () => {
1174
+ const orig = originals[h] ?? toDisplay(typeId, current);
1175
+ setDrafts((d) => ({ ...d, [h]: orig }));
1176
+ };
1177
+ const isEnum = typeId?.startsWith("enum:");
1178
+ const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
1179
+ const hasValidation = inIssues.length > 0;
1180
+ const hasErr = inIssues.some((m) => m.level === "error");
1181
+ const title = inIssues
1182
+ .map((v) => `${v.code}: ${v.message}`)
1183
+ .join("; ");
1184
+ return (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-28", children: [h, jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value: drafts[h] ?? toDisplay(typeId, current), onChange: (e) => {
1185
+ const label = String(e.target.value);
1186
+ const byLabel = registry.getEnumValue(typeId, label);
1187
+ let raw = (byLabel !== undefined ? byLabel : Number(label));
1188
+ if (!Number.isFinite(raw))
1189
+ raw = undefined;
1190
+ setInput(h, raw);
1191
+ const display = toDisplay(typeId, raw);
1192
+ setDrafts((d) => ({ ...d, [h]: display }));
1193
+ setOriginals((o) => ({ ...o, [h]: display }));
1194
+ }, ...commonProps, children: [jsx("option", { value: "", children: "(select)" }), registry.getEnumOptions?.(typeId).map((opt) => (jsx("option", { value: opt.label, children: opt.label }, opt.value)))] })) : (jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", placeholder: isLinked ? "wired" : undefined, value: value, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
1195
+ if (e.key === "Enter")
1196
+ commit();
1197
+ if (e.key === "Escape")
1198
+ revert();
1199
+ }, ...commonProps }))] }, h));
1200
+ }))] }), jsxs("div", { children: [jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsx("label", { className: "w-20", children: h }), jsx("div", { className: "flex-1", children: toDisplay(selectedDesc?.outputs?.[h], nodeOutputs[h]) }), (() => {
1201
+ const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
1202
+ if (outIssues.length === 0)
1203
+ return null;
1204
+ const outErr = outIssues.some((m) => m.level === "error");
1205
+ const outTitle = outIssues
1206
+ .map((v) => `${v.code}: ${v.message}`)
1207
+ .join("; ");
1208
+ return (jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
1209
+ })()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) }), debug && (jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
1210
+ }
1211
+
1212
+ class GraphRunner {
1213
+ constructor(registry, backend) {
1214
+ this.registry = registry;
1215
+ this.listeners = new Map();
1216
+ this.stagedInputs = {};
1217
+ this.backend = { kind: "local" };
1218
+ if (backend)
1219
+ this.backend = backend;
1220
+ }
1221
+ build(def) {
1222
+ if (this.backend.kind === "local") {
1223
+ const builder = new GraphBuilder(this.registry);
1224
+ this.runtime = builder.build(def);
1225
+ return;
1226
+ }
1227
+ // Remote: no-op here; build is performed on remote server during launch
1228
+ }
1229
+ update(def) {
1230
+ if (this.backend.kind === "local") {
1231
+ if (!this.runtime)
1232
+ return;
1233
+ this.runtime.update(def, this.registry);
1234
+ this.emit("invalidate", { reason: "graph-updated" });
1235
+ return;
1236
+ }
1237
+ // Remote: forward update; ignore errors (fire-and-forget)
1238
+ void this.ensureRemote().then(async (rc) => {
1239
+ try {
1240
+ await rc.runner.update(def);
1241
+ this.emit("invalidate", { reason: "graph-updated" });
1242
+ }
1243
+ catch { }
1244
+ });
1245
+ }
1246
+ launch(def, opts) {
1247
+ if (this.engine) {
1248
+ throw new Error("Engine already running. Stop the current engine first.");
1249
+ }
1250
+ if (this.backend.kind === "local") {
1251
+ this.build(def);
1252
+ if (!this.runtime)
1253
+ throw new Error("Runtime not built");
1254
+ const rt = this.runtime;
1255
+ switch (opts.engine) {
1256
+ case "push":
1257
+ this.engine = new PushEngine(rt);
1258
+ break;
1259
+ case "batched":
1260
+ this.engine = new BatchedEngine(rt, {
1261
+ flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
1262
+ });
1263
+ break;
1264
+ case "pull":
1265
+ this.engine = new PullEngine(rt);
1266
+ break;
1267
+ case "hybrid":
1268
+ this.engine = new HybridEngine(rt, {
1269
+ windowMs: opts.hybrid?.windowMs ?? 250,
1270
+ batchThreshold: opts.hybrid?.batchThreshold ?? 3,
1271
+ });
1272
+ break;
1273
+ case "step":
1274
+ this.engine = new StepEngine(rt);
1275
+ break;
1276
+ default:
1277
+ throw new Error("Unknown engine kind");
1278
+ }
1279
+ this.engine.on("value", (e) => this.emit("value", e));
1280
+ this.engine.on("error", (e) => this.emit("error", e));
1281
+ this.engine.on("invalidate", (e) => this.emit("invalidate", e));
1282
+ this.engine.on("stats", (e) => this.emit("stats", e));
1283
+ this.engine.launch();
1284
+ this.runningKind = opts.engine;
1285
+ this.emit("status", { running: true, engine: this.runningKind });
1286
+ for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
1287
+ for (const [handle, value] of Object.entries(map)) {
1288
+ this.engine.setInput(nodeId, handle, value);
1289
+ }
1290
+ }
1291
+ return;
1292
+ }
1293
+ // Remote: build remotely then launch
1294
+ void this.ensureRemote().then(async (rc) => {
1295
+ await rc.runner.build(def);
1296
+ const eng = rc.runner.getEngine();
1297
+ if (!rc.listenersBound) {
1298
+ eng.on("value", (e) => {
1299
+ rc.valueCache.set(`${e.nodeId}.${e.handle}`, {
1300
+ io: e.io,
1301
+ value: e.value,
1302
+ });
1303
+ this.emit("value", e);
1304
+ });
1305
+ eng.on("error", (e) => this.emit("error", e));
1306
+ eng.on("invalidate", (e) => this.emit("invalidate", e));
1307
+ eng.on("stats", (e) => this.emit("stats", e));
1308
+ rc.listenersBound = true;
1309
+ }
1310
+ this.engine = eng;
1311
+ this.engine.launch();
1312
+ this.runningKind = "push";
1313
+ this.emit("status", { running: true, engine: this.runningKind });
1314
+ for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
1315
+ for (const [handle, value] of Object.entries(map)) {
1316
+ this.engine.setInput(nodeId, handle, value);
1317
+ }
1318
+ }
1319
+ });
1320
+ }
1321
+ setInput(nodeId, handle, value) {
1322
+ if (!this.stagedInputs[nodeId])
1323
+ this.stagedInputs[nodeId] = {};
1324
+ this.stagedInputs[nodeId][handle] = value;
1325
+ if (this.engine)
1326
+ this.engine.setInput(nodeId, handle, value);
1327
+ }
1328
+ async step() {
1329
+ if (this.backend.kind !== "local")
1330
+ return; // unsupported remotely
1331
+ const eng = this.engine;
1332
+ if (eng instanceof StepEngine)
1333
+ await eng.step();
1334
+ }
1335
+ async computeNode(nodeId) {
1336
+ if (this.backend.kind !== "local")
1337
+ return; // unsupported remotely
1338
+ const eng = this.engine;
1339
+ if (eng instanceof PullEngine)
1340
+ await eng.computeNode(nodeId);
1341
+ }
1342
+ flush() {
1343
+ if (this.backend.kind !== "local")
1344
+ return; // unsupported remotely
1345
+ const eng = this.engine;
1346
+ if (eng instanceof BatchedEngine)
1347
+ eng.flush();
1348
+ }
1349
+ getOutputs(def) {
1350
+ const out = {};
1351
+ if (this.backend.kind === "local") {
1352
+ if (!this.runtime)
1353
+ return out;
1354
+ for (const n of def.nodes) {
1355
+ const desc = this.registry.nodes.get(n.typeId);
1356
+ const handles = Object.keys(desc?.outputs ?? {});
1357
+ for (const h of handles) {
1358
+ const v = this.runtime.getOutput(n.nodeId, h);
1359
+ if (v !== undefined) {
1360
+ if (!out[n.nodeId])
1361
+ out[n.nodeId] = {};
1362
+ out[n.nodeId][h] = v;
1363
+ }
1364
+ }
1365
+ }
1366
+ return out;
1367
+ }
1368
+ const cache = this.remote?.valueCache;
1369
+ if (!cache)
1370
+ return out;
1371
+ for (const n of def.nodes) {
1372
+ const desc = this.registry.nodes.get(n.typeId);
1373
+ const handles = Object.keys(desc?.outputs ?? {});
1374
+ for (const h of handles) {
1375
+ const key = `${n.nodeId}.${h}`;
1376
+ const rec = cache.get(key);
1377
+ if (rec && rec.io === "output") {
1378
+ if (!out[n.nodeId])
1379
+ out[n.nodeId] = {};
1380
+ out[n.nodeId][h] = rec.value;
1381
+ }
1382
+ }
1383
+ }
1384
+ return out;
1385
+ }
1386
+ getInputs(def) {
1387
+ const out = {};
1388
+ if (this.backend.kind === "local") {
1389
+ for (const n of def.nodes) {
1390
+ const staged = this.stagedInputs[n.nodeId] ?? {};
1391
+ const runtimeInputs = this.runtime
1392
+ ? this.runtime.__unsafe_getNodeData?.(n.nodeId)?.inputs ?? {}
1393
+ : {};
1394
+ if (this.isRunning()) {
1395
+ out[n.nodeId] = runtimeInputs;
1396
+ }
1397
+ else {
1398
+ const merged = { ...runtimeInputs, ...staged };
1399
+ if (Object.keys(merged).length > 0)
1400
+ out[n.nodeId] = merged;
1401
+ }
1402
+ }
1403
+ return out;
1404
+ }
1405
+ const cache = this.remote?.valueCache;
1406
+ for (const n of def.nodes) {
1407
+ const staged = this.stagedInputs[n.nodeId] ?? {};
1408
+ const desc = this.registry.nodes.get(n.typeId);
1409
+ const handles = Object.keys(desc?.inputs ?? {});
1410
+ const cur = {};
1411
+ for (const h of handles) {
1412
+ const rec = cache?.get(`${n.nodeId}.${h}`);
1413
+ if (rec && rec.io === "input")
1414
+ cur[h] = rec.value;
1415
+ }
1416
+ const merged = this.isRunning() ? cur : { ...cur, ...staged };
1417
+ if (Object.keys(merged).length > 0)
1418
+ out[n.nodeId] = merged;
1419
+ }
1420
+ return out;
1421
+ }
1422
+ async whenIdle() {
1423
+ await this.engine?.whenIdle();
1424
+ }
1425
+ on(event, handler) {
1426
+ if (!this.listeners.has(event))
1427
+ this.listeners.set(event, new Set());
1428
+ const set = this.listeners.get(event);
1429
+ set.add(handler);
1430
+ return () => set.delete(handler);
1431
+ }
1432
+ emit(event, payload) {
1433
+ const set = this.listeners.get(event);
1434
+ if (set)
1435
+ for (const h of Array.from(set))
1436
+ h(payload);
1437
+ }
1438
+ dispose() {
1439
+ this.engine?.dispose();
1440
+ this.engine = undefined;
1441
+ this.runtime?.dispose();
1442
+ this.runtime = undefined;
1443
+ this.remote = undefined;
1444
+ if (this.runningKind) {
1445
+ this.runningKind = undefined;
1446
+ this.emit("status", { running: false, engine: undefined });
1447
+ }
1448
+ }
1449
+ isRunning() {
1450
+ return !!this.engine;
1451
+ }
1452
+ getRunningEngine() {
1453
+ return this.runningKind;
1454
+ }
1455
+ // Ensure remote transport/runner
1456
+ async ensureRemote() {
1457
+ if (this.remote)
1458
+ return this.remote;
1459
+ let transport;
1460
+ if (this.backend.kind === "remote-http") {
1461
+ if (!HttpPollingTransport)
1462
+ throw new Error("HttpPollingTransport not available");
1463
+ transport = new HttpPollingTransport(this.backend.baseUrl);
1464
+ await transport.connect();
1465
+ }
1466
+ else if (this.backend.kind === "remote-ws") {
1467
+ if (!WebSocketTransport)
1468
+ throw new Error("WebSocketTransport not available");
1469
+ transport = new WebSocketTransport(this.backend.url);
1470
+ await transport.connect();
1471
+ }
1472
+ else {
1473
+ throw new Error("Remote backend not configured");
1474
+ }
1475
+ const runner = new RemoteRunner(transport);
1476
+ this.remote = {
1477
+ runner,
1478
+ transport,
1479
+ valueCache: new Map(),
1480
+ listenersBound: false,
1481
+ };
1482
+ return this.remote;
1483
+ }
1484
+ }
1485
+
1486
+ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, }) {
1487
+ const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
1488
+ const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
1489
+ const selectedDesc = selectedNode
1490
+ ? registry.nodes.get(selectedNode.typeId)
1491
+ : undefined;
1492
+ const [exampleState, setExampleState] = useState(example ?? "simple");
1493
+ const lastAutoLaunched = useRef(undefined);
1494
+ const autoLayoutRan = useRef(false);
1495
+ const applyExample = useCallback(async (key) => {
1496
+ if (runner.isRunning()) {
1497
+ alert(`Stop engine before switching example.`);
1498
+ return;
1499
+ }
1500
+ switch (key) {
1501
+ case "simple": {
1502
+ const r = createSimpleGraphRegistry();
1503
+ setRegistry(r);
1504
+ wb.setRegistry(r);
1505
+ await wb.load(createSimpleGraphDef());
1506
+ break;
1507
+ }
1508
+ case "async": {
1509
+ const r = createAsyncGraphRegistry();
1510
+ setRegistry(r);
1511
+ wb.setRegistry(r);
1512
+ await wb.load(createAsyncGraphDef());
1513
+ break;
1514
+ }
1515
+ case "progress": {
1516
+ const r = createProgressGraphRegistry();
1517
+ setRegistry(r);
1518
+ wb.setRegistry(r);
1519
+ await wb.load(createProgressGraphDef());
1520
+ break;
1521
+ }
1522
+ case "validation": {
1523
+ const r = createValidationGraphRegistry();
1524
+ setRegistry(r);
1525
+ wb.setRegistry(r);
1526
+ await wb.load(createValidationGraphDef());
1527
+ break;
1528
+ }
1529
+ default: {
1530
+ const r = createSimpleGraphRegistry();
1531
+ setRegistry(r);
1532
+ wb.setRegistry(r);
1533
+ await wb.load(createSimpleGraphDef());
1534
+ }
1535
+ }
1536
+ runAutoLayout();
1537
+ setExampleState(key);
1538
+ onExampleChange?.(key);
1539
+ }, [runner, wb, onExampleChange, runAutoLayout]);
1540
+ // Ensure initial example is loaded (and sync when example prop changes)
1541
+ useEffect(() => {
1542
+ applyExample(example ?? "simple");
1543
+ }, [example, wb]);
1544
+ useEffect(() => {
1545
+ if (!engine)
1546
+ return;
1547
+ if (runner.isRunning())
1548
+ return;
1549
+ const d = wb.export();
1550
+ if (!d.nodes || d.nodes.length === 0)
1551
+ return;
1552
+ if (lastAutoLaunched.current === engine)
1553
+ return;
1554
+ try {
1555
+ runner.launch(d, { engine: engine });
1556
+ lastAutoLaunched.current = engine;
1557
+ }
1558
+ catch {
1559
+ // ignore
1560
+ }
1561
+ }, [engine, runner, wb]);
1562
+ useEffect(() => {
1563
+ if (autoLayoutRan.current)
1564
+ return;
1565
+ const cur = wb.export();
1566
+ const allMissing = cur.nodes.every((n) => !wb.getPositions()[n.nodeId]);
1567
+ if (allMissing) {
1568
+ autoLayoutRan.current = true;
1569
+ runAutoLayout();
1570
+ }
1571
+ }, [wb, runAutoLayout]);
1572
+ const setInput = useCallback((handle, raw) => {
1573
+ if (!selectedNodeId)
1574
+ return;
1575
+ // If selected input is wired (has inbound edge), ignore user input to respect runtime value
1576
+ const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
1577
+ if (isLinked)
1578
+ return;
1579
+ const typeId = selectedDesc?.inputs?.[handle];
1580
+ let value = raw;
1581
+ const parseArray = (s, map) => {
1582
+ const str = String(s).trim();
1583
+ try {
1584
+ const parsed = JSON.parse(str);
1585
+ if (Array.isArray(parsed))
1586
+ return parsed.map((x) => map(String(x)));
1587
+ }
1588
+ catch { }
1589
+ if (!str)
1590
+ return [];
1591
+ return str
1592
+ .split(",")
1593
+ .map((t) => t.trim())
1594
+ .filter((t) => t.length > 0)
1595
+ .map(map);
1596
+ };
1597
+ switch (typeId) {
1598
+ case "float": {
1599
+ const n = Number(raw);
1600
+ value = Number.isFinite(n) ? n : 0;
1601
+ break;
1602
+ }
1603
+ case "bool": {
1604
+ value = Boolean(raw);
1605
+ break;
1606
+ }
1607
+ case "string": {
1608
+ value = String(raw);
1609
+ break;
1610
+ }
1611
+ case "float[]": {
1612
+ value = parseArray(String(raw), (x) => Number(x));
1613
+ break;
1614
+ }
1615
+ case "bool[]": {
1616
+ value = parseArray(String(raw), (x) => /^(true|1)$/i.test(x));
1617
+ break;
1618
+ }
1619
+ case "vec3": {
1620
+ const arr = parseArray(String(raw), (x) => Number(x));
1621
+ value = [arr[0] ?? 0, arr[1] ?? 0, arr[2] ?? 0];
1622
+ break;
1623
+ }
1624
+ case "vec3[]": {
1625
+ try {
1626
+ const parsed = JSON.parse(String(raw));
1627
+ if (Array.isArray(parsed)) {
1628
+ value = parsed.map((v) => [
1629
+ Number(v?.[0] ?? 0),
1630
+ Number(v?.[1] ?? 0),
1631
+ Number(v?.[2] ?? 0),
1632
+ ]);
1633
+ break;
1634
+ }
1635
+ }
1636
+ catch { }
1637
+ // fallback CSV triples: "x1,y1,z1; x2,y2,z2"
1638
+ value = String(raw)
1639
+ .split(";")
1640
+ .map((seg) => seg.trim())
1641
+ .filter(Boolean)
1642
+ .map((seg) => seg.split(",").map((n) => Number(n.trim())))
1643
+ .map((a) => [a[0] ?? 0, a[1] ?? 0, a[2] ?? 0]);
1644
+ break;
1645
+ }
1646
+ default: {
1647
+ // fallback to string
1648
+ value = raw;
1649
+ }
1650
+ }
1651
+ runner.setInput(selectedNodeId, handle, value);
1652
+ }, [selectedNodeId, def.edges, selectedDesc, runner]);
1653
+ const toDisplay = useCallback((typeId, value) => {
1654
+ if (value === undefined || value === null)
1655
+ return "";
1656
+ if (typeId && typeId.startsWith("enum:")) {
1657
+ const n = Number(value);
1658
+ const label = registry.getEnumLabel(typeId, n);
1659
+ return label ?? String(n);
1660
+ }
1661
+ const round4 = (n) => Math.round(Number(n) * 10000) / 10000;
1662
+ if (typeId === "vec3" && Array.isArray(value)) {
1663
+ const a = value;
1664
+ return [round4(a[0] ?? 0), round4(a[1] ?? 0), round4(a[2] ?? 0)].join(",");
1665
+ }
1666
+ const stringifyRounded = (v) => {
1667
+ try {
1668
+ return JSON.stringify(v, (_k, val) => typeof val === "number" ? round4(val) : val);
1669
+ }
1670
+ catch {
1671
+ return String(v);
1672
+ }
1673
+ };
1674
+ if (typeId?.endsWith("[]") ||
1675
+ Array.isArray(value) ||
1676
+ (typeof value === "object" && value !== null)) {
1677
+ return stringifyRounded(value);
1678
+ }
1679
+ if (typeof value === "number") {
1680
+ const rounded = Math.round(Number(value) * 10000) / 10000;
1681
+ return String(rounded);
1682
+ }
1683
+ return String(value);
1684
+ }, [registry]);
1685
+ return (jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
1686
+ ? "Stop engine before switching example"
1687
+ : undefined, children: [jsx("option", { value: "simple", children: "Simple" }), jsx("option", { value: "async", children: "Async Chain" }), jsx("option", { value: "progress", children: "Progress + Errors" }), jsx("option", { value: "validation", children: "Validation" })] }), jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
1688
+ ? "Stop engine before switching backend"
1689
+ : undefined, children: [jsx("option", { value: "local", children: "Local" }), jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && (jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && (jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: (e) => {
1690
+ const kind = e.target.value || undefined;
1691
+ onEngineChange?.(kind);
1692
+ }, children: [jsx("option", { value: "", children: "Select Engine\u2026" }), jsx("option", { value: "push", children: "Push" }), jsx("option", { value: "batched", children: "Batched" }), jsx("option", { value: "pull", children: "Pull" }), jsx("option", { value: "hybrid", children: "Hybrid" }), jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsx("button", { className: "ml-2", onClick: () => runner.step(), disabled: !runner.isRunning(), children: "Step" })), runner.getRunningEngine() === "batched" && (jsx("button", { className: "ml-2", onClick: () => runner.flush(), disabled: !runner.isRunning(), children: "Flush" })), runner.isRunning() ? (jsx("button", { onClick: () => runner.dispose(), disabled: !runner.isRunning(), children: "Stop" })) : (jsx("button", { onClick: () => {
1693
+ const kind = engine;
1694
+ if (!kind)
1695
+ return alert("Select an engine first.");
1696
+ try {
1697
+ runner.launch(wb.export(), { engine: kind });
1698
+ }
1699
+ catch (err) {
1700
+ alert(String(err?.message ?? err));
1701
+ }
1702
+ }, disabled: !engine, children: "Start" })), jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx("span", { children: "Debug events" })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx("span", { children: "Show values in nodes" })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { showValues: showValues, toDisplay: toDisplay }, exampleState) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toDisplay: toDisplay })] })] }));
1703
+ }
1704
+ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, }) {
1705
+ const [registry, setRegistry] = useState(createSimpleGraphRegistry());
1706
+ const [wb] = useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
1707
+ const runner = useMemo(() => {
1708
+ const backend = backendKind === "remote-http"
1709
+ ? { kind: "remote-http", baseUrl: httpBaseUrl }
1710
+ : backendKind === "remote-ws"
1711
+ ? { kind: "remote-ws", url: wsUrl }
1712
+ : { kind: "local" };
1713
+ return new GraphRunner(registry, backend);
1714
+ }, [registry, backendKind, httpBaseUrl, wsUrl]);
1715
+ return (jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, children: jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
1716
+ if (runner.isRunning())
1717
+ runner.dispose();
1718
+ onBackendKindChange(v);
1719
+ }, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange }) }));
1720
+ }
1721
+
1722
+ function App() {
1723
+ const [engine, setEngine] = useQueryParamString("engine", "");
1724
+ const [example, setExample] = useQueryParamString("example", "simple");
1725
+ const [debug, setDebug] = useQueryParamBoolean("debug", false);
1726
+ const [showValues, setShowValues] = useQueryParamBoolean("values", false);
1727
+ const [hideWorkbench, setHideWorkbench] = useQueryParamBoolean("hideWb", false);
1728
+ const [autoScroll, setAutoScroll] = useQueryParamBoolean("autoScroll", true);
1729
+ // Backend selection via URL params
1730
+ const [backendKind, setBackendKind] = useQueryParamString("backend", "local");
1731
+ const [httpBaseUrl, setHttpBaseUrl] = useQueryParamString("sparkHttp", "http://127.0.0.1:18080");
1732
+ const [wsUrl, setWsUrl] = useQueryParamString("sparkWs", "ws://127.0.0.1:18081");
1733
+ useEffect(() => {
1734
+ document.getElementById("loading-screen")?.remove();
1735
+ }, []);
1736
+ return (jsx(WorkbenchStudio, { engine: engine, onEngineChange: setEngine, example: example, onExampleChange: setExample, backendKind: (backendKind || "local"), onBackendKindChange: (v) => setBackendKind(v), httpBaseUrl: httpBaseUrl || "http://127.0.0.1:18080", onHttpBaseUrlChange: setHttpBaseUrl, wsUrl: wsUrl || "ws://127.0.0.1:18081", onWsUrlChange: setWsUrl, debug: debug, onDebugChange: setDebug, showValues: showValues, onShowValuesChange: setShowValues, hideWorkbench: hideWorkbench, onHideWorkbenchChange: setHideWorkbench, autoScroll: autoScroll, onAutoScrollChange: setAutoScroll }));
1737
+ }
1738
+
1739
+ export { AbstractWorkbench, App, CLIWorkbench, DefaultUIExtensionRegistry, InMemoryWorkbench, ReactFlowWorkbench, toReactFlow$1 as toReactFlow };
1740
+ //# sourceMappingURL=index.js.map