@bian-womp/spark-workbench 0.1.9 → 0.1.11

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 (41) hide show
  1. package/lib/cjs/index.cjs +1353 -1130
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/index.d.ts +8 -3
  4. package/lib/cjs/src/index.d.ts.map +1 -1
  5. package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +1 -1
  6. package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
  7. package/lib/cjs/src/misc/Inspector.d.ts +3 -2
  8. package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
  9. package/lib/cjs/src/misc/NodeContextMenu.d.ts +10 -0
  10. package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +1 -0
  11. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts +3 -2
  12. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/WorkbenchStudio.d.ts +42 -0
  14. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -0
  15. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  16. package/lib/cjs/src/misc/mapping.d.ts +35 -3
  17. package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
  18. package/lib/cjs/src/runtime/GraphRunner.d.ts.map +1 -1
  19. package/lib/esm/index.js +1253 -1042
  20. package/lib/esm/index.js.map +1 -1
  21. package/lib/esm/src/index.d.ts +8 -3
  22. package/lib/esm/src/index.d.ts.map +1 -1
  23. package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +1 -1
  24. package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
  25. package/lib/esm/src/misc/Inspector.d.ts +3 -2
  26. package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
  27. package/lib/esm/src/misc/NodeContextMenu.d.ts +10 -0
  28. package/lib/esm/src/misc/NodeContextMenu.d.ts.map +1 -0
  29. package/lib/esm/src/misc/WorkbenchCanvas.d.ts +3 -2
  30. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  31. package/lib/esm/src/misc/WorkbenchStudio.d.ts +42 -0
  32. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -0
  33. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  34. package/lib/esm/src/misc/mapping.d.ts +35 -3
  35. package/lib/esm/src/misc/mapping.d.ts.map +1 -1
  36. package/lib/esm/src/runtime/GraphRunner.d.ts.map +1 -1
  37. package/package.json +4 -4
  38. package/lib/cjs/src/examples/reactflow/WorkbenchStudio.d.ts +0 -21
  39. package/lib/cjs/src/examples/reactflow/WorkbenchStudio.d.ts.map +0 -1
  40. package/lib/esm/src/examples/reactflow/WorkbenchStudio.d.ts +0 -21
  41. package/lib/esm/src/examples/reactflow/WorkbenchStudio.d.ts.map +0 -1
package/lib/cjs/index.cjs CHANGED
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  var sparkGraph = require('@bian-womp/spark-graph');
4
- var jsxRuntime = require('react/jsx-runtime');
5
- var React = require('react');
6
4
  var sparkRemote = require('@bian-womp/spark-remote');
5
+ var React = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+ var react = require('@phosphor-icons/react');
7
8
  var ReactFlow = require('reactflow');
8
9
  var cx = require('classnames');
9
- var react = require('@phosphor-icons/react');
10
10
 
11
11
  class DefaultUIExtensionRegistry {
12
12
  constructor() {
@@ -319,445 +319,827 @@ class CLIWorkbench {
319
319
  }
320
320
  }
321
321
 
322
- function toReactFlow$1(def, positions = {}) {
323
- const nodes = def.nodes.map((n) => ({
324
- id: n.nodeId,
325
- data: { typeId: n.typeId, params: n.params },
326
- position: positions[n.nodeId] ?? { x: 0, y: 0 },
327
- }));
328
- const edges = def.edges.map((e) => ({
329
- id: e.id,
330
- source: e.source.nodeId,
331
- target: e.target.nodeId,
332
- sourceHandle: e.source.handle,
333
- targetHandle: e.target.handle,
334
- }));
335
- return { nodes, edges };
336
- }
337
- class ReactFlowWorkbench {
338
- constructor(wb) {
339
- this.wb = wb;
340
- }
341
- get actions() {
342
- return this.wb;
343
- }
344
- async load(def) {
345
- await this.wb.load(def);
346
- }
347
- export(def) {
348
- const d = def ?? this.wb.export();
349
- return toReactFlow$1(d);
322
+ class GraphRunner {
323
+ constructor(registry, backend) {
324
+ this.registry = registry;
325
+ this.listeners = new Map();
326
+ this.stagedInputs = {};
327
+ this.backend = { kind: "local" };
328
+ if (backend)
329
+ this.backend = backend;
350
330
  }
351
- }
352
-
353
- const WorkbenchContext = React.createContext(null);
354
- function useWorkbenchContext() {
355
- const ctx = React.useContext(WorkbenchContext);
356
- if (!ctx)
357
- throw new Error("useWorkbenchContext must be used within WorkbenchProvider");
358
- return ctx;
359
- }
360
-
361
- function useWorkbenchBridge(wb) {
362
- const onConnect = React.useCallback((params) => {
363
- if (!params.source || !params.target)
364
- return;
365
- if (!params.sourceHandle || !params.targetHandle)
331
+ build(def) {
332
+ if (this.backend.kind === "local") {
333
+ const builder = new sparkGraph.GraphBuilder(this.registry);
334
+ this.runtime = builder.build(def);
335
+ // Signal UI that freshly built graph should be considered invalidated
336
+ this.emit("invalidate", { reason: "graph-built" });
366
337
  return;
367
- wb.connect({
368
- source: { nodeId: params.source, handle: params.sourceHandle },
369
- target: { nodeId: params.target, handle: params.targetHandle },
370
- });
371
- }, [wb]);
372
- const onNodesChange = React.useCallback((changes) => {
373
- changes.forEach((c) => {
374
- if (c.type === "position" && c.position)
375
- wb.setPosition(c.id, c.position);
376
- if (c.type === "remove")
377
- wb.removeNode(c.id);
378
- if (c.type === "select")
379
- wb.toggleNodeSelection(c.id);
380
- });
381
- }, [wb]);
382
- const onEdgesDelete = React.useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
383
- const onEdgesChange = React.useCallback((changes) => {
384
- changes.forEach((c) => {
385
- if (c.type === "remove")
386
- wb.disconnect(c.id);
387
- else if (c.type === "select")
388
- wb.toggleEdgeSelection(c.id);
389
- });
390
- }, [wb]);
391
- const onNodesDelete = React.useCallback((nodes) => {
392
- for (const n of nodes)
393
- wb.removeNode(n.id);
394
- }, [wb]);
395
- const onSelectionChange = React.useCallback((sel) => {
396
- const next = {
397
- nodes: sel.nodes.map((n) => n.id),
398
- edges: sel.edges.map((e) => e.id),
399
- };
400
- const cur = wb.getSelection();
401
- const sameLen = cur.nodes.length === next.nodes.length &&
402
- cur.edges.length === next.edges.length;
403
- const same = sameLen &&
404
- cur.nodes.every((id, i) => id === next.nodes[i]) &&
405
- cur.edges.every((id, i) => id === next.edges[i]);
406
- if (!same)
407
- wb.setSelection(next);
408
- }, [wb]);
409
- return {
410
- onConnect,
411
- onNodesChange,
412
- onEdgesChange,
413
- onEdgesDelete,
414
- onNodesDelete,
415
- onSelectionChange,
416
- };
417
- }
418
- function useWorkbenchGraphTick(wb) {
419
- const [tick, setTick] = React.useState(0);
420
- React.useEffect(() => {
421
- const bump = () => setTick((t) => t + 1);
422
- const off = wb.on("graphChanged", bump);
423
- return () => off();
424
- }, [wb]);
425
- return tick;
426
- }
427
- function useWorkbenchGraphUiTick(wb) {
428
- const [tick, setTick] = React.useState(0);
429
- React.useEffect(() => {
430
- const bump = () => setTick((t) => t + 1);
431
- const off = wb.on("graphUiChanged", bump);
432
- return () => off();
433
- }, [wb]);
434
- return tick;
435
- }
436
- function useWorkbenchVersionTick(runner) {
437
- const [version, setVersion] = React.useState(0);
438
- React.useEffect(() => {
439
- const bump = () => setVersion((v) => v + 1);
440
- const u1 = runner.on("value", bump);
441
- const u2 = runner.on("error", bump);
442
- const u3 = runner.on("invalidate", bump);
443
- const u4 = runner.on("status", bump);
444
- const u5 = runner.on("stats", bump);
445
- return () => {
446
- u1();
447
- u2();
448
- u3();
449
- u4();
450
- u5();
451
- };
452
- }, [runner]);
453
- return version;
454
- }
455
- // Query param helpers
456
- function setSearchParam(key, val) {
457
- if (typeof window === "undefined")
458
- return;
459
- const url = new URL(window.location.href);
460
- if (val === undefined || val === "")
461
- url.searchParams.delete(key);
462
- else
463
- url.searchParams.set(key, val);
464
- window.history.replaceState({}, "", url.toString());
465
- }
466
- function useQueryParamBoolean(key, defaultValue) {
467
- const initial = React.useMemo(() => {
468
- if (typeof window === "undefined")
469
- return defaultValue;
470
- const sp = new URLSearchParams(window.location.search);
471
- const v = sp.get(key);
472
- if (v === null)
473
- return defaultValue;
474
- return v === "1" || v === "true";
475
- }, [key, defaultValue]);
476
- const [val, setVal] = React.useState(initial);
477
- const set = React.useCallback((v) => {
478
- setVal(v);
479
- setSearchParam(key, v ? "1" : undefined);
480
- }, [key]);
481
- React.useEffect(() => {
482
- const onPop = () => {
483
- const sp = new URLSearchParams(window.location.search);
484
- const v = sp.get(key);
485
- setVal(v === "1" || v === "true");
486
- };
487
- window.addEventListener("popstate", onPop);
488
- return () => window.removeEventListener("popstate", onPop);
489
- }, [key]);
490
- return [val, set];
491
- }
492
- function useQueryParamString(key, defaultValue) {
493
- const initial = React.useMemo(() => {
494
- if (typeof window === "undefined")
495
- return defaultValue;
496
- const sp = new URLSearchParams(window.location.search);
497
- const v = sp.get(key);
498
- return v ?? defaultValue;
499
- }, [key, defaultValue]);
500
- const [val, setVal] = React.useState(initial);
501
- const set = React.useCallback((v) => {
502
- setVal(v);
503
- setSearchParam(key, v);
504
- }, [key]);
505
- React.useEffect(() => {
506
- const onPop = () => {
507
- const sp = new URLSearchParams(window.location.search);
508
- const v = sp.get(key) ?? undefined;
509
- setVal(v);
510
- };
511
- window.addEventListener("popstate", onPop);
512
- return () => window.removeEventListener("popstate", onPop);
513
- }, [key]);
514
- return [val, set];
515
- }
516
-
517
- function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
518
- const [nodeStatus, setNodeStatus] = React.useState({});
519
- const [edgeStatus, setEdgeStatus] = React.useState({});
520
- const [events, setEvents] = React.useState([]);
521
- const clearEvents = React.useCallback(() => setEvents([]), []);
522
- // Validation
523
- const [validation, setValidation] = React.useState(undefined);
524
- // Selection (mirror workbench selectionChanged)
525
- const [selectedNodeId, setSelectedNodeId] = React.useState();
526
- const [selectedEdgeId, setSelectedEdgeId] = React.useState();
527
- const setSelection = React.useCallback((sel) => wb.setSelection(sel), [wb]);
528
- // Ticks
529
- const graphTick = useWorkbenchGraphTick(wb);
530
- const graphUiTick = useWorkbenchGraphUiTick(wb);
531
- const versionTick = useWorkbenchVersionTick(runner);
532
- const valuesTick = versionTick + graphTick + graphUiTick;
533
- // Def and IO values
534
- const def = wb.export();
535
- const inputsMap = React.useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
536
- const outputsMap = React.useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
537
- // Auto layout (simple layered layout)
538
- const runAutoLayout = React.useCallback(() => {
539
- const cur = wb.export();
540
- const indegree = {};
541
- const adj = {};
542
- for (const n of cur.nodes) {
543
- indegree[n.nodeId] = 0;
544
- adj[n.nodeId] = [];
545
- }
546
- for (const e of cur.edges) {
547
- indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
548
- adj[e.source.nodeId].push(e.target.nodeId);
549
338
  }
550
- const q = Object.keys(indegree).filter((k) => indegree[k] === 0);
551
- const layers = [];
552
- while (q.length) {
553
- const layer = [];
554
- const next = [];
555
- for (const id of q) {
556
- layer.push(id);
557
- for (const nb of adj[id]) {
558
- indegree[nb] -= 1;
559
- if (indegree[nb] === 0)
560
- next.push(nb);
561
- }
339
+ // Remote: no-op here; build is performed on remote server during launch
340
+ }
341
+ update(def) {
342
+ if (this.backend.kind === "local") {
343
+ if (!this.runtime)
344
+ return;
345
+ // Prevent mid-run churn while wiring changes are applied
346
+ try {
347
+ this.runtime.pause();
562
348
  }
563
- layers.push(layer);
564
- q.splice(0, q.length, ...next);
349
+ catch {
350
+ console.error("Failed to pause runtime");
351
+ }
352
+ this.runtime.update(def, this.registry);
353
+ try {
354
+ this.runtime.resume();
355
+ }
356
+ catch {
357
+ console.error("Failed to resume runtime");
358
+ }
359
+ this.emit("invalidate", { reason: "graph-updated" });
360
+ return;
565
361
  }
566
- const X = 360;
567
- const Y = 180;
568
- const pos = {};
569
- layers.forEach((layer, layerIndex) => {
570
- layer.forEach((id, itemIndex) => {
571
- pos[id] = { x: layerIndex * X, y: itemIndex * Y };
572
- });
573
- });
574
- wb.setPositions(pos);
575
- }, [wb]);
576
- // Subscribe to runner/workbench events
577
- React.useEffect(() => {
578
- const add = (source, type) => (payload) => setEvents((prev) => {
579
- if (source === "workbench" &&
580
- (type === "graphChanged" || type === "graphUiChanged")) {
581
- const changeType = payload?.change?.type;
582
- if (changeType === "moveNode" || changeType === "moveNodes")
583
- return prev;
584
- }
585
- const next = [
586
- { at: Date.now(), source, type, payload: structuredClone(payload) },
587
- ...prev,
588
- ];
589
- return next.length > 200 ? next.slice(0, 200) : next;
590
- });
591
- const off1 = runner.on("value", (e) => {
592
- if (e?.io === "input") {
593
- const nodeId = e?.nodeId;
594
- setNodeStatus((s) => ({
595
- ...s,
596
- [nodeId]: { ...(s[nodeId] ?? {}), invalidated: true },
597
- }));
362
+ // Remote: forward update; ignore errors (fire-and-forget)
363
+ void this.ensureRemote().then(async (rc) => {
364
+ try {
365
+ await rc.runner.update(def);
366
+ this.emit("invalidate", { reason: "graph-updated" });
598
367
  }
599
- return add("runner", "value")(e);
368
+ catch { }
600
369
  });
601
- const off2 = runner.on("error", (e) => {
602
- const edgeError = e;
603
- const nodeError = e;
604
- if (edgeError?.kind === "edge-convert") {
605
- const edgeId = edgeError.edgeId;
606
- setEdgeStatus((s) => ({
607
- ...s,
608
- [edgeId]: { ...(s[edgeId] ?? {}), lastError: edgeError.err },
609
- }));
370
+ }
371
+ launch(def, opts) {
372
+ if (this.engine) {
373
+ throw new Error("Engine already running. Stop the current engine first.");
374
+ }
375
+ if (this.backend.kind === "local") {
376
+ this.build(def);
377
+ if (!this.runtime)
378
+ throw new Error("Runtime not built");
379
+ const rt = this.runtime;
380
+ switch (opts.engine) {
381
+ case "push":
382
+ this.engine = new sparkGraph.PushEngine(rt);
383
+ break;
384
+ case "batched":
385
+ this.engine = new sparkGraph.BatchedEngine(rt, {
386
+ flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
387
+ });
388
+ break;
389
+ case "pull":
390
+ this.engine = new sparkGraph.PullEngine(rt);
391
+ break;
392
+ case "hybrid":
393
+ this.engine = new sparkGraph.HybridEngine(rt, {
394
+ windowMs: opts.hybrid?.windowMs ?? 250,
395
+ batchThreshold: opts.hybrid?.batchThreshold ?? 3,
396
+ });
397
+ break;
398
+ case "step":
399
+ this.engine = new sparkGraph.StepEngine(rt);
400
+ break;
401
+ default:
402
+ throw new Error("Unknown engine kind");
610
403
  }
611
- else if (nodeError?.nodeId) {
612
- const nodeId = nodeError?.nodeId;
613
- setNodeStatus((s) => ({
614
- ...s,
615
- [nodeId]: {
616
- ...(s[nodeId] ?? {}),
617
- lastError: nodeError?.err,
618
- },
619
- }));
404
+ this.engine.on("value", (e) => this.emit("value", e));
405
+ this.engine.on("error", (e) => this.emit("error", e));
406
+ this.engine.on("invalidate", (e) => this.emit("invalidate", e));
407
+ this.engine.on("stats", (e) => this.emit("stats", e));
408
+ this.engine.launch();
409
+ this.runningKind = opts.engine;
410
+ this.emit("status", { running: true, engine: this.runningKind });
411
+ for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
412
+ for (const [handle, value] of Object.entries(map)) {
413
+ this.engine.setInput(nodeId, handle, value);
414
+ }
620
415
  }
621
- return add("runner", "error")(e);
622
- });
623
- const off3 = runner.on("invalidate", (e) => {
624
- if (e?.reason === "graph-updated") {
625
- setNodeStatus((s) => {
626
- const next = {};
627
- for (const n of wb.export().nodes) {
628
- next[n.nodeId] = { ...(s[n.nodeId] ?? {}), invalidated: true };
629
- }
630
- return next;
416
+ return;
417
+ }
418
+ // Remote: build remotely then launch
419
+ void this.ensureRemote().then(async (rc) => {
420
+ await rc.runner.build(def);
421
+ // Signal UI after remote build as well
422
+ this.emit("invalidate", { reason: "graph-built" });
423
+ const eng = rc.runner.getEngine();
424
+ if (!rc.listenersBound) {
425
+ eng.on("value", (e) => {
426
+ rc.valueCache.set(`${e.nodeId}.${e.handle}`, {
427
+ io: e.io,
428
+ value: e.value,
429
+ });
430
+ this.emit("value", e);
631
431
  });
432
+ eng.on("error", (e) => this.emit("error", e));
433
+ eng.on("invalidate", (e) => this.emit("invalidate", e));
434
+ eng.on("stats", (e) => this.emit("stats", e));
435
+ rc.listenersBound = true;
632
436
  }
633
- return add("runner", "invalidate")(e);
634
- });
635
- const off3b = runner.on("stats", (s) => {
636
- if (!s)
637
- return;
638
- if (s.kind === "node-start") {
639
- const id = s.nodeId;
640
- setNodeStatus((prev) => ({
641
- ...prev,
642
- [id]: {
643
- ...(prev[id] ?? {}),
644
- running: true,
645
- progress: 0,
646
- invalidated: false,
647
- },
648
- }));
649
- }
650
- else if (s.kind === "node-progress") {
651
- const id = s.nodeId;
652
- setNodeStatus((prev) => ({
653
- ...prev,
654
- [id]: {
655
- ...(prev[id] ?? {}),
656
- running: true,
657
- progress: Number(s.progress) || 0,
658
- },
659
- }));
660
- }
661
- else if (s.kind === "node-done") {
662
- const id = s.nodeId;
663
- setNodeStatus((prev) => ({
664
- ...prev,
665
- [id]: { ...(prev[id] ?? {}), running: false },
666
- }));
667
- }
668
- else if (s.kind === "edge-start") {
669
- const id = s.edgeId;
670
- setEdgeStatus((prev) => ({
671
- ...prev,
672
- [id]: { ...(prev[id] ?? {}), running: true },
673
- }));
674
- }
675
- else if (s.kind === "edge-done") {
676
- const id = s.edgeId;
677
- setEdgeStatus((prev) => ({
678
- ...prev,
679
- [id]: { ...(prev[id] ?? {}), running: false },
680
- }));
437
+ this.engine = eng;
438
+ this.engine.launch();
439
+ this.runningKind = "push";
440
+ this.emit("status", { running: true, engine: this.runningKind });
441
+ for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
442
+ for (const [handle, value] of Object.entries(map)) {
443
+ this.engine.setInput(nodeId, handle, value);
444
+ }
681
445
  }
682
- return add("runner", "stats")(s);
683
- });
684
- const off4 = wb.on("graphChanged", add("workbench", "graphChanged"));
685
- const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
686
- const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
687
- const off5b = wb.on("validationChanged", (r) => setValidation(r));
688
- const off6 = wb.on("selectionChanged", (sel) => {
689
- setSelectedNodeId(sel.nodes?.[0]);
690
- setSelectedEdgeId(sel.edges?.[0]);
691
446
  });
692
- const off7 = wb.on("error", add("workbench", "error"));
693
- wb.refreshValidation();
694
- return () => {
695
- off1();
696
- off2();
697
- off3();
698
- off3b();
699
- off4();
700
- off4b();
701
- off5();
702
- off5b();
703
- off6();
704
- off7();
705
- };
706
- }, [runner, wb]);
707
- // Push incremental updates into running engine without full reload
708
- React.useEffect(() => {
709
- if (runner.isRunning()) {
710
- try {
711
- runner.update(def);
712
- }
713
- catch { }
447
+ }
448
+ setInput(nodeId, handle, value) {
449
+ if (!this.stagedInputs[nodeId])
450
+ this.stagedInputs[nodeId] = {};
451
+ this.stagedInputs[nodeId][handle] = value;
452
+ if (this.engine) {
453
+ this.engine.setInput(nodeId, handle, value);
714
454
  }
715
- }, [runner, def, graphTick]);
716
- const validationByNode = React.useMemo(() => {
717
- const inputs = {};
718
- const outputs = {};
719
- const issues = {};
720
- if (!validation)
721
- return { inputs, outputs, issues };
722
- for (const is of validation.issues ?? []) {
723
- const d = is?.data;
724
- const level = is?.level;
725
- const code = String(is?.code ?? "");
726
- const message = String(is?.message ?? code);
727
- if (!d)
728
- continue;
729
- if (d.nodeId) {
730
- if (d.input) {
731
- const arr = inputs[d.nodeId] ?? (inputs[d.nodeId] = []);
732
- arr.push({ handle: String(d.input), level, message, code });
733
- const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
734
- nodeArr.push({ level, message, code });
735
- }
736
- if (d.output) {
737
- const arr = outputs[d.nodeId] ?? (outputs[d.nodeId] = []);
738
- arr.push({ handle: String(d.output), level, message, code });
739
- const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
740
- nodeArr.push({ level, message, code });
741
- }
742
- if (!d.input && !d.output) {
743
- const arr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
744
- arr.push({ level, message, code });
745
- }
746
- }
455
+ else {
456
+ // Emit a value event so UI updates even when engine isn't running
457
+ this.emit("value", { nodeId, handle, value, io: "input" });
747
458
  }
748
- return { inputs, outputs, issues };
749
- }, [validation]);
750
- const validationGlobal = React.useMemo(() => {
751
- const list = [];
752
- if (!validation)
753
- return list;
754
- for (const is of validation.issues ?? []) {
755
- const d = is?.data;
756
- const level = is?.level;
757
- const code = String(is?.code ?? "");
758
- const message = String(is?.message ?? code);
759
- if (!d || (!d.nodeId && !d.edgeId)) {
760
- list.push({ level, code, message });
459
+ }
460
+ async step() {
461
+ if (this.backend.kind !== "local")
462
+ return; // unsupported remotely
463
+ const eng = this.engine;
464
+ if (eng instanceof sparkGraph.StepEngine)
465
+ await eng.step();
466
+ }
467
+ async computeNode(nodeId) {
468
+ if (this.backend.kind !== "local")
469
+ return; // unsupported remotely
470
+ const eng = this.engine;
471
+ if (eng instanceof sparkGraph.PullEngine)
472
+ await eng.computeNode(nodeId);
473
+ }
474
+ flush() {
475
+ if (this.backend.kind !== "local")
476
+ return; // unsupported remotely
477
+ const eng = this.engine;
478
+ if (eng instanceof sparkGraph.BatchedEngine)
479
+ eng.flush();
480
+ }
481
+ getOutputs(def) {
482
+ const out = {};
483
+ if (this.backend.kind === "local") {
484
+ if (!this.runtime)
485
+ return out;
486
+ for (const n of def.nodes) {
487
+ const desc = this.registry.nodes.get(n.typeId);
488
+ const handles = Object.keys(desc?.outputs ?? {});
489
+ for (const h of handles) {
490
+ const v = this.runtime.getOutput(n.nodeId, h);
491
+ if (v !== undefined) {
492
+ if (!out[n.nodeId])
493
+ out[n.nodeId] = {};
494
+ out[n.nodeId][h] = v;
495
+ }
496
+ }
497
+ }
498
+ return out;
499
+ }
500
+ const cache = this.remote?.valueCache;
501
+ if (!cache)
502
+ return out;
503
+ for (const n of def.nodes) {
504
+ const desc = this.registry.nodes.get(n.typeId);
505
+ const handles = Object.keys(desc?.outputs ?? {});
506
+ for (const h of handles) {
507
+ const key = `${n.nodeId}.${h}`;
508
+ const rec = cache.get(key);
509
+ if (rec && rec.io === "output") {
510
+ if (!out[n.nodeId])
511
+ out[n.nodeId] = {};
512
+ out[n.nodeId][h] = rec.value;
513
+ }
514
+ }
515
+ }
516
+ return out;
517
+ }
518
+ getInputs(def) {
519
+ const out = {};
520
+ if (this.backend.kind === "local") {
521
+ for (const n of def.nodes) {
522
+ const staged = this.stagedInputs[n.nodeId] ?? {};
523
+ const runtimeInputs = this.runtime
524
+ ? this.runtime.__unsafe_getNodeData?.(n.nodeId)?.inputs ?? {}
525
+ : {};
526
+ if (this.isRunning()) {
527
+ out[n.nodeId] = runtimeInputs;
528
+ }
529
+ else {
530
+ const merged = { ...runtimeInputs, ...staged };
531
+ if (Object.keys(merged).length > 0)
532
+ out[n.nodeId] = merged;
533
+ }
534
+ }
535
+ return out;
536
+ }
537
+ const cache = this.remote?.valueCache;
538
+ for (const n of def.nodes) {
539
+ const staged = this.stagedInputs[n.nodeId] ?? {};
540
+ const desc = this.registry.nodes.get(n.typeId);
541
+ const handles = Object.keys(desc?.inputs ?? {});
542
+ const cur = {};
543
+ for (const h of handles) {
544
+ const rec = cache?.get(`${n.nodeId}.${h}`);
545
+ if (rec && rec.io === "input")
546
+ cur[h] = rec.value;
547
+ }
548
+ const merged = this.isRunning() ? cur : { ...cur, ...staged };
549
+ if (Object.keys(merged).length > 0)
550
+ out[n.nodeId] = merged;
551
+ }
552
+ return out;
553
+ }
554
+ async whenIdle() {
555
+ await this.engine?.whenIdle();
556
+ }
557
+ on(event, handler) {
558
+ if (!this.listeners.has(event))
559
+ this.listeners.set(event, new Set());
560
+ const set = this.listeners.get(event);
561
+ set.add(handler);
562
+ return () => set.delete(handler);
563
+ }
564
+ emit(event, payload) {
565
+ const set = this.listeners.get(event);
566
+ if (set)
567
+ for (const h of Array.from(set))
568
+ h(payload);
569
+ }
570
+ dispose() {
571
+ this.engine?.dispose();
572
+ this.engine = undefined;
573
+ this.runtime?.dispose();
574
+ this.runtime = undefined;
575
+ this.remote = undefined;
576
+ if (this.runningKind) {
577
+ this.runningKind = undefined;
578
+ this.emit("status", { running: false, engine: undefined });
579
+ }
580
+ }
581
+ isRunning() {
582
+ return !!this.engine;
583
+ }
584
+ getRunningEngine() {
585
+ return this.runningKind;
586
+ }
587
+ // Ensure remote transport/runner
588
+ async ensureRemote() {
589
+ if (this.remote)
590
+ return this.remote;
591
+ let transport;
592
+ if (this.backend.kind === "remote-http") {
593
+ if (!sparkRemote.HttpPollingTransport)
594
+ throw new Error("HttpPollingTransport not available");
595
+ transport = new sparkRemote.HttpPollingTransport(this.backend.baseUrl);
596
+ await transport.connect();
597
+ }
598
+ else if (this.backend.kind === "remote-ws") {
599
+ if (!sparkRemote.WebSocketTransport)
600
+ throw new Error("WebSocketTransport not available");
601
+ transport = new sparkRemote.WebSocketTransport(this.backend.url);
602
+ await transport.connect();
603
+ }
604
+ else {
605
+ throw new Error("Remote backend not configured");
606
+ }
607
+ const runner = new sparkRemote.RemoteRunner(transport);
608
+ this.remote = {
609
+ runner,
610
+ transport,
611
+ valueCache: new Map(),
612
+ listenersBound: false,
613
+ };
614
+ return this.remote;
615
+ }
616
+ }
617
+
618
+ function useWorkbenchBridge(wb) {
619
+ const onConnect = React.useCallback((params) => {
620
+ if (!params.source || !params.target)
621
+ return;
622
+ if (!params.sourceHandle || !params.targetHandle)
623
+ return;
624
+ wb.connect({
625
+ source: { nodeId: params.source, handle: params.sourceHandle },
626
+ target: { nodeId: params.target, handle: params.targetHandle },
627
+ });
628
+ }, [wb]);
629
+ const onNodesChange = React.useCallback((changes) => {
630
+ changes.forEach((c) => {
631
+ if (c.type === "position" && c.position)
632
+ wb.setPosition(c.id, c.position);
633
+ if (c.type === "remove")
634
+ wb.removeNode(c.id);
635
+ if (c.type === "select")
636
+ wb.toggleNodeSelection(c.id);
637
+ });
638
+ }, [wb]);
639
+ const onEdgesDelete = React.useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
640
+ const onEdgesChange = React.useCallback((changes) => {
641
+ changes.forEach((c) => {
642
+ if (c.type === "remove")
643
+ wb.disconnect(c.id);
644
+ else if (c.type === "select")
645
+ wb.toggleEdgeSelection(c.id);
646
+ });
647
+ }, [wb]);
648
+ const onNodesDelete = React.useCallback((nodes) => {
649
+ for (const n of nodes)
650
+ wb.removeNode(n.id);
651
+ }, [wb]);
652
+ const onSelectionChange = React.useCallback((sel) => {
653
+ const next = {
654
+ nodes: sel.nodes.map((n) => n.id),
655
+ edges: sel.edges.map((e) => e.id),
656
+ };
657
+ const cur = wb.getSelection();
658
+ const sameLen = cur.nodes.length === next.nodes.length &&
659
+ cur.edges.length === next.edges.length;
660
+ const same = sameLen &&
661
+ cur.nodes.every((id, i) => id === next.nodes[i]) &&
662
+ cur.edges.every((id, i) => id === next.edges[i]);
663
+ if (!same)
664
+ wb.setSelection(next);
665
+ }, [wb]);
666
+ return {
667
+ onConnect,
668
+ onNodesChange,
669
+ onEdgesChange,
670
+ onEdgesDelete,
671
+ onNodesDelete,
672
+ onSelectionChange,
673
+ };
674
+ }
675
+ function useWorkbenchGraphTick(wb) {
676
+ const [tick, setTick] = React.useState(0);
677
+ React.useEffect(() => {
678
+ const bump = () => setTick((t) => t + 1);
679
+ const off = wb.on("graphChanged", bump);
680
+ return () => off();
681
+ }, [wb]);
682
+ return tick;
683
+ }
684
+ function useWorkbenchGraphUiTick(wb) {
685
+ const [tick, setTick] = React.useState(0);
686
+ React.useEffect(() => {
687
+ const bump = () => setTick((t) => t + 1);
688
+ const off = wb.on("graphUiChanged", bump);
689
+ return () => off();
690
+ }, [wb]);
691
+ return tick;
692
+ }
693
+ function useWorkbenchVersionTick(runner) {
694
+ const [version, setVersion] = React.useState(0);
695
+ React.useEffect(() => {
696
+ const bump = () => setVersion((v) => v + 1);
697
+ const u1 = runner.on("value", bump);
698
+ const u2 = runner.on("error", bump);
699
+ const u3 = runner.on("invalidate", bump);
700
+ const u4 = runner.on("status", bump);
701
+ const u5 = runner.on("stats", bump);
702
+ return () => {
703
+ u1();
704
+ u2();
705
+ u3();
706
+ u4();
707
+ u5();
708
+ };
709
+ }, [runner]);
710
+ return version;
711
+ }
712
+ // Query param helpers
713
+ function setSearchParam(key, val) {
714
+ if (typeof window === "undefined")
715
+ return;
716
+ const url = new URL(window.location.href);
717
+ if (val === undefined || val === "")
718
+ url.searchParams.delete(key);
719
+ else
720
+ url.searchParams.set(key, val);
721
+ window.history.replaceState({}, "", url.toString());
722
+ }
723
+ function useQueryParamBoolean(key, defaultValue) {
724
+ const initial = React.useMemo(() => {
725
+ if (typeof window === "undefined")
726
+ return defaultValue;
727
+ const sp = new URLSearchParams(window.location.search);
728
+ const v = sp.get(key);
729
+ if (v === null)
730
+ return defaultValue;
731
+ return v === "1" || v === "true";
732
+ }, [key, defaultValue]);
733
+ const [val, setVal] = React.useState(initial);
734
+ const set = React.useCallback((v) => {
735
+ setVal(v);
736
+ setSearchParam(key, v ? "1" : undefined);
737
+ }, [key]);
738
+ React.useEffect(() => {
739
+ const onPop = () => {
740
+ const sp = new URLSearchParams(window.location.search);
741
+ const v = sp.get(key);
742
+ setVal(v === "1" || v === "true");
743
+ };
744
+ window.addEventListener("popstate", onPop);
745
+ return () => window.removeEventListener("popstate", onPop);
746
+ }, [key]);
747
+ return [val, set];
748
+ }
749
+ function useQueryParamString(key, defaultValue) {
750
+ const initial = React.useMemo(() => {
751
+ if (typeof window === "undefined")
752
+ return defaultValue;
753
+ const sp = new URLSearchParams(window.location.search);
754
+ const v = sp.get(key);
755
+ return v ?? defaultValue;
756
+ }, [key, defaultValue]);
757
+ const [val, setVal] = React.useState(initial);
758
+ const set = React.useCallback((v) => {
759
+ setVal(v);
760
+ setSearchParam(key, v);
761
+ }, [key]);
762
+ React.useEffect(() => {
763
+ const onPop = () => {
764
+ const sp = new URLSearchParams(window.location.search);
765
+ const v = sp.get(key) ?? undefined;
766
+ setVal(v);
767
+ };
768
+ window.addEventListener("popstate", onPop);
769
+ return () => window.removeEventListener("popstate", onPop);
770
+ }, [key]);
771
+ return [val, set];
772
+ }
773
+
774
+ function toReactFlow(def, positions, registry, opts) {
775
+ const nodeHandleMap = {};
776
+ const nodes = def.nodes.map((n) => {
777
+ const desc = registry.nodes.get(n.typeId);
778
+ const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
779
+ const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
780
+ nodeHandleMap[n.nodeId] = {
781
+ inputs: new Set(inputHandles.map((h) => h.id)),
782
+ outputs: new Set(outputHandles.map((h) => h.id)),
783
+ };
784
+ return {
785
+ id: n.nodeId,
786
+ data: {
787
+ typeId: n.typeId,
788
+ params: n.params,
789
+ inputHandles,
790
+ outputHandles,
791
+ showValues: opts.showValues,
792
+ inputValues: opts.inputs?.[n.nodeId],
793
+ outputValues: opts.outputs?.[n.nodeId],
794
+ status: opts.nodeStatus?.[n.nodeId],
795
+ validation: {
796
+ inputs: opts.nodeValidation?.inputs?.[n.nodeId] ?? [],
797
+ outputs: opts.nodeValidation?.outputs?.[n.nodeId] ?? [],
798
+ issues: opts.nodeValidation?.issues?.[n.nodeId] ?? [],
799
+ },
800
+ toString: opts.toString,
801
+ toElement: opts.toElement,
802
+ },
803
+ position: positions[n.nodeId] ?? { x: 0, y: 0 },
804
+ type: opts.resolveNodeType?.(n.typeId) ?? "spark-default",
805
+ selected: opts.selectedNodeIds
806
+ ? opts.selectedNodeIds.has(n.nodeId)
807
+ : undefined,
808
+ };
809
+ });
810
+ const edges = def.edges
811
+ .filter((e) => {
812
+ const src = nodeHandleMap[e.source.nodeId];
813
+ const dst = nodeHandleMap[e.target.nodeId];
814
+ if (!src || !dst)
815
+ return false;
816
+ return (src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle));
817
+ })
818
+ .map((e) => {
819
+ const st = opts.edgeStatus?.[e.id];
820
+ const isRunning = !!st?.running;
821
+ const hasError = !!st?.lastError;
822
+ const isInvalidEdge = !!opts.edgeValidation?.[e.id];
823
+ const style = hasError || isInvalidEdge
824
+ ? { stroke: "#ef4444", strokeWidth: 2 }
825
+ : isRunning
826
+ ? { stroke: "#3b82f6" }
827
+ : undefined;
828
+ return {
829
+ id: e.id,
830
+ source: e.source.nodeId,
831
+ target: e.target.nodeId,
832
+ sourceHandle: e.source.handle,
833
+ targetHandle: e.target.handle,
834
+ selected: opts.selectedEdgeIds
835
+ ? opts.selectedEdgeIds.has(e.id)
836
+ : undefined,
837
+ animated: isRunning,
838
+ style,
839
+ };
840
+ });
841
+ return { nodes, edges };
842
+ }
843
+ // Shared node container border class composition for consistent visuals
844
+ function getNodeBorderClassNames(args) {
845
+ const selected = !!args.selected;
846
+ const status = args.status || {};
847
+ const issues = args.validation?.issues ?? [];
848
+ const hasError = !!status.lastError;
849
+ const hasValidationError = issues.some((i) => i?.level === "error");
850
+ const hasValidationWarning = !hasValidationError && issues.length > 0;
851
+ const isRunning = !!status.running;
852
+ const isInvalid = !!status.invalidated && !isRunning && !hasError;
853
+ const borderWidth = selected ? "border-2" : "border";
854
+ const borderStyle = isInvalid ? "border-dashed" : "border-solid";
855
+ const borderColor = hasError || hasValidationError
856
+ ? "border-red-500"
857
+ : hasValidationWarning
858
+ ? "border-amber-500"
859
+ : isRunning
860
+ ? "border-blue-500"
861
+ : "border-gray-500 dark:border-gray-400";
862
+ const ring = isRunning ? " ring-2 ring-blue-200 dark:ring-blue-900" : "";
863
+ return `${borderWidth} ${borderStyle} ${borderColor}${ring}`.trim();
864
+ }
865
+
866
+ const WorkbenchContext = React.createContext(null);
867
+ function useWorkbenchContext() {
868
+ const ctx = React.useContext(WorkbenchContext);
869
+ if (!ctx)
870
+ throw new Error("useWorkbenchContext must be used within WorkbenchProvider");
871
+ return ctx;
872
+ }
873
+
874
+ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
875
+ const [nodeStatus, setNodeStatus] = React.useState({});
876
+ const [edgeStatus, setEdgeStatus] = React.useState({});
877
+ const [events, setEvents] = React.useState([]);
878
+ const clearEvents = React.useCallback(() => setEvents([]), []);
879
+ // Validation
880
+ const [validation, setValidation] = React.useState(undefined);
881
+ // Selection (mirror workbench selectionChanged)
882
+ const [selectedNodeId, setSelectedNodeId] = React.useState();
883
+ const [selectedEdgeId, setSelectedEdgeId] = React.useState();
884
+ const setSelection = React.useCallback((sel) => wb.setSelection(sel), [wb]);
885
+ // Ticks
886
+ const graphTick = useWorkbenchGraphTick(wb);
887
+ const graphUiTick = useWorkbenchGraphUiTick(wb);
888
+ const versionTick = useWorkbenchVersionTick(runner);
889
+ const valuesTick = versionTick + graphTick + graphUiTick;
890
+ // Def and IO values
891
+ const def = wb.export();
892
+ const inputsMap = React.useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
893
+ const outputsMap = React.useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
894
+ // Initialize nodes as invalidated by default until first successful run
895
+ React.useEffect(() => {
896
+ setNodeStatus((prev) => {
897
+ const next = { ...prev };
898
+ for (const n of def.nodes) {
899
+ const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
900
+ if (cur.invalidated === undefined) {
901
+ next[n.nodeId] = { ...cur, invalidated: true };
902
+ }
903
+ }
904
+ return next;
905
+ });
906
+ }, [def]);
907
+ // Auto layout (simple layered layout)
908
+ const runAutoLayout = React.useCallback(() => {
909
+ const cur = wb.export();
910
+ const indegree = {};
911
+ const adj = {};
912
+ for (const n of cur.nodes) {
913
+ indegree[n.nodeId] = 0;
914
+ adj[n.nodeId] = [];
915
+ }
916
+ for (const e of cur.edges) {
917
+ indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
918
+ adj[e.source.nodeId].push(e.target.nodeId);
919
+ }
920
+ const q = Object.keys(indegree).filter((k) => indegree[k] === 0);
921
+ const layers = [];
922
+ while (q.length) {
923
+ const layer = [];
924
+ const next = [];
925
+ for (const id of q) {
926
+ layer.push(id);
927
+ for (const nb of adj[id]) {
928
+ indegree[nb] -= 1;
929
+ if (indegree[nb] === 0)
930
+ next.push(nb);
931
+ }
932
+ }
933
+ layers.push(layer);
934
+ q.splice(0, q.length, ...next);
935
+ }
936
+ const X = 360;
937
+ const Y = 180;
938
+ const pos = {};
939
+ layers.forEach((layer, layerIndex) => {
940
+ layer.forEach((id, itemIndex) => {
941
+ pos[id] = { x: layerIndex * X, y: itemIndex * Y };
942
+ });
943
+ });
944
+ wb.setPositions(pos);
945
+ }, [wb]);
946
+ // Subscribe to runner/workbench events
947
+ React.useEffect(() => {
948
+ const add = (source, type) => (payload) => setEvents((prev) => {
949
+ if (source === "workbench" &&
950
+ (type === "graphChanged" || type === "graphUiChanged")) {
951
+ const changeType = payload?.change?.type;
952
+ if (changeType === "moveNode" || changeType === "moveNodes")
953
+ return prev;
954
+ }
955
+ const next = [
956
+ { at: Date.now(), source, type, payload: structuredClone(payload) },
957
+ ...prev,
958
+ ];
959
+ return next.length > 200 ? next.slice(0, 200) : next;
960
+ });
961
+ const off1 = runner.on("value", (e) => {
962
+ if (e?.io === "input") {
963
+ const nodeId = e?.nodeId;
964
+ setNodeStatus((s) => ({
965
+ ...s,
966
+ [nodeId]: { ...(s[nodeId] ?? {}), invalidated: true },
967
+ }));
968
+ }
969
+ return add("runner", "value")(e);
970
+ });
971
+ const off2 = runner.on("error", (e) => {
972
+ const edgeError = e;
973
+ const nodeError = e;
974
+ if (edgeError.kind === "edge-convert") {
975
+ const edgeId = edgeError.edgeId;
976
+ setEdgeStatus((s) => ({
977
+ ...s,
978
+ [edgeId]: { ...(s[edgeId] ?? {}), lastError: edgeError.err },
979
+ }));
980
+ }
981
+ else if (nodeError.nodeId) {
982
+ const nodeId = nodeError.nodeId;
983
+ setNodeStatus((s) => ({
984
+ ...s,
985
+ [nodeId]: {
986
+ ...(s[nodeId] ?? {}),
987
+ lastError: nodeError.err,
988
+ },
989
+ }));
990
+ }
991
+ return add("runner", "error")(e);
992
+ });
993
+ const off3 = runner.on("invalidate", (e) => {
994
+ if (e?.reason === "graph-updated") {
995
+ setNodeStatus((s) => {
996
+ const next = {};
997
+ for (const n of wb.export().nodes) {
998
+ next[n.nodeId] = { ...(s[n.nodeId] ?? {}), invalidated: true };
999
+ }
1000
+ return next;
1001
+ });
1002
+ }
1003
+ return add("runner", "invalidate")(e);
1004
+ });
1005
+ const off3b = runner.on("stats", (s) => {
1006
+ if (!s)
1007
+ return;
1008
+ if (s.kind === "node-start") {
1009
+ const id = s.nodeId;
1010
+ setNodeStatus((prev) => ({
1011
+ ...prev,
1012
+ [id]: {
1013
+ ...(prev[id] ?? {}),
1014
+ running: true,
1015
+ progress: 0,
1016
+ invalidated: false,
1017
+ },
1018
+ }));
1019
+ }
1020
+ else if (s.kind === "node-progress") {
1021
+ const id = s.nodeId;
1022
+ setNodeStatus((prev) => ({
1023
+ ...prev,
1024
+ [id]: {
1025
+ ...(prev[id] ?? {}),
1026
+ running: true,
1027
+ progress: Number(s.progress) || 0,
1028
+ },
1029
+ }));
1030
+ }
1031
+ else if (s.kind === "node-done") {
1032
+ const id = s.nodeId;
1033
+ setNodeStatus((prev) => ({
1034
+ ...prev,
1035
+ [id]: { ...(prev[id] ?? {}), running: false },
1036
+ }));
1037
+ }
1038
+ else if (s.kind === "edge-start") {
1039
+ const id = s.edgeId;
1040
+ setEdgeStatus((prev) => ({
1041
+ ...prev,
1042
+ [id]: { ...(prev[id] ?? {}), running: true },
1043
+ }));
1044
+ }
1045
+ else if (s.kind === "edge-done") {
1046
+ const id = s.edgeId;
1047
+ setEdgeStatus((prev) => ({
1048
+ ...prev,
1049
+ [id]: { ...(prev[id] ?? {}), running: false },
1050
+ }));
1051
+ }
1052
+ return add("runner", "stats")(s);
1053
+ });
1054
+ const off4 = wb.on("graphChanged", add("workbench", "graphChanged"));
1055
+ // Ensure newly added nodes start as invalidated until first evaluation
1056
+ const off4c = wb.on("graphChanged", (e) => {
1057
+ const change = e.change;
1058
+ if (change?.type === "addNode" && typeof change.nodeId === "string") {
1059
+ const id = change.nodeId;
1060
+ setNodeStatus((s) => ({
1061
+ ...s,
1062
+ [id]: { ...(s[id] ?? {}), invalidated: true },
1063
+ }));
1064
+ }
1065
+ });
1066
+ const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
1067
+ const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
1068
+ const off5b = wb.on("validationChanged", (r) => setValidation(r));
1069
+ const off6 = wb.on("selectionChanged", (sel) => {
1070
+ setSelectedNodeId(sel.nodes?.[0]);
1071
+ setSelectedEdgeId(sel.edges?.[0]);
1072
+ });
1073
+ const off7 = wb.on("error", add("workbench", "error"));
1074
+ wb.refreshValidation();
1075
+ return () => {
1076
+ off1();
1077
+ off2();
1078
+ off3();
1079
+ off3b();
1080
+ off4();
1081
+ off4b();
1082
+ off4c();
1083
+ off5();
1084
+ off5b();
1085
+ off6();
1086
+ off7();
1087
+ };
1088
+ }, [runner, wb]);
1089
+ // Push incremental updates into running engine without full reload
1090
+ React.useEffect(() => {
1091
+ if (runner.isRunning()) {
1092
+ try {
1093
+ runner.update(def);
1094
+ }
1095
+ catch { }
1096
+ }
1097
+ }, [runner, def, graphTick]);
1098
+ const validationByNode = React.useMemo(() => {
1099
+ const inputs = {};
1100
+ const outputs = {};
1101
+ const issues = {};
1102
+ if (!validation)
1103
+ return { inputs, outputs, issues };
1104
+ for (const is of validation.issues ?? []) {
1105
+ const d = is.data;
1106
+ const level = is.level;
1107
+ const code = String(is.code ?? "");
1108
+ const message = String(is.message ?? code);
1109
+ if (!d)
1110
+ continue;
1111
+ if (d.nodeId) {
1112
+ if (d.input) {
1113
+ const arr = inputs[d.nodeId] ?? (inputs[d.nodeId] = []);
1114
+ arr.push({ handle: String(d.input), level, message, code });
1115
+ const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
1116
+ nodeArr.push({ level, message, code });
1117
+ }
1118
+ if (d.output) {
1119
+ const arr = outputs[d.nodeId] ?? (outputs[d.nodeId] = []);
1120
+ arr.push({ handle: String(d.output), level, message, code });
1121
+ const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
1122
+ nodeArr.push({ level, message, code });
1123
+ }
1124
+ if (!d.input && !d.output) {
1125
+ const arr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
1126
+ arr.push({ level, message, code });
1127
+ }
1128
+ }
1129
+ }
1130
+ return { inputs, outputs, issues };
1131
+ }, [validation]);
1132
+ const validationGlobal = React.useMemo(() => {
1133
+ const list = [];
1134
+ if (!validation)
1135
+ return list;
1136
+ for (const is of validation.issues ?? []) {
1137
+ const d = is.data;
1138
+ const level = is.level;
1139
+ const code = String(is.code ?? "");
1140
+ const message = String(is.message ?? code);
1141
+ if (!d || (!d.nodeId && !d.edgeId)) {
1142
+ list.push({ level, code, message });
761
1143
  }
762
1144
  }
763
1145
  return list;
@@ -768,10 +1150,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
768
1150
  if (!validation)
769
1151
  return { errors, issues };
770
1152
  for (const is of validation.issues ?? []) {
771
- const d = is?.data;
772
- const level = is?.level;
773
- const code = String(is?.code ?? "");
774
- const message = String(is?.message ?? code);
1153
+ const d = is.data;
1154
+ const level = is.level;
1155
+ const code = String(is.code ?? "");
1156
+ const message = String(is.message ?? code);
775
1157
  if (d?.edgeId) {
776
1158
  if (level === "error")
777
1159
  errors[d.edgeId] = true;
@@ -784,269 +1166,73 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
784
1166
  const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
785
1167
  const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
786
1168
  const start = React.useCallback((engine) => {
787
- try {
788
- runner.launch(wb.export(), { engine });
789
- }
790
- catch { }
791
- }, [runner, wb]);
792
- const stop = React.useCallback(() => runner.dispose(), [runner]);
793
- const step = React.useCallback(() => runner.step(), [runner]);
794
- const flush = React.useCallback(() => runner.flush(), [runner]);
795
- const value = React.useMemo(() => ({
796
- wb,
797
- runner,
798
- registry,
799
- setRegistry,
800
- def,
801
- selectedNodeId,
802
- selectedEdgeId,
803
- setSelection,
804
- nodeStatus,
805
- edgeStatus,
806
- valuesTick,
807
- inputsMap,
808
- outputsMap,
809
- validationByNode,
810
- validationByEdge,
811
- validationGlobal,
812
- events,
813
- clearEvents,
814
- isRunning,
815
- engineKind,
816
- start,
817
- stop,
818
- step,
819
- flush,
820
- runAutoLayout,
821
- }), [
822
- wb,
823
- runner,
824
- registry,
825
- setRegistry,
826
- def,
827
- selectedNodeId,
828
- selectedEdgeId,
829
- setSelection,
830
- nodeStatus,
831
- edgeStatus,
832
- valuesTick,
833
- inputsMap,
834
- outputsMap,
835
- validationByNode,
836
- validationByEdge,
837
- validationGlobal,
838
- events,
839
- clearEvents,
840
- isRunning,
841
- engineKind,
842
- start,
843
- stop,
844
- step,
845
- flush,
846
- runAutoLayout,
847
- ]);
848
- return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
849
- }
850
-
851
- function toReactFlow(def, positions, registry, selectedNodeIds, selectedEdgeIds, opts) {
852
- const nodeHandleMap = {};
853
- const nodes = def.nodes.map((n) => {
854
- const desc = registry.nodes.get(n.typeId);
855
- const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
856
- const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
857
- nodeHandleMap[n.nodeId] = {
858
- inputs: new Set(inputHandles.map((h) => h.id)),
859
- outputs: new Set(outputHandles.map((h) => h.id)),
860
- };
861
- return {
862
- id: n.nodeId,
863
- data: {
864
- typeId: n.typeId,
865
- params: n.params,
866
- inputHandles,
867
- outputHandles,
868
- showValues: opts?.showValues,
869
- inputValues: opts?.inputs?.[n.nodeId],
870
- outputValues: opts?.outputs?.[n.nodeId],
871
- status: opts?.nodeStatus?.[n.nodeId],
872
- validation: {
873
- inputs: opts?.nodeValidation?.inputs?.[n.nodeId] ?? [],
874
- outputs: opts?.nodeValidation?.outputs?.[n.nodeId] ?? [],
875
- issues: opts?.nodeValidation?.issues?.[n.nodeId] ?? [],
876
- },
877
- toDisplay: opts?.toDisplay,
878
- },
879
- position: positions[n.nodeId] ?? { x: 0, y: 0 },
880
- type: opts?.resolveNodeType?.(n.typeId) ?? "spark:default",
881
- selected: selectedNodeIds ? selectedNodeIds.has(n.nodeId) : undefined,
882
- };
883
- });
884
- const edges = def.edges
885
- .filter((e) => {
886
- const src = nodeHandleMap[e.source.nodeId];
887
- const dst = nodeHandleMap[e.target.nodeId];
888
- if (!src || !dst)
889
- return false;
890
- return src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle);
891
- })
892
- .map((e) => {
893
- const st = opts?.edgeStatus?.[e.id];
894
- const isRunning = !!st?.running;
895
- const hasError = !!st?.lastError;
896
- const isInvalidEdge = !!opts?.edgeValidation?.[e.id];
897
- const style = hasError || isInvalidEdge
898
- ? { stroke: "#ef4444", strokeWidth: 2 }
899
- : isRunning
900
- ? { stroke: "#3b82f6" }
901
- : undefined;
902
- return {
903
- id: e.id,
904
- source: e.source.nodeId,
905
- target: e.target.nodeId,
906
- sourceHandle: e.source.handle,
907
- targetHandle: e.target.handle,
908
- selected: selectedEdgeIds ? selectedEdgeIds.has(e.id) : undefined,
909
- animated: isRunning,
910
- style,
911
- };
912
- });
913
- return { nodes, edges };
914
- }
915
-
916
- function IssueBadge({ level, title, size = 12, className, }) {
917
- const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
918
- return (jsxRuntime.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" ? (jsxRuntime.jsx(react.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react.WarningCircleIcon, { size: size, weight: "fill" })) }));
919
- }
920
-
921
- const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
922
- const typeId = data.typeId;
923
- const inputEntries = data.inputHandles ?? [];
924
- const outputEntries = data.outputHandles ?? [];
925
- const status = data.status ?? {};
926
- const validation = data.validation ?? {
927
- inputs: [],
928
- outputs: [],
929
- issues: [],
930
- };
931
- const HEADER_SIZE = 24;
932
- const ROW_SIZE = 22;
933
- const maxRows = Math.max(inputEntries.length, outputEntries.length);
934
- const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
935
- const minWidth = data.showValues ? 320 : 160;
936
- const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
937
- const hasError = !!status.lastError;
938
- const isRunning = !!status.running;
939
- const isInvalid = !!status.invalidated && !isRunning && !hasError;
940
- const borderClasses = selected
941
- ? "border-2 border-gray-900 dark:border-gray-100"
942
- : hasError
943
- ? "border-2 border-red-500"
944
- : isRunning
945
- ? "border-2 border-blue-500 ring-2 ring-blue-200 dark:ring-blue-900"
946
- : isInvalid
947
- ? "border-2 border-amber-500 border-dashed"
948
- : "border border-gray-500 dark:border-gray-400";
949
- const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
950
- return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900 border-solid", borderClasses), style: { position: "relative", minHeight: minHeight, minWidth }, children: [jsxRuntime.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: [jsxRuntime.jsx("strong", { className: "flex-1 h-full leading-6 text-xs", children: typeId }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsxRuntime.jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsxRuntime.jsx(react.XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
951
- ? "error"
952
- : "warning", size: 12, className: "w-3 h-3", title: validation.issues
953
- .map((v) => `${v.code}: ${v.message}`)
954
- .join("; ") })), jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }), (isRunning || pct > 0) && (jsxRuntime.jsx("div", { className: "h-1 bg-blue-200 dark:bg-blue-900", children: jsxRuntime.jsx("div", { className: "h-1 bg-blue-500 transition-all", style: { width: `${pct}%` } }) })), inputEntries.map((entry, i) => {
955
- const vIssues = validation.inputs.filter((v) => v.handle === entry.id);
956
- const hasAny = vIssues.length > 0;
957
- const hasErr = vIssues.some((v) => v.level === "error");
958
- const title = vIssues
959
- .map((v) => `${v.code}: ${v.message}`)
960
- .join("; ");
961
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.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) } }), jsxRuntime.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 && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), data.showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: data.toDisplay
962
- ? data.toDisplay(entry.typeId, data.inputValues?.[entry.id])
963
- : String(data.inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
964
- }), outputEntries.map((entry, i) => {
965
- const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
966
- const hasAny = vIssues.length > 0;
967
- const hasErr = vIssues.some((v) => v.level === "error");
968
- const title = vIssues
969
- .map((v) => `${v.code}: ${v.message}`)
970
- .join("; ");
971
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.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) } }), jsxRuntime.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 && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), data.showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: data.toDisplay
972
- ? data.toDisplay(entry.typeId, data.outputValues?.[entry.id])
973
- : String(data.outputValues?.[entry.id]) }))] })] }, `out-${entry.id}`));
974
- })] }));
975
- });
976
- DefaultNode.displayName = "DefaultNode";
977
-
978
- function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
979
- const { registry } = useWorkbenchContext();
980
- const rf = ReactFlow.useReactFlow();
981
- if (!open || !clientPos)
982
- return null;
983
- const items = Array.from(registry.nodes.keys());
984
- const handleClick = (typeId) => {
985
- const p = rf.project({ x: clientPos.x, y: clientPos.y });
986
- onAdd(typeId, p);
987
- onClose();
988
- };
989
- return (jsxRuntime.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: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Add Node" }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: items.map((id) => (jsxRuntime.jsx("button", { onClick: () => handleClick(id), className: "block w-full text-left px-2 py-1 hover:bg-gray-100 cursor-pointer", children: id }, id))) })] }));
990
- }
991
-
992
- function WorkbenchCanvas({ showValues, toDisplay, }) {
993
- const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
994
- const ioValues = { inputs: inputsMap, outputs: outputsMap };
995
- const nodeValidation = validationByNode;
996
- const edgeValidation = validationByEdge.errors;
997
- const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
998
- const { nodeTypes, resolveNodeType } = React.useMemo(() => {
999
- // Build nodeTypes map using UI extension registry
1000
- const ui = wb.getUI();
1001
- const custom = new Map();
1002
- for (const typeId of Array.from(registry.nodes.keys())) {
1003
- const renderer = ui.getNodeRenderer(typeId);
1004
- if (renderer)
1005
- custom.set(typeId, renderer);
1006
- }
1007
- const types = { "spark:default": DefaultNode };
1008
- for (const [typeId, comp] of custom.entries()) {
1009
- types[`spark:${typeId}`] = comp;
1169
+ try {
1170
+ runner.launch(wb.export(), { engine });
1010
1171
  }
1011
- const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark:${nodeTypeId}` : "spark:default";
1012
- return { nodeTypes: types, resolveNodeType: resolver };
1013
- // registry is stable; ui renderers expected to be set up before mount
1014
- }, [wb, registry]);
1015
- const { nodes, edges } = React.useMemo(() => {
1016
- const def = wb.export();
1017
- const sel = wb.getSelection();
1018
- return toReactFlow(def, wb.getPositions(), registry, new Set(sel.nodes), new Set(sel.edges), {
1019
- showValues,
1020
- inputs: ioValues.inputs,
1021
- outputs: ioValues.outputs,
1022
- resolveNodeType,
1023
- toDisplay,
1024
- nodeStatus,
1025
- edgeStatus,
1026
- nodeValidation,
1027
- edgeValidation,
1028
- });
1029
- }, [
1030
- showValues,
1031
- ioValues,
1172
+ catch { }
1173
+ }, [runner, wb]);
1174
+ const stop = React.useCallback(() => runner.dispose(), [runner]);
1175
+ const step = React.useCallback(() => runner.step(), [runner]);
1176
+ const flush = React.useCallback(() => runner.flush(), [runner]);
1177
+ const value = React.useMemo(() => ({
1178
+ wb,
1179
+ runner,
1180
+ registry,
1181
+ setRegistry,
1182
+ def,
1183
+ selectedNodeId,
1184
+ selectedEdgeId,
1185
+ setSelection,
1186
+ nodeStatus,
1187
+ edgeStatus,
1032
1188
  valuesTick,
1033
- toDisplay,
1189
+ inputsMap,
1190
+ outputsMap,
1191
+ validationByNode,
1192
+ validationByEdge,
1193
+ validationGlobal,
1194
+ events,
1195
+ clearEvents,
1196
+ isRunning,
1197
+ engineKind,
1198
+ start,
1199
+ stop,
1200
+ step,
1201
+ flush,
1202
+ runAutoLayout,
1203
+ }), [
1204
+ wb,
1205
+ runner,
1206
+ registry,
1207
+ setRegistry,
1208
+ def,
1209
+ selectedNodeId,
1210
+ selectedEdgeId,
1211
+ setSelection,
1034
1212
  nodeStatus,
1035
1213
  edgeStatus,
1036
- nodeValidation,
1037
- edgeValidation,
1214
+ valuesTick,
1215
+ inputsMap,
1216
+ outputsMap,
1217
+ validationByNode,
1218
+ validationByEdge,
1219
+ validationGlobal,
1220
+ events,
1221
+ clearEvents,
1222
+ isRunning,
1223
+ engineKind,
1224
+ start,
1225
+ stop,
1226
+ step,
1227
+ flush,
1228
+ runAutoLayout,
1038
1229
  ]);
1039
- const [menuOpen, setMenuOpen] = React.useState(false);
1040
- const [menuPos, setMenuPos] = React.useState(null);
1041
- const onContextMenu = (e) => {
1042
- e.preventDefault();
1043
- setMenuPos({ x: e.clientX, y: e.clientY });
1044
- setMenuOpen(true);
1045
- };
1046
- const addNodeAt = (typeId, pos) => {
1047
- wb.addNode({ typeId, position: pos });
1048
- };
1049
- return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.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: [jsxRuntime.jsx(ReactFlow.Background, {}), jsxRuntime.jsx(ReactFlow.MiniMap, {}), jsxRuntime.jsx(ReactFlow.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) })] }) }));
1230
+ return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
1231
+ }
1232
+
1233
+ function IssueBadge({ level, title, size = 12, className, }) {
1234
+ const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
1235
+ return (jsxRuntime.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" ? (jsxRuntime.jsx(react.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react.WarningCircleIcon, { size: size, weight: "fill" })) }));
1050
1236
  }
1051
1237
 
1052
1238
  function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWorkbenchChange, }) {
@@ -1055,439 +1241,450 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
1055
1241
  const rows = React.useMemo(() => {
1056
1242
  const filtered = hideWorkbench
1057
1243
  ? events.filter((e) => e.source !== "workbench")
1058
- : events;
1059
- return filtered.slice().reverse();
1060
- }, [events, hideWorkbench]);
1061
- React.useEffect(() => {
1062
- if (!autoScroll)
1063
- return;
1064
- const el = scrollRef.current;
1065
- if (!el)
1066
- return;
1067
- el.scrollTop = el.scrollHeight;
1068
- }, [rows, autoScroll]);
1069
- const renderPayload = (v) => {
1070
- try {
1071
- return JSON.stringify(v, null, 0);
1072
- }
1073
- catch {
1074
- return String(v);
1075
- }
1076
- };
1077
- return (jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Events" }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Hide workbench" })] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Auto scroll" })] }), jsxRuntime.jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsxRuntime.jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev, idx) => (jsxRuntime.jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [jsxRuntime.jsx("span", { className: "w-8 shrink-0 text-right text-gray-500 select-none", children: idx + 1 }), jsxRuntime.jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsxRuntime.jsx("pre", { className: "m-0 whitespace-pre-wrap ml-10", children: renderPayload(ev.payload) })] }, `${ev.at}:${idx}`))) })] }));
1078
- }
1079
-
1080
- function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toDisplay, setInput, }) {
1081
- const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
1082
- const nodeValidationIssues = validationByNode.issues;
1083
- const edgeValidationIssues = validationByEdge.issues;
1084
- const nodeValidationHandles = validationByNode;
1085
- const globalValidationIssues = validationGlobal;
1086
- const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
1087
- const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
1088
- const selectedDesc = selectedNode
1089
- ? registry.nodes.get(selectedNode.typeId)
1090
- : undefined;
1091
- const inputHandles = Object.keys(selectedDesc?.inputs ?? {});
1092
- const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
1093
- const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
1094
- const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
1095
- const selectedNodeStatus = selectedNodeId
1096
- ? nodeStatus?.[selectedNodeId]
1097
- : undefined;
1098
- const selectedNodeValidation = selectedNodeId
1099
- ? nodeValidationIssues?.[selectedNodeId] ?? []
1100
- : [];
1101
- const selectedEdgeValidation = selectedEdge
1102
- ? edgeValidationIssues?.[selectedEdge.id] ?? []
1103
- : [];
1104
- const selectedNodeHandleValidation = selectedNodeId
1105
- ? {
1106
- inputs: nodeValidationHandles?.inputs?.[selectedNodeId] ?? [],
1107
- outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
1108
- }
1109
- : { inputs: [], outputs: [] };
1110
- // Local drafts and originals for commit-on-blur/enter behavior
1111
- const [drafts, setDrafts] = React.useState({});
1112
- const [originals, setOriginals] = React.useState({});
1113
- // Initialize drafts from current inputs whenever selection or valuesTick change,
1114
- // but do not clobber fields currently being edited (dirty drafts)
1115
- React.useEffect(() => {
1116
- const shallowEqual = (a, b) => {
1117
- const ak = Object.keys(a);
1118
- const bk = Object.keys(b);
1119
- if (ak.length !== bk.length)
1120
- return false;
1121
- for (const k of ak)
1122
- if (a[k] !== b[k])
1123
- return false;
1124
- return true;
1125
- };
1126
- if (!selectedNodeId) {
1127
- if (Object.keys(drafts).length || Object.keys(originals).length) {
1128
- setDrafts({});
1129
- setOriginals({});
1130
- }
1131
- return;
1132
- }
1133
- const desc = selectedDesc;
1134
- const handles = Object.keys(desc?.inputs ?? {});
1135
- const nextDrafts = { ...drafts };
1136
- const nextOriginals = { ...originals };
1137
- for (const h of handles) {
1138
- const typeId = desc?.inputs?.[h];
1139
- const current = nodeInputs[h];
1140
- const display = toDisplay(typeId, current);
1141
- const wasOriginal = originals[h];
1142
- const isDirty = drafts[h] !== undefined &&
1143
- wasOriginal !== undefined &&
1144
- drafts[h] !== wasOriginal;
1145
- if (!isDirty) {
1146
- nextDrafts[h] = display;
1147
- nextOriginals[h] = display;
1148
- }
1149
- }
1150
- if (!shallowEqual(drafts, nextDrafts))
1151
- setDrafts(nextDrafts);
1152
- if (!shallowEqual(originals, nextOriginals))
1153
- setOriginals(nextOriginals);
1154
- }, [selectedNodeId, selectedDesc, valuesTick]);
1155
- const widthClass = debug ? "w-[480px]" : "w-[320px]";
1156
- return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.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 ??
1157
- selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
1158
- const typeId = (selectedDesc?.inputs ?? {})[h];
1159
- const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
1160
- e.target.handle === h);
1161
- const commonProps = {
1162
- style: { flex: 1 },
1163
- disabled: isLinked,
1164
- };
1165
- const current = nodeInputs[h];
1166
- const value = drafts[h] ?? toDisplay(typeId, current);
1167
- const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
1168
- const commit = () => {
1169
- const draft = drafts[h];
1170
- if (draft === undefined)
1171
- return;
1172
- setInput(h, draft);
1173
- setOriginals((o) => ({ ...o, [h]: draft }));
1174
- };
1175
- const revert = () => {
1176
- const orig = originals[h] ?? toDisplay(typeId, current);
1177
- setDrafts((d) => ({ ...d, [h]: orig }));
1178
- };
1179
- const isEnum = typeId?.includes("enum:");
1180
- const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
1181
- const hasValidation = inIssues.length > 0;
1182
- const hasErr = inIssues.some((m) => m.level === "error");
1183
- const title = inIssues
1184
- .map((v) => `${v.code}: ${v.message}`)
1185
- .join("; ");
1186
- return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-28", children: [h, jsxRuntime.jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.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) => {
1187
- const label = String(e.target.value);
1188
- const byLabel = registry.enums
1189
- .get(typeId)
1190
- ?.labelToValue.get(label.toLowerCase());
1191
- let raw = (byLabel !== undefined ? byLabel : Number(label));
1192
- if (!Number.isFinite(raw))
1193
- raw = undefined;
1194
- setInput(h, raw);
1195
- const display = toDisplay(typeId, raw);
1196
- setDrafts((d) => ({ ...d, [h]: display }));
1197
- setOriginals((o) => ({ ...o, [h]: display }));
1198
- }, ...commonProps, children: [jsxRuntime.jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsxRuntime.jsx("option", { value: opt.label, children: opt.label }, opt.value)))] })) : (jsxRuntime.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) => {
1199
- if (e.key === "Enter")
1200
- commit();
1201
- if (e.key === "Escape")
1202
- revert();
1203
- }, ...commonProps }))] }, h));
1204
- }))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsx("label", { className: "w-20", children: h }), jsxRuntime.jsx("div", { className: "flex-1", children: toDisplay(selectedDesc?.outputs?.[h], nodeOutputs[h]) }), (() => {
1205
- const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
1206
- if (outIssues.length === 0)
1207
- return null;
1208
- const outErr = outIssues.some((m) => m.level === "error");
1209
- const outTitle = outIssues
1210
- .map((v) => `${v.code}: ${v.message}`)
1211
- .join("; ");
1212
- return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
1213
- })()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) }), debug && (jsxRuntime.jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsxRuntime.jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
1214
- }
1215
-
1216
- class GraphRunner {
1217
- constructor(registry, backend) {
1218
- this.registry = registry;
1219
- this.listeners = new Map();
1220
- this.stagedInputs = {};
1221
- this.backend = { kind: "local" };
1222
- if (backend)
1223
- this.backend = backend;
1224
- }
1225
- build(def) {
1226
- if (this.backend.kind === "local") {
1227
- const builder = new sparkGraph.GraphBuilder(this.registry);
1228
- this.runtime = builder.build(def);
1229
- return;
1230
- }
1231
- // Remote: no-op here; build is performed on remote server during launch
1232
- }
1233
- update(def) {
1234
- if (this.backend.kind === "local") {
1235
- if (!this.runtime)
1236
- return;
1237
- this.runtime.update(def, this.registry);
1238
- this.emit("invalidate", { reason: "graph-updated" });
1244
+ : events;
1245
+ return filtered.slice().reverse();
1246
+ }, [events, hideWorkbench]);
1247
+ React.useEffect(() => {
1248
+ if (!autoScroll)
1249
+ return;
1250
+ const el = scrollRef.current;
1251
+ if (!el)
1239
1252
  return;
1253
+ el.scrollTop = el.scrollHeight;
1254
+ }, [rows, autoScroll]);
1255
+ const renderPayload = (v) => {
1256
+ try {
1257
+ return JSON.stringify(v, null, 0);
1240
1258
  }
1241
- // Remote: forward update; ignore errors (fire-and-forget)
1242
- void this.ensureRemote().then(async (rc) => {
1243
- try {
1244
- await rc.runner.update(def);
1245
- this.emit("invalidate", { reason: "graph-updated" });
1246
- }
1247
- catch { }
1248
- });
1249
- }
1250
- launch(def, opts) {
1251
- if (this.engine) {
1252
- throw new Error("Engine already running. Stop the current engine first.");
1259
+ catch {
1260
+ return String(v);
1253
1261
  }
1254
- if (this.backend.kind === "local") {
1255
- this.build(def);
1256
- if (!this.runtime)
1257
- throw new Error("Runtime not built");
1258
- const rt = this.runtime;
1259
- switch (opts.engine) {
1260
- case "push":
1261
- this.engine = new sparkGraph.PushEngine(rt);
1262
- break;
1263
- case "batched":
1264
- this.engine = new sparkGraph.BatchedEngine(rt, {
1265
- flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
1266
- });
1267
- break;
1268
- case "pull":
1269
- this.engine = new sparkGraph.PullEngine(rt);
1270
- break;
1271
- case "hybrid":
1272
- this.engine = new sparkGraph.HybridEngine(rt, {
1273
- windowMs: opts.hybrid?.windowMs ?? 250,
1274
- batchThreshold: opts.hybrid?.batchThreshold ?? 3,
1275
- });
1276
- break;
1277
- case "step":
1278
- this.engine = new sparkGraph.StepEngine(rt);
1279
- break;
1280
- default:
1281
- throw new Error("Unknown engine kind");
1282
- }
1283
- this.engine.on("value", (e) => this.emit("value", e));
1284
- this.engine.on("error", (e) => this.emit("error", e));
1285
- this.engine.on("invalidate", (e) => this.emit("invalidate", e));
1286
- this.engine.on("stats", (e) => this.emit("stats", e));
1287
- this.engine.launch();
1288
- this.runningKind = opts.engine;
1289
- this.emit("status", { running: true, engine: this.runningKind });
1290
- for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
1291
- for (const [handle, value] of Object.entries(map)) {
1292
- this.engine.setInput(nodeId, handle, value);
1293
- }
1294
- }
1295
- return;
1262
+ };
1263
+ return (jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Events" }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Hide workbench" })] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Auto scroll" })] }), jsxRuntime.jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsxRuntime.jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev, idx) => (jsxRuntime.jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [jsxRuntime.jsx("span", { className: "w-8 shrink-0 text-right text-gray-500 select-none", children: idx + 1 }), jsxRuntime.jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsxRuntime.jsx("pre", { className: "m-0 whitespace-pre-wrap ml-10", children: renderPayload(ev.payload) })] }, `${ev.at}:${idx}`))) })] }));
1264
+ }
1265
+
1266
+ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, setInput, }) {
1267
+ const safeToString = (typeId, value) => {
1268
+ try {
1269
+ return typeof toString === "function"
1270
+ ? toString(typeId, value)
1271
+ : String(value ?? "");
1296
1272
  }
1297
- // Remote: build remotely then launch
1298
- void this.ensureRemote().then(async (rc) => {
1299
- await rc.runner.build(def);
1300
- const eng = rc.runner.getEngine();
1301
- if (!rc.listenersBound) {
1302
- eng.on("value", (e) => {
1303
- rc.valueCache.set(`${e.nodeId}.${e.handle}`, {
1304
- io: e.io,
1305
- value: e.value,
1306
- });
1307
- this.emit("value", e);
1308
- });
1309
- eng.on("error", (e) => this.emit("error", e));
1310
- eng.on("invalidate", (e) => this.emit("invalidate", e));
1311
- eng.on("stats", (e) => this.emit("stats", e));
1312
- rc.listenersBound = true;
1313
- }
1314
- this.engine = eng;
1315
- this.engine.launch();
1316
- this.runningKind = "push";
1317
- this.emit("status", { running: true, engine: this.runningKind });
1318
- for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
1319
- for (const [handle, value] of Object.entries(map)) {
1320
- this.engine.setInput(nodeId, handle, value);
1321
- }
1322
- }
1323
- });
1324
- }
1325
- setInput(nodeId, handle, value) {
1326
- if (!this.stagedInputs[nodeId])
1327
- this.stagedInputs[nodeId] = {};
1328
- this.stagedInputs[nodeId][handle] = value;
1329
- if (this.engine)
1330
- this.engine.setInput(nodeId, handle, value);
1331
- }
1332
- async step() {
1333
- if (this.backend.kind !== "local")
1334
- return; // unsupported remotely
1335
- const eng = this.engine;
1336
- if (eng instanceof sparkGraph.StepEngine)
1337
- await eng.step();
1338
- }
1339
- async computeNode(nodeId) {
1340
- if (this.backend.kind !== "local")
1341
- return; // unsupported remotely
1342
- const eng = this.engine;
1343
- if (eng instanceof sparkGraph.PullEngine)
1344
- await eng.computeNode(nodeId);
1345
- }
1346
- flush() {
1347
- if (this.backend.kind !== "local")
1348
- return; // unsupported remotely
1349
- const eng = this.engine;
1350
- if (eng instanceof sparkGraph.BatchedEngine)
1351
- eng.flush();
1352
- }
1353
- getOutputs(def) {
1354
- const out = {};
1355
- if (this.backend.kind === "local") {
1356
- if (!this.runtime)
1357
- return out;
1358
- for (const n of def.nodes) {
1359
- const desc = this.registry.nodes.get(n.typeId);
1360
- const handles = Object.keys(desc?.outputs ?? {});
1361
- for (const h of handles) {
1362
- const v = this.runtime.getOutput(n.nodeId, h);
1363
- if (v !== undefined) {
1364
- if (!out[n.nodeId])
1365
- out[n.nodeId] = {};
1366
- out[n.nodeId][h] = v;
1367
- }
1368
- }
1369
- }
1370
- return out;
1273
+ catch {
1274
+ return String(value ?? "");
1371
1275
  }
1372
- const cache = this.remote?.valueCache;
1373
- if (!cache)
1374
- return out;
1375
- for (const n of def.nodes) {
1376
- const desc = this.registry.nodes.get(n.typeId);
1377
- const handles = Object.keys(desc?.outputs ?? {});
1378
- for (const h of handles) {
1379
- const key = `${n.nodeId}.${h}`;
1380
- const rec = cache.get(key);
1381
- if (rec && rec.io === "output") {
1382
- if (!out[n.nodeId])
1383
- out[n.nodeId] = {};
1384
- out[n.nodeId][h] = rec.value;
1385
- }
1386
- }
1276
+ };
1277
+ const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
1278
+ const nodeValidationIssues = validationByNode.issues;
1279
+ const edgeValidationIssues = validationByEdge.issues;
1280
+ const nodeValidationHandles = validationByNode;
1281
+ const globalValidationIssues = validationGlobal;
1282
+ const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
1283
+ const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
1284
+ const selectedDesc = selectedNode
1285
+ ? registry.nodes.get(selectedNode.typeId)
1286
+ : undefined;
1287
+ const inputHandles = Object.keys(selectedDesc?.inputs ?? {});
1288
+ const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
1289
+ const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
1290
+ const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
1291
+ const selectedNodeStatus = selectedNodeId
1292
+ ? nodeStatus?.[selectedNodeId]
1293
+ : undefined;
1294
+ const selectedNodeValidation = selectedNodeId
1295
+ ? nodeValidationIssues?.[selectedNodeId] ?? []
1296
+ : [];
1297
+ const selectedEdgeValidation = selectedEdge
1298
+ ? edgeValidationIssues?.[selectedEdge.id] ?? []
1299
+ : [];
1300
+ const selectedNodeHandleValidation = selectedNodeId
1301
+ ? {
1302
+ inputs: nodeValidationHandles?.inputs?.[selectedNodeId] ?? [],
1303
+ outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
1387
1304
  }
1388
- return out;
1389
- }
1390
- getInputs(def) {
1391
- const out = {};
1392
- if (this.backend.kind === "local") {
1393
- for (const n of def.nodes) {
1394
- const staged = this.stagedInputs[n.nodeId] ?? {};
1395
- const runtimeInputs = this.runtime
1396
- ? this.runtime.__unsafe_getNodeData?.(n.nodeId)?.inputs ?? {}
1397
- : {};
1398
- if (this.isRunning()) {
1399
- out[n.nodeId] = runtimeInputs;
1400
- }
1401
- else {
1402
- const merged = { ...runtimeInputs, ...staged };
1403
- if (Object.keys(merged).length > 0)
1404
- out[n.nodeId] = merged;
1405
- }
1305
+ : { inputs: [], outputs: [] };
1306
+ // Local drafts and originals for commit-on-blur/enter behavior
1307
+ const [drafts, setDrafts] = React.useState({});
1308
+ const [originals, setOriginals] = React.useState({});
1309
+ // Initialize drafts from current inputs whenever selection or valuesTick change,
1310
+ // but do not clobber fields currently being edited (dirty drafts)
1311
+ React.useEffect(() => {
1312
+ const shallowEqual = (a, b) => {
1313
+ const ak = Object.keys(a);
1314
+ const bk = Object.keys(b);
1315
+ if (ak.length !== bk.length)
1316
+ return false;
1317
+ for (const k of ak)
1318
+ if (a[k] !== b[k])
1319
+ return false;
1320
+ return true;
1321
+ };
1322
+ if (!selectedNodeId) {
1323
+ if (Object.keys(drafts).length || Object.keys(originals).length) {
1324
+ setDrafts({});
1325
+ setOriginals({});
1406
1326
  }
1407
- return out;
1327
+ return;
1408
1328
  }
1409
- const cache = this.remote?.valueCache;
1410
- for (const n of def.nodes) {
1411
- const staged = this.stagedInputs[n.nodeId] ?? {};
1412
- const desc = this.registry.nodes.get(n.typeId);
1413
- const handles = Object.keys(desc?.inputs ?? {});
1414
- const cur = {};
1415
- for (const h of handles) {
1416
- const rec = cache?.get(`${n.nodeId}.${h}`);
1417
- if (rec && rec.io === "input")
1418
- cur[h] = rec.value;
1329
+ const desc = selectedDesc;
1330
+ const handles = Object.keys(desc?.inputs ?? {});
1331
+ const nextDrafts = { ...drafts };
1332
+ const nextOriginals = { ...originals };
1333
+ for (const h of handles) {
1334
+ const typeId = desc?.inputs?.[h];
1335
+ const current = nodeInputs[h];
1336
+ const display = safeToString(typeId, current);
1337
+ const wasOriginal = originals[h];
1338
+ const isDirty = drafts[h] !== undefined &&
1339
+ wasOriginal !== undefined &&
1340
+ drafts[h] !== wasOriginal;
1341
+ if (!isDirty) {
1342
+ nextDrafts[h] = display;
1343
+ nextOriginals[h] = display;
1419
1344
  }
1420
- const merged = this.isRunning() ? cur : { ...cur, ...staged };
1421
- if (Object.keys(merged).length > 0)
1422
- out[n.nodeId] = merged;
1423
1345
  }
1424
- return out;
1425
- }
1426
- async whenIdle() {
1427
- await this.engine?.whenIdle();
1428
- }
1429
- on(event, handler) {
1430
- if (!this.listeners.has(event))
1431
- this.listeners.set(event, new Set());
1432
- const set = this.listeners.get(event);
1433
- set.add(handler);
1434
- return () => set.delete(handler);
1435
- }
1436
- emit(event, payload) {
1437
- const set = this.listeners.get(event);
1438
- if (set)
1439
- for (const h of Array.from(set))
1440
- h(payload);
1441
- }
1442
- dispose() {
1443
- this.engine?.dispose();
1444
- this.engine = undefined;
1445
- this.runtime?.dispose();
1446
- this.runtime = undefined;
1447
- this.remote = undefined;
1448
- if (this.runningKind) {
1449
- this.runningKind = undefined;
1450
- this.emit("status", { running: false, engine: undefined });
1346
+ if (!shallowEqual(drafts, nextDrafts))
1347
+ setDrafts(nextDrafts);
1348
+ if (!shallowEqual(originals, nextOriginals))
1349
+ setOriginals(nextOriginals);
1350
+ }, [selectedNodeId, selectedDesc, valuesTick]);
1351
+ const widthClass = debug ? "w-[480px]" : "w-[320px]";
1352
+ return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.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 ??
1353
+ selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
1354
+ const typeId = (selectedDesc?.inputs ?? {})[h];
1355
+ const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
1356
+ e.target.handle === h);
1357
+ const commonProps = {
1358
+ style: { flex: 1 },
1359
+ disabled: isLinked,
1360
+ };
1361
+ const current = nodeInputs[h];
1362
+ const value = drafts[h] ?? safeToString(typeId, current);
1363
+ const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
1364
+ const commit = () => {
1365
+ const draft = drafts[h];
1366
+ if (draft === undefined)
1367
+ return;
1368
+ setInput(h, draft);
1369
+ setOriginals((o) => ({ ...o, [h]: draft }));
1370
+ };
1371
+ const revert = () => {
1372
+ const orig = originals[h] ?? safeToString(typeId, current);
1373
+ setDrafts((d) => ({ ...d, [h]: orig }));
1374
+ };
1375
+ const isEnum = typeId?.includes("enum:");
1376
+ const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
1377
+ const hasValidation = inIssues.length > 0;
1378
+ const hasErr = inIssues.some((m) => m.level === "error");
1379
+ const title = inIssues
1380
+ .map((v) => `${v.code}: ${v.message}`)
1381
+ .join("; ");
1382
+ return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-28", children: [h, jsxRuntime.jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.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: current !== undefined && current !== null
1383
+ ? String(current)
1384
+ : "", onChange: (e) => {
1385
+ const val = e.target.value;
1386
+ const raw = val === "" ? undefined : Number(val);
1387
+ setInput(h, raw);
1388
+ // keep drafts/originals in sync with label for display elsewhere
1389
+ const display = safeToString(typeId, raw);
1390
+ setDrafts((d) => ({ ...d, [h]: display }));
1391
+ setOriginals((o) => ({ ...o, [h]: display }));
1392
+ }, ...commonProps, children: [jsxRuntime.jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] })) : isLinked ? (toElement(typeId, current)) : (jsxRuntime.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) => {
1393
+ if (e.key === "Enter")
1394
+ commit();
1395
+ if (e.key === "Escape")
1396
+ revert();
1397
+ }, ...commonProps }))] }, h));
1398
+ }))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsx("label", { className: "w-20", children: h }), jsxRuntime.jsx("div", { className: "flex-1", children: toElement(selectedDesc?.outputs?.[h], nodeOutputs[h]) }), (() => {
1399
+ const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
1400
+ if (outIssues.length === 0)
1401
+ return null;
1402
+ const outErr = outIssues.some((m) => m.level === "error");
1403
+ const outTitle = outIssues
1404
+ .map((v) => `${v.code}: ${v.message}`)
1405
+ .join("; ");
1406
+ return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
1407
+ })()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) }), debug && (jsxRuntime.jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsxRuntime.jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
1408
+ }
1409
+
1410
+ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
1411
+ const { typeId, showValues, inputValues, outputValues, toString } = data;
1412
+ const inputEntries = data.inputHandles ?? [];
1413
+ const outputEntries = data.outputHandles ?? [];
1414
+ const status = data.status ?? {};
1415
+ const validation = data.validation ?? {
1416
+ inputs: [],
1417
+ outputs: [],
1418
+ issues: [],
1419
+ };
1420
+ const HEADER_SIZE = 24;
1421
+ const ROW_SIZE = 22;
1422
+ const maxRows = Math.max(inputEntries.length, outputEntries.length);
1423
+ const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
1424
+ const minWidth = data.showValues ? 320 : 160;
1425
+ const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
1426
+ const hasError = !!status.lastError;
1427
+ const hasValidationError = validation.issues.some((i) => i.level === "error");
1428
+ const hasValidationWarning = !hasValidationError && validation.issues.length > 0;
1429
+ const isRunning = !!status.running;
1430
+ const isInvalid = !!status.invalidated && !isRunning && !hasError;
1431
+ // Border color encodes severity; thickness encodes selection; style (dashed) encodes invalidated
1432
+ const borderWidth = selected ? "border-2" : "border";
1433
+ const borderStyle = isInvalid ? "border-dashed" : "border-solid";
1434
+ const borderColor = hasError || hasValidationError
1435
+ ? "border-red-500"
1436
+ : hasValidationWarning
1437
+ ? "border-amber-500"
1438
+ : isRunning
1439
+ ? "border-blue-500"
1440
+ : "border-gray-500 dark:border-gray-400";
1441
+ const ringClasses = isRunning
1442
+ ? "ring-2 ring-blue-200 dark:ring-blue-900"
1443
+ : undefined;
1444
+ const borderClasses = cx(borderWidth, borderStyle, borderColor, ringClasses);
1445
+ const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
1446
+ return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900", borderClasses), style: { position: "relative", minHeight: minHeight, minWidth }, children: [jsxRuntime.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: [jsxRuntime.jsx("strong", { className: "flex-1 h-full leading-6 text-xs", children: typeId }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsxRuntime.jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsxRuntime.jsx(react.XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
1447
+ ? "error"
1448
+ : "warning", size: 12, className: "w-3 h-3", title: validation.issues
1449
+ .map((v) => `${v.code}: ${v.message}`)
1450
+ .join("; ") })), jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }), (isRunning || pct > 0) && (jsxRuntime.jsx("div", { className: "h-1 bg-blue-200 dark:bg-blue-900", children: jsxRuntime.jsx("div", { className: "h-1 bg-blue-500 transition-all", style: { width: `${pct}%` } }) })), inputEntries.map((entry, i) => {
1451
+ const vIssues = validation.inputs.filter((v) => v.handle === entry.id);
1452
+ const hasAny = vIssues.length > 0;
1453
+ const hasErr = vIssues.some((v) => v.level === "error");
1454
+ const title = vIssues
1455
+ .map((v) => `${v.code}: ${v.message}`)
1456
+ .join("; ");
1457
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.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) } }), jsxRuntime.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 && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
1458
+ }), outputEntries.map((entry, i) => {
1459
+ const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
1460
+ const hasAny = vIssues.length > 0;
1461
+ const hasErr = vIssues.some((v) => v.level === "error");
1462
+ const title = vIssues
1463
+ .map((v) => `${v.code}: ${v.message}`)
1464
+ .join("; ");
1465
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.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) } }), jsxRuntime.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 && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, outputValues?.[entry.id]) }))] })] }, `out-${entry.id}`));
1466
+ })] }));
1467
+ });
1468
+ DefaultNode.displayName = "DefaultNode";
1469
+
1470
+ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
1471
+ const { registry } = useWorkbenchContext();
1472
+ const rf = ReactFlow.useReactFlow();
1473
+ const ids = Array.from(registry.nodes.keys());
1474
+ // Group node ids by the segment before the first '.'
1475
+ const grouped = {};
1476
+ for (const id of ids) {
1477
+ const parts = id.split(".");
1478
+ const cat = parts.length > 1 ? parts[0] : "other";
1479
+ const label = parts.length > 1 ? parts.slice(1).join(".") : id;
1480
+ (grouped[cat] = grouped[cat] || []).push({ id, label });
1481
+ }
1482
+ const cats = Object.keys(grouped).sort((a, b) => a.localeCompare(b));
1483
+ cats.forEach((c) => grouped[c].sort((a, b) => a.label.localeCompare(b.label)));
1484
+ const totalCount = ids.length;
1485
+ // Ref for focus/outside click handling
1486
+ const ref = React.useRef(null);
1487
+ // Close on outside click and on ESC
1488
+ React.useEffect(() => {
1489
+ if (!open)
1490
+ return;
1491
+ const onDown = (e) => {
1492
+ if (!ref.current)
1493
+ return;
1494
+ if (!ref.current.contains(e.target))
1495
+ onClose();
1496
+ };
1497
+ const onKey = (e) => {
1498
+ if (e.key === "Escape")
1499
+ onClose();
1500
+ };
1501
+ window.addEventListener("mousedown", onDown, true);
1502
+ window.addEventListener("keydown", onKey);
1503
+ return () => {
1504
+ window.removeEventListener("mousedown", onDown, true);
1505
+ window.removeEventListener("keydown", onKey);
1506
+ };
1507
+ }, [open, onClose]);
1508
+ // Focus for keyboard accessibility
1509
+ React.useEffect(() => {
1510
+ if (open)
1511
+ ref.current?.focus();
1512
+ }, [open]);
1513
+ if (!open || !clientPos)
1514
+ return null;
1515
+ // Clamp menu position to viewport
1516
+ const MENU_MIN_WIDTH = 180;
1517
+ const PADDING = 16; // rough padding/shadow
1518
+ const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
1519
+ (MENU_MIN_WIDTH + PADDING));
1520
+ const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
1521
+ const handleClick = (typeId) => {
1522
+ // project() is deprecated; use screenToFlowPosition for screen coordinates
1523
+ const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
1524
+ onAdd(typeId, p);
1525
+ onClose();
1526
+ };
1527
+ return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-none shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
1528
+ e.preventDefault();
1529
+ e.stopPropagation();
1530
+ }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: cats.map((cat) => (jsxRuntime.jsxs("div", { className: "py-1", children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 text-[11px] uppercase tracking-wide text-gray-400", children: [cat, " ", jsxRuntime.jsxs("span", { className: "opacity-60 normal-case", children: ["(", grouped[cat].length, ")"] })] }), grouped[cat].map(({ id, label }) => (jsxRuntime.jsx("button", { onClick: () => handleClick(id), className: "block w-full text-left px-3 py-1 hover:bg-gray-100 cursor-pointer", title: id, children: label }, id)))] }, cat))) })] }));
1531
+ }
1532
+
1533
+ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
1534
+ const { wb, runner, engineKind } = useWorkbenchContext();
1535
+ const ref = React.useRef(null);
1536
+ // outside click + ESC
1537
+ React.useEffect(() => {
1538
+ if (!open)
1539
+ return;
1540
+ const onDown = (e) => {
1541
+ if (!ref.current)
1542
+ return;
1543
+ if (!ref.current.contains(e.target))
1544
+ onClose();
1545
+ };
1546
+ const onKey = (e) => {
1547
+ if (e.key === "Escape")
1548
+ onClose();
1549
+ };
1550
+ window.addEventListener("mousedown", onDown, true);
1551
+ window.addEventListener("keydown", onKey);
1552
+ return () => {
1553
+ window.removeEventListener("mousedown", onDown, true);
1554
+ window.removeEventListener("keydown", onKey);
1555
+ };
1556
+ }, [open, onClose]);
1557
+ React.useEffect(() => {
1558
+ if (open)
1559
+ ref.current?.focus();
1560
+ }, [open]);
1561
+ if (!open || !clientPos || !nodeId)
1562
+ return null;
1563
+ // clamp
1564
+ const MENU_MIN_WIDTH = 180;
1565
+ const PADDING = 16;
1566
+ const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
1567
+ (MENU_MIN_WIDTH + PADDING));
1568
+ const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
1569
+ // actions
1570
+ const handleDelete = () => {
1571
+ wb.removeNode(nodeId);
1572
+ onClose();
1573
+ };
1574
+ const handleDuplicate = () => {
1575
+ const def = wb.export();
1576
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
1577
+ if (!n)
1578
+ return onClose();
1579
+ const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
1580
+ wb.addNode({ typeId: n.typeId, params: n.params, position: { x: pos.x + 24, y: pos.y + 24 } });
1581
+ onClose();
1582
+ };
1583
+ const handleCopyId = async () => {
1584
+ try {
1585
+ await navigator.clipboard.writeText(nodeId);
1451
1586
  }
1452
- }
1453
- isRunning() {
1454
- return !!this.engine;
1455
- }
1456
- getRunningEngine() {
1457
- return this.runningKind;
1458
- }
1459
- // Ensure remote transport/runner
1460
- async ensureRemote() {
1461
- if (this.remote)
1462
- return this.remote;
1463
- let transport;
1464
- if (this.backend.kind === "remote-http") {
1465
- if (!sparkRemote.HttpPollingTransport)
1466
- throw new Error("HttpPollingTransport not available");
1467
- transport = new sparkRemote.HttpPollingTransport(this.backend.baseUrl);
1468
- await transport.connect();
1587
+ catch { }
1588
+ onClose();
1589
+ };
1590
+ const canRunPull = engineKind()?.toString() === "pull";
1591
+ const handleRunPull = async () => {
1592
+ try {
1593
+ await runner.computeNode(nodeId);
1469
1594
  }
1470
- else if (this.backend.kind === "remote-ws") {
1471
- if (!sparkRemote.WebSocketTransport)
1472
- throw new Error("WebSocketTransport not available");
1473
- transport = new sparkRemote.WebSocketTransport(this.backend.url);
1474
- await transport.connect();
1595
+ catch { }
1596
+ onClose();
1597
+ };
1598
+ return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, 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: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
1599
+ e.preventDefault();
1600
+ e.stopPropagation();
1601
+ }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDelete, children: "Delete" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDuplicate, children: "Duplicate" }), canRunPull && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleRunPull, children: "Run (pull)" })), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleCopyId, children: "Copy Node ID" })] }));
1602
+ }
1603
+
1604
+ function WorkbenchCanvas({ showValues, toString, toElement, }) {
1605
+ const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
1606
+ const ioValues = { inputs: inputsMap, outputs: outputsMap };
1607
+ const nodeValidation = validationByNode;
1608
+ const edgeValidation = validationByEdge.errors;
1609
+ const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
1610
+ const { nodeTypes, resolveNodeType } = React.useMemo(() => {
1611
+ // Build nodeTypes map using UI extension registry
1612
+ const ui = wb.getUI();
1613
+ const custom = new Map();
1614
+ for (const typeId of Array.from(registry.nodes.keys())) {
1615
+ const renderer = ui.getNodeRenderer(typeId);
1616
+ if (renderer)
1617
+ custom.set(typeId, renderer);
1618
+ }
1619
+ const types = {
1620
+ "spark-default": DefaultNode,
1621
+ default: DefaultNode,
1622
+ };
1623
+ for (const [typeId, comp] of custom.entries()) {
1624
+ types[`spark-${typeId}`] = comp;
1625
+ }
1626
+ const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark-${nodeTypeId}` : "spark-default";
1627
+ return { nodeTypes: types, resolveNodeType: resolver };
1628
+ // registry is stable; ui renderers expected to be set up before mount
1629
+ }, [wb, registry]);
1630
+ const { nodes, edges } = React.useMemo(() => {
1631
+ const def = wb.export();
1632
+ const sel = wb.getSelection();
1633
+ return toReactFlow(def, wb.getPositions(), registry, {
1634
+ showValues,
1635
+ inputs: ioValues.inputs,
1636
+ outputs: ioValues.outputs,
1637
+ resolveNodeType,
1638
+ toString,
1639
+ toElement,
1640
+ nodeStatus,
1641
+ edgeStatus,
1642
+ nodeValidation,
1643
+ edgeValidation,
1644
+ selectedNodeIds: new Set(sel.nodes),
1645
+ selectedEdgeIds: new Set(sel.edges),
1646
+ });
1647
+ }, [
1648
+ showValues,
1649
+ ioValues,
1650
+ valuesTick,
1651
+ toString,
1652
+ toElement,
1653
+ nodeStatus,
1654
+ edgeStatus,
1655
+ nodeValidation,
1656
+ edgeValidation,
1657
+ ]);
1658
+ const [menuOpen, setMenuOpen] = React.useState(false);
1659
+ const [menuPos, setMenuPos] = React.useState(null);
1660
+ const [nodeMenuOpen, setNodeMenuOpen] = React.useState(false);
1661
+ const [nodeMenuPos, setNodeMenuPos] = React.useState(null);
1662
+ const [nodeAtMenu, setNodeAtMenu] = React.useState(null);
1663
+ const onContextMenu = (e) => {
1664
+ e.preventDefault();
1665
+ // Determine if right-clicked over a node by hit-testing selection
1666
+ const target = e.target?.closest(".react-flow__node");
1667
+ if (target) {
1668
+ // Resolve node id from data-id attribute React Flow sets
1669
+ const nodeId = target.getAttribute("data-id");
1670
+ setNodeAtMenu(nodeId);
1671
+ setNodeMenuPos({ x: e.clientX, y: e.clientY });
1672
+ setNodeMenuOpen(true);
1673
+ setMenuOpen(false);
1475
1674
  }
1476
1675
  else {
1477
- throw new Error("Remote backend not configured");
1676
+ setMenuPos({ x: e.clientX, y: e.clientY });
1677
+ setMenuOpen(true);
1678
+ setNodeMenuOpen(false);
1478
1679
  }
1479
- const runner = new sparkRemote.RemoteRunner(transport);
1480
- this.remote = {
1481
- runner,
1482
- transport,
1483
- valueCache: new Map(),
1484
- listenersBound: false,
1485
- };
1486
- return this.remote;
1487
- }
1680
+ };
1681
+ const addNodeAt = (typeId, pos) => {
1682
+ wb.addNode({ typeId, position: pos });
1683
+ };
1684
+ return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.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: [jsxRuntime.jsx(ReactFlow.Background, {}), jsxRuntime.jsx(ReactFlow.MiniMap, {}), jsxRuntime.jsx(ReactFlow.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) }), jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: () => setNodeMenuOpen(false) })] }) }));
1488
1685
  }
1489
1686
 
1490
- function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, }) {
1687
+ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, }) {
1491
1688
  const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
1492
1689
  const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
1493
1690
  const selectedDesc = selectedNode
@@ -1653,7 +1850,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1653
1850
  runAutoLayout();
1654
1851
  }
1655
1852
  }, [wb, runAutoLayout]);
1656
- const setInput = React.useCallback((handle, raw) => {
1853
+ const baseSetInput = React.useCallback((handle, raw) => {
1657
1854
  if (!selectedNodeId)
1658
1855
  return;
1659
1856
  // If selected input is wired (has inbound edge), ignore user input to respect runtime value
@@ -1734,7 +1931,17 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1734
1931
  }
1735
1932
  runner.setInput(selectedNodeId, handle, value);
1736
1933
  }, [selectedNodeId, def.edges, selectedDesc, runner]);
1737
- const toDisplay = React.useCallback((typeId, value) => {
1934
+ const setInput = React.useMemo(() => {
1935
+ if (overrides?.setInput) {
1936
+ return overrides.setInput(baseSetInput, {
1937
+ runner,
1938
+ selectedNodeId,
1939
+ registry,
1940
+ });
1941
+ }
1942
+ return baseSetInput;
1943
+ }, [overrides, baseSetInput, runner, selectedNodeId, registry]);
1944
+ const baseToString = React.useCallback((typeId, value) => {
1738
1945
  if (value === undefined || value === null)
1739
1946
  return "";
1740
1947
  if (typeId && typeId.includes("enum:")) {
@@ -1766,6 +1973,21 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1766
1973
  }
1767
1974
  return String(value);
1768
1975
  }, [registry]);
1976
+ const baseToElement = React.useCallback((typeId, value) => {
1977
+ return (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: baseToString(typeId, value) }));
1978
+ }, [baseToString]);
1979
+ const toString = React.useMemo(() => {
1980
+ if (overrides?.toString)
1981
+ return overrides.toString(baseToString, { registry });
1982
+ return baseToString;
1983
+ }, [overrides, baseToString, registry]);
1984
+ // Optional: toElement (not currently consumed by core UI)
1985
+ // Consumers can access it by passing through their own node renderers.
1986
+ const toElement = React.useMemo(() => {
1987
+ if (overrides?.toElement)
1988
+ return overrides.toElement(baseToElement, { registry });
1989
+ return baseToElement;
1990
+ }, [overrides, baseToElement, registry]);
1769
1991
  return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.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()
1770
1992
  ? "Stop engine before switching example"
1771
1993
  : undefined, children: [jsxRuntime.jsx("option", { value: "simple", children: "Simple" }), jsxRuntime.jsx("option", { value: "async", children: "Async Chain" }), jsxRuntime.jsx("option", { value: "progress", children: "Progress + Errors" }), jsxRuntime.jsx("option", { value: "validation", children: "Validation" })] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxRuntime.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()
@@ -1783,9 +2005,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1783
2005
  catch (err) {
1784
2006
  alert(String(err?.message ?? err));
1785
2007
  }
1786
- }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { showValues: showValues, toDisplay: toDisplay }, exampleState) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toDisplay: toDisplay })] })] }));
2008
+ }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { showValues: showValues, toString: toString, toElement: toElement }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
1787
2009
  }
1788
- function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, }) {
2010
+ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
1789
2011
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
1790
2012
  const [wb] = React.useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
1791
2013
  const runner = React.useMemo(() => {
@@ -1796,35 +2018,36 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
1796
2018
  : { kind: "local" };
1797
2019
  return new GraphRunner(registry, backend);
1798
2020
  }, [registry, backendKind, httpBaseUrl, wsUrl]);
2021
+ // Allow external UI registration (e.g., node renderers) with access to wb
2022
+ React.useEffect(() => {
2023
+ const baseRegisterUI = (_wb) => { };
2024
+ overrides?.registerUI?.(baseRegisterUI, { wb });
2025
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2026
+ }, [wb, overrides]);
1799
2027
  return (jsxRuntime.jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, children: jsxRuntime.jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
1800
2028
  if (runner.isRunning())
1801
2029
  runner.dispose();
1802
2030
  onBackendKindChange(v);
1803
- }, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange }) }));
1804
- }
1805
-
1806
- function App() {
1807
- const [engine, setEngine] = useQueryParamString("engine", "");
1808
- const [example, setExample] = useQueryParamString("example", "simple");
1809
- const [debug, setDebug] = useQueryParamBoolean("debug", false);
1810
- const [showValues, setShowValues] = useQueryParamBoolean("values", false);
1811
- const [hideWorkbench, setHideWorkbench] = useQueryParamBoolean("hideWb", false);
1812
- const [autoScroll, setAutoScroll] = useQueryParamBoolean("autoScroll", true);
1813
- // Backend selection via URL params
1814
- const [backendKind, setBackendKind] = useQueryParamString("backend", "local");
1815
- const [httpBaseUrl, setHttpBaseUrl] = useQueryParamString("sparkHttp", "http://127.0.0.1:18080");
1816
- const [wsUrl, setWsUrl] = useQueryParamString("sparkWs", "ws://127.0.0.1:18081");
1817
- React.useEffect(() => {
1818
- document.getElementById("loading-screen")?.remove();
1819
- }, []);
1820
- return (jsxRuntime.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 }));
2031
+ }, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, overrides: overrides }) }));
1821
2032
  }
1822
2033
 
1823
2034
  exports.AbstractWorkbench = AbstractWorkbench;
1824
- exports.App = App;
1825
2035
  exports.CLIWorkbench = CLIWorkbench;
1826
2036
  exports.DefaultUIExtensionRegistry = DefaultUIExtensionRegistry;
2037
+ exports.GraphRunner = GraphRunner;
1827
2038
  exports.InMemoryWorkbench = InMemoryWorkbench;
1828
- exports.ReactFlowWorkbench = ReactFlowWorkbench;
1829
- exports.toReactFlow = toReactFlow$1;
2039
+ exports.Inspector = Inspector;
2040
+ exports.WorkbenchCanvas = WorkbenchCanvas;
2041
+ exports.WorkbenchContext = WorkbenchContext;
2042
+ exports.WorkbenchProvider = WorkbenchProvider;
2043
+ exports.WorkbenchStudio = WorkbenchStudio;
2044
+ exports.getNodeBorderClassNames = getNodeBorderClassNames;
2045
+ exports.toReactFlow = toReactFlow;
2046
+ exports.useQueryParamBoolean = useQueryParamBoolean;
2047
+ exports.useQueryParamString = useQueryParamString;
2048
+ exports.useWorkbenchBridge = useWorkbenchBridge;
2049
+ exports.useWorkbenchContext = useWorkbenchContext;
2050
+ exports.useWorkbenchGraphTick = useWorkbenchGraphTick;
2051
+ exports.useWorkbenchGraphUiTick = useWorkbenchGraphUiTick;
2052
+ exports.useWorkbenchVersionTick = useWorkbenchVersionTick;
1830
2053
  //# sourceMappingURL=index.cjs.map