@bian-womp/spark-workbench 0.1.9 → 0.1.10
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.
- package/lib/cjs/index.cjs +1136 -1109
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/index.d.ts +7 -2
- package/lib/cjs/src/index.d.ts.map +1 -1
- package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/cjs/src/misc/Inspector.d.ts +3 -2
- package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts +3 -2
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts +42 -0
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -0
- package/lib/cjs/src/misc/mapping.d.ts +2 -1
- package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
- package/lib/esm/index.js +1124 -1109
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/index.d.ts +7 -2
- package/lib/esm/src/index.d.ts.map +1 -1
- package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/esm/src/misc/Inspector.d.ts +3 -2
- package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts +3 -2
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchStudio.d.ts +42 -0
- package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -0
- package/lib/esm/src/misc/mapping.d.ts +2 -1
- package/lib/esm/src/misc/mapping.d.ts.map +1 -1
- package/package.json +4 -4
- package/lib/cjs/src/examples/reactflow/WorkbenchStudio.d.ts +0 -21
- package/lib/cjs/src/examples/reactflow/WorkbenchStudio.d.ts.map +0 -1
- package/lib/esm/src/examples/reactflow/WorkbenchStudio.d.ts +0 -21
- 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() {
|
|
@@ -350,1144 +350,1145 @@ class ReactFlowWorkbench {
|
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
|
|
353
|
+
class GraphRunner {
|
|
354
|
+
constructor(registry, backend) {
|
|
355
|
+
this.registry = registry;
|
|
356
|
+
this.listeners = new Map();
|
|
357
|
+
this.stagedInputs = {};
|
|
358
|
+
this.backend = { kind: "local" };
|
|
359
|
+
if (backend)
|
|
360
|
+
this.backend = backend;
|
|
361
|
+
}
|
|
362
|
+
build(def) {
|
|
363
|
+
if (this.backend.kind === "local") {
|
|
364
|
+
const builder = new sparkGraph.GraphBuilder(this.registry);
|
|
365
|
+
this.runtime = builder.build(def);
|
|
366
366
|
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
367
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
368
|
+
// Remote: no-op here; build is performed on remote server during launch
|
|
369
|
+
}
|
|
370
|
+
update(def) {
|
|
371
|
+
if (this.backend.kind === "local") {
|
|
372
|
+
if (!this.runtime)
|
|
373
|
+
return;
|
|
374
|
+
this.runtime.update(def, this.registry);
|
|
375
|
+
this.emit("invalidate", { reason: "graph-updated" });
|
|
376
|
+
return;
|
|
549
377
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
}
|
|
378
|
+
// Remote: forward update; ignore errors (fire-and-forget)
|
|
379
|
+
void this.ensureRemote().then(async (rc) => {
|
|
380
|
+
try {
|
|
381
|
+
await rc.runner.update(def);
|
|
382
|
+
this.emit("invalidate", { reason: "graph-updated" });
|
|
562
383
|
}
|
|
563
|
-
|
|
564
|
-
q.splice(0, q.length, ...next);
|
|
565
|
-
}
|
|
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
|
-
});
|
|
384
|
+
catch { }
|
|
573
385
|
});
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
386
|
+
}
|
|
387
|
+
launch(def, opts) {
|
|
388
|
+
if (this.engine) {
|
|
389
|
+
throw new Error("Engine already running. Stop the current engine first.");
|
|
390
|
+
}
|
|
391
|
+
if (this.backend.kind === "local") {
|
|
392
|
+
this.build(def);
|
|
393
|
+
if (!this.runtime)
|
|
394
|
+
throw new Error("Runtime not built");
|
|
395
|
+
const rt = this.runtime;
|
|
396
|
+
switch (opts.engine) {
|
|
397
|
+
case "push":
|
|
398
|
+
this.engine = new sparkGraph.PushEngine(rt);
|
|
399
|
+
break;
|
|
400
|
+
case "batched":
|
|
401
|
+
this.engine = new sparkGraph.BatchedEngine(rt, {
|
|
402
|
+
flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
|
|
403
|
+
});
|
|
404
|
+
break;
|
|
405
|
+
case "pull":
|
|
406
|
+
this.engine = new sparkGraph.PullEngine(rt);
|
|
407
|
+
break;
|
|
408
|
+
case "hybrid":
|
|
409
|
+
this.engine = new sparkGraph.HybridEngine(rt, {
|
|
410
|
+
windowMs: opts.hybrid?.windowMs ?? 250,
|
|
411
|
+
batchThreshold: opts.hybrid?.batchThreshold ?? 3,
|
|
412
|
+
});
|
|
413
|
+
break;
|
|
414
|
+
case "step":
|
|
415
|
+
this.engine = new sparkGraph.StepEngine(rt);
|
|
416
|
+
break;
|
|
417
|
+
default:
|
|
418
|
+
throw new Error("Unknown engine kind");
|
|
584
419
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
[nodeId]: { ...(s[nodeId] ?? {}), invalidated: true },
|
|
597
|
-
}));
|
|
420
|
+
this.engine.on("value", (e) => this.emit("value", e));
|
|
421
|
+
this.engine.on("error", (e) => this.emit("error", e));
|
|
422
|
+
this.engine.on("invalidate", (e) => this.emit("invalidate", e));
|
|
423
|
+
this.engine.on("stats", (e) => this.emit("stats", e));
|
|
424
|
+
this.engine.launch();
|
|
425
|
+
this.runningKind = opts.engine;
|
|
426
|
+
this.emit("status", { running: true, engine: this.runningKind });
|
|
427
|
+
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
428
|
+
for (const [handle, value] of Object.entries(map)) {
|
|
429
|
+
this.engine.setInput(nodeId, handle, value);
|
|
430
|
+
}
|
|
598
431
|
}
|
|
599
|
-
return
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// Remote: build remotely then launch
|
|
435
|
+
void this.ensureRemote().then(async (rc) => {
|
|
436
|
+
await rc.runner.build(def);
|
|
437
|
+
const eng = rc.runner.getEngine();
|
|
438
|
+
if (!rc.listenersBound) {
|
|
439
|
+
eng.on("value", (e) => {
|
|
440
|
+
rc.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
441
|
+
io: e.io,
|
|
442
|
+
value: e.value,
|
|
443
|
+
});
|
|
444
|
+
this.emit("value", e);
|
|
445
|
+
});
|
|
446
|
+
eng.on("error", (e) => this.emit("error", e));
|
|
447
|
+
eng.on("invalidate", (e) => this.emit("invalidate", e));
|
|
448
|
+
eng.on("stats", (e) => this.emit("stats", e));
|
|
449
|
+
rc.listenersBound = true;
|
|
610
450
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}));
|
|
451
|
+
this.engine = eng;
|
|
452
|
+
this.engine.launch();
|
|
453
|
+
this.runningKind = "push";
|
|
454
|
+
this.emit("status", { running: true, engine: this.runningKind });
|
|
455
|
+
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
456
|
+
for (const [handle, value] of Object.entries(map)) {
|
|
457
|
+
this.engine.setInput(nodeId, handle, value);
|
|
458
|
+
}
|
|
620
459
|
}
|
|
621
|
-
return add("runner", "error")(e);
|
|
622
460
|
});
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
461
|
+
}
|
|
462
|
+
setInput(nodeId, handle, value) {
|
|
463
|
+
if (!this.stagedInputs[nodeId])
|
|
464
|
+
this.stagedInputs[nodeId] = {};
|
|
465
|
+
this.stagedInputs[nodeId][handle] = value;
|
|
466
|
+
if (this.engine)
|
|
467
|
+
this.engine.setInput(nodeId, handle, value);
|
|
468
|
+
}
|
|
469
|
+
async step() {
|
|
470
|
+
if (this.backend.kind !== "local")
|
|
471
|
+
return; // unsupported remotely
|
|
472
|
+
const eng = this.engine;
|
|
473
|
+
if (eng instanceof sparkGraph.StepEngine)
|
|
474
|
+
await eng.step();
|
|
475
|
+
}
|
|
476
|
+
async computeNode(nodeId) {
|
|
477
|
+
if (this.backend.kind !== "local")
|
|
478
|
+
return; // unsupported remotely
|
|
479
|
+
const eng = this.engine;
|
|
480
|
+
if (eng instanceof sparkGraph.PullEngine)
|
|
481
|
+
await eng.computeNode(nodeId);
|
|
482
|
+
}
|
|
483
|
+
flush() {
|
|
484
|
+
if (this.backend.kind !== "local")
|
|
485
|
+
return; // unsupported remotely
|
|
486
|
+
const eng = this.engine;
|
|
487
|
+
if (eng instanceof sparkGraph.BatchedEngine)
|
|
488
|
+
eng.flush();
|
|
489
|
+
}
|
|
490
|
+
getOutputs(def) {
|
|
491
|
+
const out = {};
|
|
492
|
+
if (this.backend.kind === "local") {
|
|
493
|
+
if (!this.runtime)
|
|
494
|
+
return out;
|
|
495
|
+
for (const n of def.nodes) {
|
|
496
|
+
const desc = this.registry.nodes.get(n.typeId);
|
|
497
|
+
const handles = Object.keys(desc?.outputs ?? {});
|
|
498
|
+
for (const h of handles) {
|
|
499
|
+
const v = this.runtime.getOutput(n.nodeId, h);
|
|
500
|
+
if (v !== undefined) {
|
|
501
|
+
if (!out[n.nodeId])
|
|
502
|
+
out[n.nodeId] = {};
|
|
503
|
+
out[n.nodeId][h] = v;
|
|
629
504
|
}
|
|
630
|
-
|
|
631
|
-
});
|
|
632
|
-
}
|
|
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
|
-
}));
|
|
505
|
+
}
|
|
660
506
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
const cache = this.remote?.valueCache;
|
|
510
|
+
if (!cache)
|
|
511
|
+
return out;
|
|
512
|
+
for (const n of def.nodes) {
|
|
513
|
+
const desc = this.registry.nodes.get(n.typeId);
|
|
514
|
+
const handles = Object.keys(desc?.outputs ?? {});
|
|
515
|
+
for (const h of handles) {
|
|
516
|
+
const key = `${n.nodeId}.${h}`;
|
|
517
|
+
const rec = cache.get(key);
|
|
518
|
+
if (rec && rec.io === "output") {
|
|
519
|
+
if (!out[n.nodeId])
|
|
520
|
+
out[n.nodeId] = {};
|
|
521
|
+
out[n.nodeId][h] = rec.value;
|
|
522
|
+
}
|
|
667
523
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
524
|
+
}
|
|
525
|
+
return out;
|
|
526
|
+
}
|
|
527
|
+
getInputs(def) {
|
|
528
|
+
const out = {};
|
|
529
|
+
if (this.backend.kind === "local") {
|
|
530
|
+
for (const n of def.nodes) {
|
|
531
|
+
const staged = this.stagedInputs[n.nodeId] ?? {};
|
|
532
|
+
const runtimeInputs = this.runtime
|
|
533
|
+
? this.runtime.__unsafe_getNodeData?.(n.nodeId)?.inputs ?? {}
|
|
534
|
+
: {};
|
|
535
|
+
if (this.isRunning()) {
|
|
536
|
+
out[n.nodeId] = runtimeInputs;
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
const merged = { ...runtimeInputs, ...staged };
|
|
540
|
+
if (Object.keys(merged).length > 0)
|
|
541
|
+
out[n.nodeId] = merged;
|
|
542
|
+
}
|
|
674
543
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
});
|
|
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 { }
|
|
714
|
-
}
|
|
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
|
-
}
|
|
747
|
-
}
|
|
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 });
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
return list;
|
|
764
|
-
}, [validation]);
|
|
765
|
-
const validationByEdge = React.useMemo(() => {
|
|
766
|
-
const errors = {};
|
|
767
|
-
const issues = {};
|
|
768
|
-
if (!validation)
|
|
769
|
-
return { errors, issues };
|
|
770
|
-
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);
|
|
775
|
-
if (d?.edgeId) {
|
|
776
|
-
if (level === "error")
|
|
777
|
-
errors[d.edgeId] = true;
|
|
778
|
-
const arr = issues[d.edgeId] ?? (issues[d.edgeId] = []);
|
|
779
|
-
arr.push({ level, message, code });
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
const cache = this.remote?.valueCache;
|
|
547
|
+
for (const n of def.nodes) {
|
|
548
|
+
const staged = this.stagedInputs[n.nodeId] ?? {};
|
|
549
|
+
const desc = this.registry.nodes.get(n.typeId);
|
|
550
|
+
const handles = Object.keys(desc?.inputs ?? {});
|
|
551
|
+
const cur = {};
|
|
552
|
+
for (const h of handles) {
|
|
553
|
+
const rec = cache?.get(`${n.nodeId}.${h}`);
|
|
554
|
+
if (rec && rec.io === "input")
|
|
555
|
+
cur[h] = rec.value;
|
|
780
556
|
}
|
|
557
|
+
const merged = this.isRunning() ? cur : { ...cur, ...staged };
|
|
558
|
+
if (Object.keys(merged).length > 0)
|
|
559
|
+
out[n.nodeId] = merged;
|
|
781
560
|
}
|
|
782
|
-
return
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
561
|
+
return out;
|
|
562
|
+
}
|
|
563
|
+
async whenIdle() {
|
|
564
|
+
await this.engine?.whenIdle();
|
|
565
|
+
}
|
|
566
|
+
on(event, handler) {
|
|
567
|
+
if (!this.listeners.has(event))
|
|
568
|
+
this.listeners.set(event, new Set());
|
|
569
|
+
const set = this.listeners.get(event);
|
|
570
|
+
set.add(handler);
|
|
571
|
+
return () => set.delete(handler);
|
|
572
|
+
}
|
|
573
|
+
emit(event, payload) {
|
|
574
|
+
const set = this.listeners.get(event);
|
|
575
|
+
if (set)
|
|
576
|
+
for (const h of Array.from(set))
|
|
577
|
+
h(payload);
|
|
578
|
+
}
|
|
579
|
+
dispose() {
|
|
580
|
+
this.engine?.dispose();
|
|
581
|
+
this.engine = undefined;
|
|
582
|
+
this.runtime?.dispose();
|
|
583
|
+
this.runtime = undefined;
|
|
584
|
+
this.remote = undefined;
|
|
585
|
+
if (this.runningKind) {
|
|
586
|
+
this.runningKind = undefined;
|
|
587
|
+
this.emit("status", { running: false, engine: undefined });
|
|
789
588
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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);
|
|
589
|
+
}
|
|
590
|
+
isRunning() {
|
|
591
|
+
return !!this.engine;
|
|
592
|
+
}
|
|
593
|
+
getRunningEngine() {
|
|
594
|
+
return this.runningKind;
|
|
595
|
+
}
|
|
596
|
+
// Ensure remote transport/runner
|
|
597
|
+
async ensureRemote() {
|
|
598
|
+
if (this.remote)
|
|
599
|
+
return this.remote;
|
|
600
|
+
let transport;
|
|
601
|
+
if (this.backend.kind === "remote-http") {
|
|
602
|
+
if (!sparkRemote.HttpPollingTransport)
|
|
603
|
+
throw new Error("HttpPollingTransport not available");
|
|
604
|
+
transport = new sparkRemote.HttpPollingTransport(this.backend.baseUrl);
|
|
605
|
+
await transport.connect();
|
|
1006
606
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
607
|
+
else if (this.backend.kind === "remote-ws") {
|
|
608
|
+
if (!sparkRemote.WebSocketTransport)
|
|
609
|
+
throw new Error("WebSocketTransport not available");
|
|
610
|
+
transport = new sparkRemote.WebSocketTransport(this.backend.url);
|
|
611
|
+
await transport.connect();
|
|
1010
612
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
toDisplay,
|
|
1024
|
-
nodeStatus,
|
|
1025
|
-
edgeStatus,
|
|
1026
|
-
nodeValidation,
|
|
1027
|
-
edgeValidation,
|
|
1028
|
-
});
|
|
1029
|
-
}, [
|
|
1030
|
-
showValues,
|
|
1031
|
-
ioValues,
|
|
1032
|
-
valuesTick,
|
|
1033
|
-
toDisplay,
|
|
1034
|
-
nodeStatus,
|
|
1035
|
-
edgeStatus,
|
|
1036
|
-
nodeValidation,
|
|
1037
|
-
edgeValidation,
|
|
1038
|
-
]);
|
|
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) })] }) }));
|
|
613
|
+
else {
|
|
614
|
+
throw new Error("Remote backend not configured");
|
|
615
|
+
}
|
|
616
|
+
const runner = new sparkRemote.RemoteRunner(transport);
|
|
617
|
+
this.remote = {
|
|
618
|
+
runner,
|
|
619
|
+
transport,
|
|
620
|
+
valueCache: new Map(),
|
|
621
|
+
listenersBound: false,
|
|
622
|
+
};
|
|
623
|
+
return this.remote;
|
|
624
|
+
}
|
|
1050
625
|
}
|
|
1051
626
|
|
|
1052
|
-
function
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
const rows = React.useMemo(() => {
|
|
1056
|
-
const filtered = hideWorkbench
|
|
1057
|
-
? events.filter((e) => e.source !== "workbench")
|
|
1058
|
-
: events;
|
|
1059
|
-
return filtered.slice().reverse();
|
|
1060
|
-
}, [events, hideWorkbench]);
|
|
1061
|
-
React.useEffect(() => {
|
|
1062
|
-
if (!autoScroll)
|
|
627
|
+
function useWorkbenchBridge(wb) {
|
|
628
|
+
const onConnect = React.useCallback((params) => {
|
|
629
|
+
if (!params.source || !params.target)
|
|
1063
630
|
return;
|
|
1064
|
-
|
|
1065
|
-
if (!el)
|
|
631
|
+
if (!params.sourceHandle || !params.targetHandle)
|
|
1066
632
|
return;
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
633
|
+
wb.connect({
|
|
634
|
+
source: { nodeId: params.source, handle: params.sourceHandle },
|
|
635
|
+
target: { nodeId: params.target, handle: params.targetHandle },
|
|
636
|
+
});
|
|
637
|
+
}, [wb]);
|
|
638
|
+
const onNodesChange = React.useCallback((changes) => {
|
|
639
|
+
changes.forEach((c) => {
|
|
640
|
+
if (c.type === "position" && c.position)
|
|
641
|
+
wb.setPosition(c.id, c.position);
|
|
642
|
+
if (c.type === "remove")
|
|
643
|
+
wb.removeNode(c.id);
|
|
644
|
+
if (c.type === "select")
|
|
645
|
+
wb.toggleNodeSelection(c.id);
|
|
646
|
+
});
|
|
647
|
+
}, [wb]);
|
|
648
|
+
const onEdgesDelete = React.useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
|
|
649
|
+
const onEdgesChange = React.useCallback((changes) => {
|
|
650
|
+
changes.forEach((c) => {
|
|
651
|
+
if (c.type === "remove")
|
|
652
|
+
wb.disconnect(c.id);
|
|
653
|
+
else if (c.type === "select")
|
|
654
|
+
wb.toggleEdgeSelection(c.id);
|
|
655
|
+
});
|
|
656
|
+
}, [wb]);
|
|
657
|
+
const onNodesDelete = React.useCallback((nodes) => {
|
|
658
|
+
for (const n of nodes)
|
|
659
|
+
wb.removeNode(n.id);
|
|
660
|
+
}, [wb]);
|
|
661
|
+
const onSelectionChange = React.useCallback((sel) => {
|
|
662
|
+
const next = {
|
|
663
|
+
nodes: sel.nodes.map((n) => n.id),
|
|
664
|
+
edges: sel.edges.map((e) => e.id),
|
|
665
|
+
};
|
|
666
|
+
const cur = wb.getSelection();
|
|
667
|
+
const sameLen = cur.nodes.length === next.nodes.length &&
|
|
668
|
+
cur.edges.length === next.edges.length;
|
|
669
|
+
const same = sameLen &&
|
|
670
|
+
cur.nodes.every((id, i) => id === next.nodes[i]) &&
|
|
671
|
+
cur.edges.every((id, i) => id === next.edges[i]);
|
|
672
|
+
if (!same)
|
|
673
|
+
wb.setSelection(next);
|
|
674
|
+
}, [wb]);
|
|
675
|
+
return {
|
|
676
|
+
onConnect,
|
|
677
|
+
onNodesChange,
|
|
678
|
+
onEdgesChange,
|
|
679
|
+
onEdgesDelete,
|
|
680
|
+
onNodesDelete,
|
|
681
|
+
onSelectionChange,
|
|
1076
682
|
};
|
|
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
683
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
684
|
+
function useWorkbenchGraphTick(wb) {
|
|
685
|
+
const [tick, setTick] = React.useState(0);
|
|
686
|
+
React.useEffect(() => {
|
|
687
|
+
const bump = () => setTick((t) => t + 1);
|
|
688
|
+
const off = wb.on("graphChanged", bump);
|
|
689
|
+
return () => off();
|
|
690
|
+
}, [wb]);
|
|
691
|
+
return tick;
|
|
692
|
+
}
|
|
693
|
+
function useWorkbenchGraphUiTick(wb) {
|
|
694
|
+
const [tick, setTick] = React.useState(0);
|
|
695
|
+
React.useEffect(() => {
|
|
696
|
+
const bump = () => setTick((t) => t + 1);
|
|
697
|
+
const off = wb.on("graphUiChanged", bump);
|
|
698
|
+
return () => off();
|
|
699
|
+
}, [wb]);
|
|
700
|
+
return tick;
|
|
701
|
+
}
|
|
702
|
+
function useWorkbenchVersionTick(runner) {
|
|
703
|
+
const [version, setVersion] = React.useState(0);
|
|
704
|
+
React.useEffect(() => {
|
|
705
|
+
const bump = () => setVersion((v) => v + 1);
|
|
706
|
+
const u1 = runner.on("value", bump);
|
|
707
|
+
const u2 = runner.on("error", bump);
|
|
708
|
+
const u3 = runner.on("invalidate", bump);
|
|
709
|
+
const u4 = runner.on("status", bump);
|
|
710
|
+
const u5 = runner.on("stats", bump);
|
|
711
|
+
return () => {
|
|
712
|
+
u1();
|
|
713
|
+
u2();
|
|
714
|
+
u3();
|
|
715
|
+
u4();
|
|
716
|
+
u5();
|
|
717
|
+
};
|
|
718
|
+
}, [runner]);
|
|
719
|
+
return version;
|
|
720
|
+
}
|
|
721
|
+
// Query param helpers
|
|
722
|
+
function setSearchParam(key, val) {
|
|
723
|
+
if (typeof window === "undefined")
|
|
724
|
+
return;
|
|
725
|
+
const url = new URL(window.location.href);
|
|
726
|
+
if (val === undefined || val === "")
|
|
727
|
+
url.searchParams.delete(key);
|
|
728
|
+
else
|
|
729
|
+
url.searchParams.set(key, val);
|
|
730
|
+
window.history.replaceState({}, "", url.toString());
|
|
731
|
+
}
|
|
732
|
+
function useQueryParamBoolean(key, defaultValue) {
|
|
733
|
+
const initial = React.useMemo(() => {
|
|
734
|
+
if (typeof window === "undefined")
|
|
735
|
+
return defaultValue;
|
|
736
|
+
const sp = new URLSearchParams(window.location.search);
|
|
737
|
+
const v = sp.get(key);
|
|
738
|
+
if (v === null)
|
|
739
|
+
return defaultValue;
|
|
740
|
+
return v === "1" || v === "true";
|
|
741
|
+
}, [key, defaultValue]);
|
|
742
|
+
const [val, setVal] = React.useState(initial);
|
|
743
|
+
const set = React.useCallback((v) => {
|
|
744
|
+
setVal(v);
|
|
745
|
+
setSearchParam(key, v ? "1" : undefined);
|
|
746
|
+
}, [key]);
|
|
747
|
+
React.useEffect(() => {
|
|
748
|
+
const onPop = () => {
|
|
749
|
+
const sp = new URLSearchParams(window.location.search);
|
|
750
|
+
const v = sp.get(key);
|
|
751
|
+
setVal(v === "1" || v === "true");
|
|
752
|
+
};
|
|
753
|
+
window.addEventListener("popstate", onPop);
|
|
754
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
755
|
+
}, [key]);
|
|
756
|
+
return [val, set];
|
|
757
|
+
}
|
|
758
|
+
function useQueryParamString(key, defaultValue) {
|
|
759
|
+
const initial = React.useMemo(() => {
|
|
760
|
+
if (typeof window === "undefined")
|
|
761
|
+
return defaultValue;
|
|
762
|
+
const sp = new URLSearchParams(window.location.search);
|
|
763
|
+
const v = sp.get(key);
|
|
764
|
+
return v ?? defaultValue;
|
|
765
|
+
}, [key, defaultValue]);
|
|
766
|
+
const [val, setVal] = React.useState(initial);
|
|
767
|
+
const set = React.useCallback((v) => {
|
|
768
|
+
setVal(v);
|
|
769
|
+
setSearchParam(key, v);
|
|
770
|
+
}, [key]);
|
|
1115
771
|
React.useEffect(() => {
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
return false;
|
|
1121
|
-
for (const k of ak)
|
|
1122
|
-
if (a[k] !== b[k])
|
|
1123
|
-
return false;
|
|
1124
|
-
return true;
|
|
772
|
+
const onPop = () => {
|
|
773
|
+
const sp = new URLSearchParams(window.location.search);
|
|
774
|
+
const v = sp.get(key) ?? undefined;
|
|
775
|
+
setVal(v);
|
|
1125
776
|
};
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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 }) }))] }));
|
|
777
|
+
window.addEventListener("popstate", onPop);
|
|
778
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
779
|
+
}, [key]);
|
|
780
|
+
return [val, set];
|
|
1214
781
|
}
|
|
1215
782
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
783
|
+
const WorkbenchContext = React.createContext(null);
|
|
784
|
+
function useWorkbenchContext() {
|
|
785
|
+
const ctx = React.useContext(WorkbenchContext);
|
|
786
|
+
if (!ctx)
|
|
787
|
+
throw new Error("useWorkbenchContext must be used within WorkbenchProvider");
|
|
788
|
+
return ctx;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
792
|
+
const [nodeStatus, setNodeStatus] = React.useState({});
|
|
793
|
+
const [edgeStatus, setEdgeStatus] = React.useState({});
|
|
794
|
+
const [events, setEvents] = React.useState([]);
|
|
795
|
+
const clearEvents = React.useCallback(() => setEvents([]), []);
|
|
796
|
+
// Validation
|
|
797
|
+
const [validation, setValidation] = React.useState(undefined);
|
|
798
|
+
// Selection (mirror workbench selectionChanged)
|
|
799
|
+
const [selectedNodeId, setSelectedNodeId] = React.useState();
|
|
800
|
+
const [selectedEdgeId, setSelectedEdgeId] = React.useState();
|
|
801
|
+
const setSelection = React.useCallback((sel) => wb.setSelection(sel), [wb]);
|
|
802
|
+
// Ticks
|
|
803
|
+
const graphTick = useWorkbenchGraphTick(wb);
|
|
804
|
+
const graphUiTick = useWorkbenchGraphUiTick(wb);
|
|
805
|
+
const versionTick = useWorkbenchVersionTick(runner);
|
|
806
|
+
const valuesTick = versionTick + graphTick + graphUiTick;
|
|
807
|
+
// Def and IO values
|
|
808
|
+
const def = wb.export();
|
|
809
|
+
const inputsMap = React.useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
|
|
810
|
+
const outputsMap = React.useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
|
|
811
|
+
// Auto layout (simple layered layout)
|
|
812
|
+
const runAutoLayout = React.useCallback(() => {
|
|
813
|
+
const cur = wb.export();
|
|
814
|
+
const indegree = {};
|
|
815
|
+
const adj = {};
|
|
816
|
+
for (const n of cur.nodes) {
|
|
817
|
+
indegree[n.nodeId] = 0;
|
|
818
|
+
adj[n.nodeId] = [];
|
|
1230
819
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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" });
|
|
1239
|
-
return;
|
|
820
|
+
for (const e of cur.edges) {
|
|
821
|
+
indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
|
|
822
|
+
adj[e.source.nodeId].push(e.target.nodeId);
|
|
1240
823
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
824
|
+
const q = Object.keys(indegree).filter((k) => indegree[k] === 0);
|
|
825
|
+
const layers = [];
|
|
826
|
+
while (q.length) {
|
|
827
|
+
const layer = [];
|
|
828
|
+
const next = [];
|
|
829
|
+
for (const id of q) {
|
|
830
|
+
layer.push(id);
|
|
831
|
+
for (const nb of adj[id]) {
|
|
832
|
+
indegree[nb] -= 1;
|
|
833
|
+
if (indegree[nb] === 0)
|
|
834
|
+
next.push(nb);
|
|
835
|
+
}
|
|
1246
836
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
launch(def, opts) {
|
|
1251
|
-
if (this.engine) {
|
|
1252
|
-
throw new Error("Engine already running. Stop the current engine first.");
|
|
837
|
+
layers.push(layer);
|
|
838
|
+
q.splice(0, q.length, ...next);
|
|
1253
839
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
840
|
+
const X = 360;
|
|
841
|
+
const Y = 180;
|
|
842
|
+
const pos = {};
|
|
843
|
+
layers.forEach((layer, layerIndex) => {
|
|
844
|
+
layer.forEach((id, itemIndex) => {
|
|
845
|
+
pos[id] = { x: layerIndex * X, y: itemIndex * Y };
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
wb.setPositions(pos);
|
|
849
|
+
}, [wb]);
|
|
850
|
+
// Subscribe to runner/workbench events
|
|
851
|
+
React.useEffect(() => {
|
|
852
|
+
const add = (source, type) => (payload) => setEvents((prev) => {
|
|
853
|
+
if (source === "workbench" &&
|
|
854
|
+
(type === "graphChanged" || type === "graphUiChanged")) {
|
|
855
|
+
const changeType = payload?.change?.type;
|
|
856
|
+
if (changeType === "moveNode" || changeType === "moveNodes")
|
|
857
|
+
return prev;
|
|
858
|
+
}
|
|
859
|
+
const next = [
|
|
860
|
+
{ at: Date.now(), source, type, payload: structuredClone(payload) },
|
|
861
|
+
...prev,
|
|
862
|
+
];
|
|
863
|
+
return next.length > 200 ? next.slice(0, 200) : next;
|
|
864
|
+
});
|
|
865
|
+
const off1 = runner.on("value", (e) => {
|
|
866
|
+
if (e?.io === "input") {
|
|
867
|
+
const nodeId = e?.nodeId;
|
|
868
|
+
setNodeStatus((s) => ({
|
|
869
|
+
...s,
|
|
870
|
+
[nodeId]: { ...(s[nodeId] ?? {}), invalidated: true },
|
|
871
|
+
}));
|
|
872
|
+
}
|
|
873
|
+
return add("runner", "value")(e);
|
|
874
|
+
});
|
|
875
|
+
const off2 = runner.on("error", (e) => {
|
|
876
|
+
const edgeError = e;
|
|
877
|
+
const nodeError = e;
|
|
878
|
+
if (edgeError?.kind === "edge-convert") {
|
|
879
|
+
const edgeId = edgeError.edgeId;
|
|
880
|
+
setEdgeStatus((s) => ({
|
|
881
|
+
...s,
|
|
882
|
+
[edgeId]: { ...(s[edgeId] ?? {}), lastError: edgeError.err },
|
|
883
|
+
}));
|
|
1282
884
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
this.engine.setInput(nodeId, handle, value);
|
|
1293
|
-
}
|
|
885
|
+
else if (nodeError?.nodeId) {
|
|
886
|
+
const nodeId = nodeError?.nodeId;
|
|
887
|
+
setNodeStatus((s) => ({
|
|
888
|
+
...s,
|
|
889
|
+
[nodeId]: {
|
|
890
|
+
...(s[nodeId] ?? {}),
|
|
891
|
+
lastError: nodeError?.err,
|
|
892
|
+
},
|
|
893
|
+
}));
|
|
1294
894
|
}
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
value: e.value,
|
|
1306
|
-
});
|
|
1307
|
-
this.emit("value", e);
|
|
895
|
+
return add("runner", "error")(e);
|
|
896
|
+
});
|
|
897
|
+
const off3 = runner.on("invalidate", (e) => {
|
|
898
|
+
if (e?.reason === "graph-updated") {
|
|
899
|
+
setNodeStatus((s) => {
|
|
900
|
+
const next = {};
|
|
901
|
+
for (const n of wb.export().nodes) {
|
|
902
|
+
next[n.nodeId] = { ...(s[n.nodeId] ?? {}), invalidated: true };
|
|
903
|
+
}
|
|
904
|
+
return next;
|
|
1308
905
|
});
|
|
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
906
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
907
|
+
return add("runner", "invalidate")(e);
|
|
908
|
+
});
|
|
909
|
+
const off3b = runner.on("stats", (s) => {
|
|
910
|
+
if (!s)
|
|
911
|
+
return;
|
|
912
|
+
if (s.kind === "node-start") {
|
|
913
|
+
const id = s.nodeId;
|
|
914
|
+
setNodeStatus((prev) => ({
|
|
915
|
+
...prev,
|
|
916
|
+
[id]: {
|
|
917
|
+
...(prev[id] ?? {}),
|
|
918
|
+
running: true,
|
|
919
|
+
progress: 0,
|
|
920
|
+
invalidated: false,
|
|
921
|
+
},
|
|
922
|
+
}));
|
|
923
|
+
}
|
|
924
|
+
else if (s.kind === "node-progress") {
|
|
925
|
+
const id = s.nodeId;
|
|
926
|
+
setNodeStatus((prev) => ({
|
|
927
|
+
...prev,
|
|
928
|
+
[id]: {
|
|
929
|
+
...(prev[id] ?? {}),
|
|
930
|
+
running: true,
|
|
931
|
+
progress: Number(s.progress) || 0,
|
|
932
|
+
},
|
|
933
|
+
}));
|
|
934
|
+
}
|
|
935
|
+
else if (s.kind === "node-done") {
|
|
936
|
+
const id = s.nodeId;
|
|
937
|
+
setNodeStatus((prev) => ({
|
|
938
|
+
...prev,
|
|
939
|
+
[id]: { ...(prev[id] ?? {}), running: false },
|
|
940
|
+
}));
|
|
941
|
+
}
|
|
942
|
+
else if (s.kind === "edge-start") {
|
|
943
|
+
const id = s.edgeId;
|
|
944
|
+
setEdgeStatus((prev) => ({
|
|
945
|
+
...prev,
|
|
946
|
+
[id]: { ...(prev[id] ?? {}), running: true },
|
|
947
|
+
}));
|
|
948
|
+
}
|
|
949
|
+
else if (s.kind === "edge-done") {
|
|
950
|
+
const id = s.edgeId;
|
|
951
|
+
setEdgeStatus((prev) => ({
|
|
952
|
+
...prev,
|
|
953
|
+
[id]: { ...(prev[id] ?? {}), running: false },
|
|
954
|
+
}));
|
|
1322
955
|
}
|
|
956
|
+
return add("runner", "stats")(s);
|
|
1323
957
|
});
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
-
}
|
|
958
|
+
const off4 = wb.on("graphChanged", add("workbench", "graphChanged"));
|
|
959
|
+
const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
960
|
+
const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
961
|
+
const off5b = wb.on("validationChanged", (r) => setValidation(r));
|
|
962
|
+
const off6 = wb.on("selectionChanged", (sel) => {
|
|
963
|
+
setSelectedNodeId(sel.nodes?.[0]);
|
|
964
|
+
setSelectedEdgeId(sel.edges?.[0]);
|
|
965
|
+
});
|
|
966
|
+
const off7 = wb.on("error", add("workbench", "error"));
|
|
967
|
+
wb.refreshValidation();
|
|
968
|
+
return () => {
|
|
969
|
+
off1();
|
|
970
|
+
off2();
|
|
971
|
+
off3();
|
|
972
|
+
off3b();
|
|
973
|
+
off4();
|
|
974
|
+
off4b();
|
|
975
|
+
off5();
|
|
976
|
+
off5b();
|
|
977
|
+
off6();
|
|
978
|
+
off7();
|
|
979
|
+
};
|
|
980
|
+
}, [runner, wb]);
|
|
981
|
+
// Push incremental updates into running engine without full reload
|
|
982
|
+
React.useEffect(() => {
|
|
983
|
+
if (runner.isRunning()) {
|
|
984
|
+
try {
|
|
985
|
+
runner.update(def);
|
|
1369
986
|
}
|
|
1370
|
-
|
|
987
|
+
catch { }
|
|
1371
988
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
989
|
+
}, [runner, def, graphTick]);
|
|
990
|
+
const validationByNode = React.useMemo(() => {
|
|
991
|
+
const inputs = {};
|
|
992
|
+
const outputs = {};
|
|
993
|
+
const issues = {};
|
|
994
|
+
if (!validation)
|
|
995
|
+
return { inputs, outputs, issues };
|
|
996
|
+
for (const is of validation.issues ?? []) {
|
|
997
|
+
const d = is?.data;
|
|
998
|
+
const level = is?.level;
|
|
999
|
+
const code = String(is?.code ?? "");
|
|
1000
|
+
const message = String(is?.message ?? code);
|
|
1001
|
+
if (!d)
|
|
1002
|
+
continue;
|
|
1003
|
+
if (d.nodeId) {
|
|
1004
|
+
if (d.input) {
|
|
1005
|
+
const arr = inputs[d.nodeId] ?? (inputs[d.nodeId] = []);
|
|
1006
|
+
arr.push({ handle: String(d.input), level, message, code });
|
|
1007
|
+
const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
|
|
1008
|
+
nodeArr.push({ level, message, code });
|
|
1385
1009
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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;
|
|
1010
|
+
if (d.output) {
|
|
1011
|
+
const arr = outputs[d.nodeId] ?? (outputs[d.nodeId] = []);
|
|
1012
|
+
arr.push({ handle: String(d.output), level, message, code });
|
|
1013
|
+
const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
|
|
1014
|
+
nodeArr.push({ level, message, code });
|
|
1400
1015
|
}
|
|
1401
|
-
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
out[n.nodeId] = merged;
|
|
1016
|
+
if (!d.input && !d.output) {
|
|
1017
|
+
const arr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
|
|
1018
|
+
arr.push({ level, message, code });
|
|
1405
1019
|
}
|
|
1406
1020
|
}
|
|
1407
|
-
return out;
|
|
1408
1021
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1022
|
+
return { inputs, outputs, issues };
|
|
1023
|
+
}, [validation]);
|
|
1024
|
+
const validationGlobal = React.useMemo(() => {
|
|
1025
|
+
const list = [];
|
|
1026
|
+
if (!validation)
|
|
1027
|
+
return list;
|
|
1028
|
+
for (const is of validation.issues ?? []) {
|
|
1029
|
+
const d = is?.data;
|
|
1030
|
+
const level = is?.level;
|
|
1031
|
+
const code = String(is?.code ?? "");
|
|
1032
|
+
const message = String(is?.message ?? code);
|
|
1033
|
+
if (!d || (!d.nodeId && !d.edgeId)) {
|
|
1034
|
+
list.push({ level, code, message });
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return list;
|
|
1038
|
+
}, [validation]);
|
|
1039
|
+
const validationByEdge = React.useMemo(() => {
|
|
1040
|
+
const errors = {};
|
|
1041
|
+
const issues = {};
|
|
1042
|
+
if (!validation)
|
|
1043
|
+
return { errors, issues };
|
|
1044
|
+
for (const is of validation.issues ?? []) {
|
|
1045
|
+
const d = is?.data;
|
|
1046
|
+
const level = is?.level;
|
|
1047
|
+
const code = String(is?.code ?? "");
|
|
1048
|
+
const message = String(is?.message ?? code);
|
|
1049
|
+
if (d?.edgeId) {
|
|
1050
|
+
if (level === "error")
|
|
1051
|
+
errors[d.edgeId] = true;
|
|
1052
|
+
const arr = issues[d.edgeId] ?? (issues[d.edgeId] = []);
|
|
1053
|
+
arr.push({ level, message, code });
|
|
1419
1054
|
}
|
|
1420
|
-
const merged = this.isRunning() ? cur : { ...cur, ...staged };
|
|
1421
|
-
if (Object.keys(merged).length > 0)
|
|
1422
|
-
out[n.nodeId] = merged;
|
|
1423
1055
|
}
|
|
1424
|
-
return
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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 });
|
|
1056
|
+
return { errors, issues };
|
|
1057
|
+
}, [validation]);
|
|
1058
|
+
const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
|
|
1059
|
+
const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
|
|
1060
|
+
const start = React.useCallback((engine) => {
|
|
1061
|
+
try {
|
|
1062
|
+
runner.launch(wb.export(), { engine });
|
|
1451
1063
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1064
|
+
catch { }
|
|
1065
|
+
}, [runner, wb]);
|
|
1066
|
+
const stop = React.useCallback(() => runner.dispose(), [runner]);
|
|
1067
|
+
const step = React.useCallback(() => runner.step(), [runner]);
|
|
1068
|
+
const flush = React.useCallback(() => runner.flush(), [runner]);
|
|
1069
|
+
const value = React.useMemo(() => ({
|
|
1070
|
+
wb,
|
|
1071
|
+
runner,
|
|
1072
|
+
registry,
|
|
1073
|
+
setRegistry,
|
|
1074
|
+
def,
|
|
1075
|
+
selectedNodeId,
|
|
1076
|
+
selectedEdgeId,
|
|
1077
|
+
setSelection,
|
|
1078
|
+
nodeStatus,
|
|
1079
|
+
edgeStatus,
|
|
1080
|
+
valuesTick,
|
|
1081
|
+
inputsMap,
|
|
1082
|
+
outputsMap,
|
|
1083
|
+
validationByNode,
|
|
1084
|
+
validationByEdge,
|
|
1085
|
+
validationGlobal,
|
|
1086
|
+
events,
|
|
1087
|
+
clearEvents,
|
|
1088
|
+
isRunning,
|
|
1089
|
+
engineKind,
|
|
1090
|
+
start,
|
|
1091
|
+
stop,
|
|
1092
|
+
step,
|
|
1093
|
+
flush,
|
|
1094
|
+
runAutoLayout,
|
|
1095
|
+
}), [
|
|
1096
|
+
wb,
|
|
1097
|
+
runner,
|
|
1098
|
+
registry,
|
|
1099
|
+
setRegistry,
|
|
1100
|
+
def,
|
|
1101
|
+
selectedNodeId,
|
|
1102
|
+
selectedEdgeId,
|
|
1103
|
+
setSelection,
|
|
1104
|
+
nodeStatus,
|
|
1105
|
+
edgeStatus,
|
|
1106
|
+
valuesTick,
|
|
1107
|
+
inputsMap,
|
|
1108
|
+
outputsMap,
|
|
1109
|
+
validationByNode,
|
|
1110
|
+
validationByEdge,
|
|
1111
|
+
validationGlobal,
|
|
1112
|
+
events,
|
|
1113
|
+
clearEvents,
|
|
1114
|
+
isRunning,
|
|
1115
|
+
engineKind,
|
|
1116
|
+
start,
|
|
1117
|
+
stop,
|
|
1118
|
+
step,
|
|
1119
|
+
flush,
|
|
1120
|
+
runAutoLayout,
|
|
1121
|
+
]);
|
|
1122
|
+
return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function IssueBadge({ level, title, size = 12, className, }) {
|
|
1126
|
+
const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
|
|
1127
|
+
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" })) }));
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWorkbenchChange, }) {
|
|
1131
|
+
const { events, clearEvents } = useWorkbenchContext();
|
|
1132
|
+
const scrollRef = React.useRef(null);
|
|
1133
|
+
const rows = React.useMemo(() => {
|
|
1134
|
+
const filtered = hideWorkbench
|
|
1135
|
+
? events.filter((e) => e.source !== "workbench")
|
|
1136
|
+
: events;
|
|
1137
|
+
return filtered.slice().reverse();
|
|
1138
|
+
}, [events, hideWorkbench]);
|
|
1139
|
+
React.useEffect(() => {
|
|
1140
|
+
if (!autoScroll)
|
|
1141
|
+
return;
|
|
1142
|
+
const el = scrollRef.current;
|
|
1143
|
+
if (!el)
|
|
1144
|
+
return;
|
|
1145
|
+
el.scrollTop = el.scrollHeight;
|
|
1146
|
+
}, [rows, autoScroll]);
|
|
1147
|
+
const renderPayload = (v) => {
|
|
1148
|
+
try {
|
|
1149
|
+
return JSON.stringify(v, null, 0);
|
|
1469
1150
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
throw new Error("WebSocketTransport not available");
|
|
1473
|
-
transport = new sparkRemote.WebSocketTransport(this.backend.url);
|
|
1474
|
-
await transport.connect();
|
|
1151
|
+
catch {
|
|
1152
|
+
return String(v);
|
|
1475
1153
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1154
|
+
};
|
|
1155
|
+
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}`))) })] }));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, setInput, }) {
|
|
1159
|
+
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
|
|
1160
|
+
const nodeValidationIssues = validationByNode.issues;
|
|
1161
|
+
const edgeValidationIssues = validationByEdge.issues;
|
|
1162
|
+
const nodeValidationHandles = validationByNode;
|
|
1163
|
+
const globalValidationIssues = validationGlobal;
|
|
1164
|
+
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
1165
|
+
const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
|
|
1166
|
+
const selectedDesc = selectedNode
|
|
1167
|
+
? registry.nodes.get(selectedNode.typeId)
|
|
1168
|
+
: undefined;
|
|
1169
|
+
const inputHandles = Object.keys(selectedDesc?.inputs ?? {});
|
|
1170
|
+
const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
|
|
1171
|
+
const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
|
|
1172
|
+
const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
|
|
1173
|
+
const selectedNodeStatus = selectedNodeId
|
|
1174
|
+
? nodeStatus?.[selectedNodeId]
|
|
1175
|
+
: undefined;
|
|
1176
|
+
const selectedNodeValidation = selectedNodeId
|
|
1177
|
+
? nodeValidationIssues?.[selectedNodeId] ?? []
|
|
1178
|
+
: [];
|
|
1179
|
+
const selectedEdgeValidation = selectedEdge
|
|
1180
|
+
? edgeValidationIssues?.[selectedEdge.id] ?? []
|
|
1181
|
+
: [];
|
|
1182
|
+
const selectedNodeHandleValidation = selectedNodeId
|
|
1183
|
+
? {
|
|
1184
|
+
inputs: nodeValidationHandles?.inputs?.[selectedNodeId] ?? [],
|
|
1185
|
+
outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
|
|
1186
|
+
}
|
|
1187
|
+
: { inputs: [], outputs: [] };
|
|
1188
|
+
// Local drafts and originals for commit-on-blur/enter behavior
|
|
1189
|
+
const [drafts, setDrafts] = React.useState({});
|
|
1190
|
+
const [originals, setOriginals] = React.useState({});
|
|
1191
|
+
// Initialize drafts from current inputs whenever selection or valuesTick change,
|
|
1192
|
+
// but do not clobber fields currently being edited (dirty drafts)
|
|
1193
|
+
React.useEffect(() => {
|
|
1194
|
+
const shallowEqual = (a, b) => {
|
|
1195
|
+
const ak = Object.keys(a);
|
|
1196
|
+
const bk = Object.keys(b);
|
|
1197
|
+
if (ak.length !== bk.length)
|
|
1198
|
+
return false;
|
|
1199
|
+
for (const k of ak)
|
|
1200
|
+
if (a[k] !== b[k])
|
|
1201
|
+
return false;
|
|
1202
|
+
return true;
|
|
1203
|
+
};
|
|
1204
|
+
if (!selectedNodeId) {
|
|
1205
|
+
if (Object.keys(drafts).length || Object.keys(originals).length) {
|
|
1206
|
+
setDrafts({});
|
|
1207
|
+
setOriginals({});
|
|
1208
|
+
}
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const desc = selectedDesc;
|
|
1212
|
+
const handles = Object.keys(desc?.inputs ?? {});
|
|
1213
|
+
const nextDrafts = { ...drafts };
|
|
1214
|
+
const nextOriginals = { ...originals };
|
|
1215
|
+
for (const h of handles) {
|
|
1216
|
+
const typeId = desc?.inputs?.[h];
|
|
1217
|
+
const current = nodeInputs[h];
|
|
1218
|
+
const display = toString(typeId, current);
|
|
1219
|
+
const wasOriginal = originals[h];
|
|
1220
|
+
const isDirty = drafts[h] !== undefined &&
|
|
1221
|
+
wasOriginal !== undefined &&
|
|
1222
|
+
drafts[h] !== wasOriginal;
|
|
1223
|
+
if (!isDirty) {
|
|
1224
|
+
nextDrafts[h] = display;
|
|
1225
|
+
nextOriginals[h] = display;
|
|
1226
|
+
}
|
|
1478
1227
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1228
|
+
if (!shallowEqual(drafts, nextDrafts))
|
|
1229
|
+
setDrafts(nextDrafts);
|
|
1230
|
+
if (!shallowEqual(originals, nextOriginals))
|
|
1231
|
+
setOriginals(nextOriginals);
|
|
1232
|
+
}, [selectedNodeId, selectedDesc, valuesTick]);
|
|
1233
|
+
const widthClass = debug ? "w-[480px]" : "w-[320px]";
|
|
1234
|
+
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 ??
|
|
1235
|
+
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) => {
|
|
1236
|
+
const typeId = (selectedDesc?.inputs ?? {})[h];
|
|
1237
|
+
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
1238
|
+
e.target.handle === h);
|
|
1239
|
+
const commonProps = {
|
|
1240
|
+
style: { flex: 1 },
|
|
1241
|
+
disabled: isLinked,
|
|
1242
|
+
};
|
|
1243
|
+
const current = nodeInputs[h];
|
|
1244
|
+
const value = drafts[h] ?? toString(typeId, current);
|
|
1245
|
+
const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
|
|
1246
|
+
const commit = () => {
|
|
1247
|
+
const draft = drafts[h];
|
|
1248
|
+
if (draft === undefined)
|
|
1249
|
+
return;
|
|
1250
|
+
setInput(h, draft);
|
|
1251
|
+
setOriginals((o) => ({ ...o, [h]: draft }));
|
|
1252
|
+
};
|
|
1253
|
+
const revert = () => {
|
|
1254
|
+
const orig = originals[h] ?? toString(typeId, current);
|
|
1255
|
+
setDrafts((d) => ({ ...d, [h]: orig }));
|
|
1256
|
+
};
|
|
1257
|
+
const isEnum = typeId?.includes("enum:");
|
|
1258
|
+
const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
|
|
1259
|
+
const hasValidation = inIssues.length > 0;
|
|
1260
|
+
const hasErr = inIssues.some((m) => m.level === "error");
|
|
1261
|
+
const title = inIssues
|
|
1262
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
1263
|
+
.join("; ");
|
|
1264
|
+
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] ?? toString(typeId, current), onChange: (e) => {
|
|
1265
|
+
const label = String(e.target.value);
|
|
1266
|
+
const byLabel = registry.enums
|
|
1267
|
+
.get(typeId)
|
|
1268
|
+
?.labelToValue.get(label.toLowerCase());
|
|
1269
|
+
let raw = (byLabel !== undefined ? byLabel : Number(label));
|
|
1270
|
+
if (!Number.isFinite(raw))
|
|
1271
|
+
raw = undefined;
|
|
1272
|
+
setInput(h, raw);
|
|
1273
|
+
const display = toString(typeId, raw);
|
|
1274
|
+
setDrafts((d) => ({ ...d, [h]: display }));
|
|
1275
|
+
setOriginals((o) => ({ ...o, [h]: display }));
|
|
1276
|
+
}, ...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) => {
|
|
1277
|
+
if (e.key === "Enter")
|
|
1278
|
+
commit();
|
|
1279
|
+
if (e.key === "Escape")
|
|
1280
|
+
revert();
|
|
1281
|
+
}, ...commonProps }))] }, h));
|
|
1282
|
+
}))] }), 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]) }), (() => {
|
|
1283
|
+
const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
|
|
1284
|
+
if (outIssues.length === 0)
|
|
1285
|
+
return null;
|
|
1286
|
+
const outErr = outIssues.some((m) => m.level === "error");
|
|
1287
|
+
const outTitle = outIssues
|
|
1288
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
1289
|
+
.join("; ");
|
|
1290
|
+
return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
|
|
1291
|
+
})()] }, 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 }) }))] }));
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function toReactFlow(def, positions, registry, selectedNodeIds, selectedEdgeIds, opts) {
|
|
1295
|
+
const nodeHandleMap = {};
|
|
1296
|
+
const nodes = def.nodes.map((n) => {
|
|
1297
|
+
const desc = registry.nodes.get(n.typeId);
|
|
1298
|
+
const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
1299
|
+
const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
1300
|
+
nodeHandleMap[n.nodeId] = {
|
|
1301
|
+
inputs: new Set(inputHandles.map((h) => h.id)),
|
|
1302
|
+
outputs: new Set(outputHandles.map((h) => h.id)),
|
|
1485
1303
|
};
|
|
1486
|
-
return
|
|
1487
|
-
|
|
1304
|
+
return {
|
|
1305
|
+
id: n.nodeId,
|
|
1306
|
+
data: {
|
|
1307
|
+
typeId: n.typeId,
|
|
1308
|
+
params: n.params,
|
|
1309
|
+
inputHandles,
|
|
1310
|
+
outputHandles,
|
|
1311
|
+
showValues: opts?.showValues,
|
|
1312
|
+
inputValues: opts?.inputs?.[n.nodeId],
|
|
1313
|
+
outputValues: opts?.outputs?.[n.nodeId],
|
|
1314
|
+
status: opts?.nodeStatus?.[n.nodeId],
|
|
1315
|
+
validation: {
|
|
1316
|
+
inputs: opts?.nodeValidation?.inputs?.[n.nodeId] ?? [],
|
|
1317
|
+
outputs: opts?.nodeValidation?.outputs?.[n.nodeId] ?? [],
|
|
1318
|
+
issues: opts?.nodeValidation?.issues?.[n.nodeId] ?? [],
|
|
1319
|
+
},
|
|
1320
|
+
toString: opts?.toString,
|
|
1321
|
+
toElement: opts?.toElement,
|
|
1322
|
+
},
|
|
1323
|
+
position: positions[n.nodeId] ?? { x: 0, y: 0 },
|
|
1324
|
+
type: opts?.resolveNodeType?.(n.typeId) ?? "spark:default",
|
|
1325
|
+
selected: selectedNodeIds ? selectedNodeIds.has(n.nodeId) : undefined,
|
|
1326
|
+
};
|
|
1327
|
+
});
|
|
1328
|
+
const edges = def.edges
|
|
1329
|
+
.filter((e) => {
|
|
1330
|
+
const src = nodeHandleMap[e.source.nodeId];
|
|
1331
|
+
const dst = nodeHandleMap[e.target.nodeId];
|
|
1332
|
+
if (!src || !dst)
|
|
1333
|
+
return false;
|
|
1334
|
+
return (src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle));
|
|
1335
|
+
})
|
|
1336
|
+
.map((e) => {
|
|
1337
|
+
const st = opts?.edgeStatus?.[e.id];
|
|
1338
|
+
const isRunning = !!st?.running;
|
|
1339
|
+
const hasError = !!st?.lastError;
|
|
1340
|
+
const isInvalidEdge = !!opts?.edgeValidation?.[e.id];
|
|
1341
|
+
const style = hasError || isInvalidEdge
|
|
1342
|
+
? { stroke: "#ef4444", strokeWidth: 2 }
|
|
1343
|
+
: isRunning
|
|
1344
|
+
? { stroke: "#3b82f6" }
|
|
1345
|
+
: undefined;
|
|
1346
|
+
return {
|
|
1347
|
+
id: e.id,
|
|
1348
|
+
source: e.source.nodeId,
|
|
1349
|
+
target: e.target.nodeId,
|
|
1350
|
+
sourceHandle: e.source.handle,
|
|
1351
|
+
targetHandle: e.target.handle,
|
|
1352
|
+
selected: selectedEdgeIds ? selectedEdgeIds.has(e.id) : undefined,
|
|
1353
|
+
animated: isRunning,
|
|
1354
|
+
style,
|
|
1355
|
+
};
|
|
1356
|
+
});
|
|
1357
|
+
return { nodes, edges };
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
|
|
1361
|
+
const { typeId, showValues, inputValues, outputValues, toString, toElement, } = data;
|
|
1362
|
+
const inputEntries = data.inputHandles ?? [];
|
|
1363
|
+
const outputEntries = data.outputHandles ?? [];
|
|
1364
|
+
const status = data.status ?? {};
|
|
1365
|
+
const validation = data.validation ?? {
|
|
1366
|
+
inputs: [],
|
|
1367
|
+
outputs: [],
|
|
1368
|
+
issues: [],
|
|
1369
|
+
};
|
|
1370
|
+
const HEADER_SIZE = 24;
|
|
1371
|
+
const ROW_SIZE = 22;
|
|
1372
|
+
const maxRows = Math.max(inputEntries.length, outputEntries.length);
|
|
1373
|
+
const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
|
|
1374
|
+
const minWidth = data.showValues ? 320 : 160;
|
|
1375
|
+
const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
|
|
1376
|
+
const hasError = !!status.lastError;
|
|
1377
|
+
const isRunning = !!status.running;
|
|
1378
|
+
const isInvalid = !!status.invalidated && !isRunning && !hasError;
|
|
1379
|
+
const borderClasses = selected
|
|
1380
|
+
? "border-2 border-gray-900 dark:border-gray-100"
|
|
1381
|
+
: hasError
|
|
1382
|
+
? "border-2 border-red-500"
|
|
1383
|
+
: isRunning
|
|
1384
|
+
? "border-2 border-blue-500 ring-2 ring-blue-200 dark:ring-blue-900"
|
|
1385
|
+
: isInvalid
|
|
1386
|
+
? "border-2 border-amber-500 border-dashed"
|
|
1387
|
+
: "border border-gray-500 dark:border-gray-400";
|
|
1388
|
+
const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
|
|
1389
|
+
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")
|
|
1390
|
+
? "error"
|
|
1391
|
+
: "warning", size: 12, className: "w-3 h-3", title: validation.issues
|
|
1392
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
1393
|
+
.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) => {
|
|
1394
|
+
const vIssues = validation.inputs.filter((v) => v.handle === entry.id);
|
|
1395
|
+
const hasAny = vIssues.length > 0;
|
|
1396
|
+
const hasErr = vIssues.some((v) => v.level === "error");
|
|
1397
|
+
const title = vIssues
|
|
1398
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
1399
|
+
.join("; ");
|
|
1400
|
+
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 &&
|
|
1401
|
+
(toElement ? (toElement(entry.typeId, inputValues?.[entry.id])) : toString ? (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) })) : (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: String(inputValues?.[entry.id]) })))] })] }, `in-${entry.id}`));
|
|
1402
|
+
}), outputEntries.map((entry, i) => {
|
|
1403
|
+
const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
|
|
1404
|
+
const hasAny = vIssues.length > 0;
|
|
1405
|
+
const hasErr = vIssues.some((v) => v.level === "error");
|
|
1406
|
+
const title = vIssues
|
|
1407
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
1408
|
+
.join("; ");
|
|
1409
|
+
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 &&
|
|
1410
|
+
(toElement ? (toElement(entry.typeId, outputValues?.[entry.id])) : toString ? (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, outputValues?.[entry.id]) })) : (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: String(outputValues?.[entry.id]) })))] })] }, `out-${entry.id}`));
|
|
1411
|
+
})] }));
|
|
1412
|
+
});
|
|
1413
|
+
DefaultNode.displayName = "DefaultNode";
|
|
1414
|
+
|
|
1415
|
+
function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
|
|
1416
|
+
const { registry } = useWorkbenchContext();
|
|
1417
|
+
const rf = ReactFlow.useReactFlow();
|
|
1418
|
+
if (!open || !clientPos)
|
|
1419
|
+
return null;
|
|
1420
|
+
const items = Array.from(registry.nodes.keys());
|
|
1421
|
+
const handleClick = (typeId) => {
|
|
1422
|
+
const p = rf.project({ x: clientPos.x, y: clientPos.y });
|
|
1423
|
+
onAdd(typeId, p);
|
|
1424
|
+
onClose();
|
|
1425
|
+
};
|
|
1426
|
+
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))) })] }));
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
1430
|
+
const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
|
|
1431
|
+
const ioValues = { inputs: inputsMap, outputs: outputsMap };
|
|
1432
|
+
const nodeValidation = validationByNode;
|
|
1433
|
+
const edgeValidation = validationByEdge.errors;
|
|
1434
|
+
const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
|
|
1435
|
+
const { nodeTypes, resolveNodeType } = React.useMemo(() => {
|
|
1436
|
+
// Build nodeTypes map using UI extension registry
|
|
1437
|
+
const ui = wb.getUI();
|
|
1438
|
+
const custom = new Map();
|
|
1439
|
+
for (const typeId of Array.from(registry.nodes.keys())) {
|
|
1440
|
+
const renderer = ui.getNodeRenderer(typeId);
|
|
1441
|
+
if (renderer)
|
|
1442
|
+
custom.set(typeId, renderer);
|
|
1443
|
+
}
|
|
1444
|
+
const types = { "spark:default": DefaultNode };
|
|
1445
|
+
for (const [typeId, comp] of custom.entries()) {
|
|
1446
|
+
types[`spark:${typeId}`] = comp;
|
|
1447
|
+
}
|
|
1448
|
+
const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark:${nodeTypeId}` : "spark:default";
|
|
1449
|
+
return { nodeTypes: types, resolveNodeType: resolver };
|
|
1450
|
+
// registry is stable; ui renderers expected to be set up before mount
|
|
1451
|
+
}, [wb, registry]);
|
|
1452
|
+
const { nodes, edges } = React.useMemo(() => {
|
|
1453
|
+
const def = wb.export();
|
|
1454
|
+
const sel = wb.getSelection();
|
|
1455
|
+
return toReactFlow(def, wb.getPositions(), registry, new Set(sel.nodes), new Set(sel.edges), {
|
|
1456
|
+
showValues,
|
|
1457
|
+
inputs: ioValues.inputs,
|
|
1458
|
+
outputs: ioValues.outputs,
|
|
1459
|
+
resolveNodeType,
|
|
1460
|
+
toString,
|
|
1461
|
+
toElement,
|
|
1462
|
+
nodeStatus,
|
|
1463
|
+
edgeStatus,
|
|
1464
|
+
nodeValidation,
|
|
1465
|
+
edgeValidation,
|
|
1466
|
+
});
|
|
1467
|
+
}, [
|
|
1468
|
+
showValues,
|
|
1469
|
+
ioValues,
|
|
1470
|
+
valuesTick,
|
|
1471
|
+
toString,
|
|
1472
|
+
toElement,
|
|
1473
|
+
nodeStatus,
|
|
1474
|
+
edgeStatus,
|
|
1475
|
+
nodeValidation,
|
|
1476
|
+
edgeValidation,
|
|
1477
|
+
]);
|
|
1478
|
+
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
1479
|
+
const [menuPos, setMenuPos] = React.useState(null);
|
|
1480
|
+
const onContextMenu = (e) => {
|
|
1481
|
+
e.preventDefault();
|
|
1482
|
+
setMenuPos({ x: e.clientX, y: e.clientY });
|
|
1483
|
+
setMenuOpen(true);
|
|
1484
|
+
};
|
|
1485
|
+
const addNodeAt = (typeId, pos) => {
|
|
1486
|
+
wb.addNode({ typeId, position: pos });
|
|
1487
|
+
};
|
|
1488
|
+
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) })] }) }));
|
|
1488
1489
|
}
|
|
1489
1490
|
|
|
1490
|
-
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, }) {
|
|
1491
|
+
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, }) {
|
|
1491
1492
|
const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
|
|
1492
1493
|
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
1493
1494
|
const selectedDesc = selectedNode
|
|
@@ -1653,7 +1654,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1653
1654
|
runAutoLayout();
|
|
1654
1655
|
}
|
|
1655
1656
|
}, [wb, runAutoLayout]);
|
|
1656
|
-
const
|
|
1657
|
+
const baseSetInput = React.useCallback((handle, raw) => {
|
|
1657
1658
|
if (!selectedNodeId)
|
|
1658
1659
|
return;
|
|
1659
1660
|
// If selected input is wired (has inbound edge), ignore user input to respect runtime value
|
|
@@ -1734,7 +1735,17 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1734
1735
|
}
|
|
1735
1736
|
runner.setInput(selectedNodeId, handle, value);
|
|
1736
1737
|
}, [selectedNodeId, def.edges, selectedDesc, runner]);
|
|
1737
|
-
const
|
|
1738
|
+
const setInput = React.useMemo(() => {
|
|
1739
|
+
if (overrides?.setInput) {
|
|
1740
|
+
return overrides.setInput(baseSetInput, {
|
|
1741
|
+
runner,
|
|
1742
|
+
selectedNodeId,
|
|
1743
|
+
registry,
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
return baseSetInput;
|
|
1747
|
+
}, [overrides, baseSetInput, runner, selectedNodeId, registry]);
|
|
1748
|
+
const baseToString = React.useCallback((typeId, value) => {
|
|
1738
1749
|
if (value === undefined || value === null)
|
|
1739
1750
|
return "";
|
|
1740
1751
|
if (typeId && typeId.includes("enum:")) {
|
|
@@ -1766,6 +1777,21 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1766
1777
|
}
|
|
1767
1778
|
return String(value);
|
|
1768
1779
|
}, [registry]);
|
|
1780
|
+
const baseToElement = React.useCallback((typeId, value) => {
|
|
1781
|
+
return jsxRuntime.jsx("span", { children: baseToString(typeId, value) });
|
|
1782
|
+
}, [baseToString]);
|
|
1783
|
+
const toString = React.useMemo(() => {
|
|
1784
|
+
if (overrides?.toString)
|
|
1785
|
+
return overrides.toString(baseToString, { registry });
|
|
1786
|
+
return baseToString;
|
|
1787
|
+
}, [overrides, baseToString, registry]);
|
|
1788
|
+
// Optional: toElement (not currently consumed by core UI)
|
|
1789
|
+
// Consumers can access it by passing through their own node renderers.
|
|
1790
|
+
const toElement = React.useMemo(() => {
|
|
1791
|
+
if (overrides?.toElement)
|
|
1792
|
+
return overrides.toElement(baseToElement, { registry });
|
|
1793
|
+
return baseToElement;
|
|
1794
|
+
}, [overrides, baseToElement, registry]);
|
|
1769
1795
|
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
1796
|
? "Stop engine before switching example"
|
|
1771
1797
|
: 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 +1809,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1783
1809
|
catch (err) {
|
|
1784
1810
|
alert(String(err?.message ?? err));
|
|
1785
1811
|
}
|
|
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,
|
|
1812
|
+
}, 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 }, exampleState) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
|
|
1787
1813
|
}
|
|
1788
|
-
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, }) {
|
|
1814
|
+
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
|
|
1789
1815
|
const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
|
|
1790
1816
|
const [wb] = React.useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
|
|
1791
1817
|
const runner = React.useMemo(() => {
|
|
@@ -1796,35 +1822,36 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
1796
1822
|
: { kind: "local" };
|
|
1797
1823
|
return new GraphRunner(registry, backend);
|
|
1798
1824
|
}, [registry, backendKind, httpBaseUrl, wsUrl]);
|
|
1825
|
+
// Allow external UI registration (e.g., node renderers) with access to wb
|
|
1826
|
+
React.useEffect(() => {
|
|
1827
|
+
const baseRegisterUI = (_wb) => { };
|
|
1828
|
+
overrides?.registerUI?.(baseRegisterUI, { wb });
|
|
1829
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1830
|
+
}, [wb, overrides]);
|
|
1799
1831
|
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
1832
|
if (runner.isRunning())
|
|
1801
1833
|
runner.dispose();
|
|
1802
1834
|
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 }));
|
|
1835
|
+
}, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, overrides: overrides }) }));
|
|
1821
1836
|
}
|
|
1822
1837
|
|
|
1823
1838
|
exports.AbstractWorkbench = AbstractWorkbench;
|
|
1824
|
-
exports.App = App;
|
|
1825
1839
|
exports.CLIWorkbench = CLIWorkbench;
|
|
1826
1840
|
exports.DefaultUIExtensionRegistry = DefaultUIExtensionRegistry;
|
|
1841
|
+
exports.GraphRunner = GraphRunner;
|
|
1827
1842
|
exports.InMemoryWorkbench = InMemoryWorkbench;
|
|
1843
|
+
exports.Inspector = Inspector;
|
|
1828
1844
|
exports.ReactFlowWorkbench = ReactFlowWorkbench;
|
|
1845
|
+
exports.WorkbenchCanvas = WorkbenchCanvas;
|
|
1846
|
+
exports.WorkbenchContext = WorkbenchContext;
|
|
1847
|
+
exports.WorkbenchProvider = WorkbenchProvider;
|
|
1848
|
+
exports.WorkbenchStudio = WorkbenchStudio;
|
|
1829
1849
|
exports.toReactFlow = toReactFlow$1;
|
|
1850
|
+
exports.useQueryParamBoolean = useQueryParamBoolean;
|
|
1851
|
+
exports.useQueryParamString = useQueryParamString;
|
|
1852
|
+
exports.useWorkbenchBridge = useWorkbenchBridge;
|
|
1853
|
+
exports.useWorkbenchContext = useWorkbenchContext;
|
|
1854
|
+
exports.useWorkbenchGraphTick = useWorkbenchGraphTick;
|
|
1855
|
+
exports.useWorkbenchGraphUiTick = useWorkbenchGraphUiTick;
|
|
1856
|
+
exports.useWorkbenchVersionTick = useWorkbenchVersionTick;
|
|
1830
1857
|
//# sourceMappingURL=index.cjs.map
|