@bian-womp/spark-workbench 0.1.8 → 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 +1012 -976
- 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 +1002 -978
- 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,338 +350,612 @@ 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
|
-
|
|
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;
|
|
681
556
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
557
|
+
const merged = this.isRunning() ? cur : { ...cur, ...staged };
|
|
558
|
+
if (Object.keys(merged).length > 0)
|
|
559
|
+
out[n.nodeId] = merged;
|
|
560
|
+
}
|
|
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 });
|
|
588
|
+
}
|
|
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();
|
|
606
|
+
}
|
|
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();
|
|
612
|
+
}
|
|
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
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function useWorkbenchBridge(wb) {
|
|
628
|
+
const onConnect = React.useCallback((params) => {
|
|
629
|
+
if (!params.source || !params.target)
|
|
630
|
+
return;
|
|
631
|
+
if (!params.sourceHandle || !params.targetHandle)
|
|
632
|
+
return;
|
|
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,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
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]);
|
|
771
|
+
React.useEffect(() => {
|
|
772
|
+
const onPop = () => {
|
|
773
|
+
const sp = new URLSearchParams(window.location.search);
|
|
774
|
+
const v = sp.get(key) ?? undefined;
|
|
775
|
+
setVal(v);
|
|
776
|
+
};
|
|
777
|
+
window.addEventListener("popstate", onPop);
|
|
778
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
779
|
+
}, [key]);
|
|
780
|
+
return [val, set];
|
|
781
|
+
}
|
|
782
|
+
|
|
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] = [];
|
|
819
|
+
}
|
|
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);
|
|
823
|
+
}
|
|
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
|
+
}
|
|
836
|
+
}
|
|
837
|
+
layers.push(layer);
|
|
838
|
+
q.splice(0, q.length, ...next);
|
|
839
|
+
}
|
|
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
|
+
}));
|
|
884
|
+
}
|
|
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
|
+
}));
|
|
894
|
+
}
|
|
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;
|
|
905
|
+
});
|
|
906
|
+
}
|
|
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
|
+
}));
|
|
955
|
+
}
|
|
956
|
+
return add("runner", "stats")(s);
|
|
957
|
+
});
|
|
958
|
+
const off4 = wb.on("graphChanged", add("workbench", "graphChanged"));
|
|
685
959
|
const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
686
960
|
const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
687
961
|
const off5b = wb.on("validationChanged", (r) => setValidation(r));
|
|
@@ -833,220 +1107,24 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
833
1107
|
inputsMap,
|
|
834
1108
|
outputsMap,
|
|
835
1109
|
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) ?? "@bian-womp/spark:default",
|
|
881
|
-
selected: selectedNodeIds ? selectedNodeIds.has(n.nodeId) : undefined,
|
|
882
|
-
};
|
|
883
|
-
});
|
|
884
|
-
const edges = def.edges
|
|
885
|
-
.filter((e) => {
|
|
886
|
-
const src = nodeHandleMap[e.source.nodeId];
|
|
887
|
-
const dst = nodeHandleMap[e.target.nodeId];
|
|
888
|
-
if (!src || !dst)
|
|
889
|
-
return false;
|
|
890
|
-
return src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle);
|
|
891
|
-
})
|
|
892
|
-
.map((e) => {
|
|
893
|
-
const st = opts?.edgeStatus?.[e.id];
|
|
894
|
-
const isRunning = !!st?.running;
|
|
895
|
-
const hasError = !!st?.lastError;
|
|
896
|
-
const isInvalidEdge = !!opts?.edgeValidation?.[e.id];
|
|
897
|
-
const style = hasError || isInvalidEdge
|
|
898
|
-
? { stroke: "#ef4444", strokeWidth: 2 }
|
|
899
|
-
: isRunning
|
|
900
|
-
? { stroke: "#3b82f6" }
|
|
901
|
-
: undefined;
|
|
902
|
-
return {
|
|
903
|
-
id: e.id,
|
|
904
|
-
source: e.source.nodeId,
|
|
905
|
-
target: e.target.nodeId,
|
|
906
|
-
sourceHandle: e.source.handle,
|
|
907
|
-
targetHandle: e.target.handle,
|
|
908
|
-
selected: selectedEdgeIds ? selectedEdgeIds.has(e.id) : undefined,
|
|
909
|
-
animated: isRunning,
|
|
910
|
-
style,
|
|
911
|
-
};
|
|
912
|
-
});
|
|
913
|
-
return { nodes, edges };
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
function IssueBadge({ level, title, size = 12, className, }) {
|
|
917
|
-
const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
|
|
918
|
-
return (jsxRuntime.jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsxRuntime.jsx(react.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react.WarningCircleIcon, { size: size, weight: "fill" })) }));
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
|
|
922
|
-
const typeId = data.typeId;
|
|
923
|
-
const inputEntries = data.inputHandles ?? [];
|
|
924
|
-
const outputEntries = data.outputHandles ?? [];
|
|
925
|
-
const status = data.status ?? {};
|
|
926
|
-
const validation = data.validation ?? {
|
|
927
|
-
inputs: [],
|
|
928
|
-
outputs: [],
|
|
929
|
-
issues: [],
|
|
930
|
-
};
|
|
931
|
-
const HEADER_SIZE = 24;
|
|
932
|
-
const ROW_SIZE = 22;
|
|
933
|
-
const maxRows = Math.max(inputEntries.length, outputEntries.length);
|
|
934
|
-
const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
|
|
935
|
-
const minWidth = data.showValues ? 320 : 160;
|
|
936
|
-
const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
|
|
937
|
-
const hasError = !!status.lastError;
|
|
938
|
-
const isRunning = !!status.running;
|
|
939
|
-
const isInvalid = !!status.invalidated && !isRunning && !hasError;
|
|
940
|
-
const borderClasses = selected
|
|
941
|
-
? "border-2 border-gray-900 dark:border-gray-100"
|
|
942
|
-
: hasError
|
|
943
|
-
? "border-2 border-red-500"
|
|
944
|
-
: isRunning
|
|
945
|
-
? "border-2 border-blue-500 ring-2 ring-blue-200 dark:ring-blue-900"
|
|
946
|
-
: isInvalid
|
|
947
|
-
? "border-2 border-amber-500 border-dashed"
|
|
948
|
-
: "border border-gray-500 dark:border-gray-400";
|
|
949
|
-
const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
|
|
950
|
-
return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900 border-solid", borderClasses), style: { position: "relative", minHeight: minHeight, minWidth }, children: [jsxRuntime.jsxs("div", { className: "flex h-6 items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", children: [jsxRuntime.jsx("strong", { className: "flex-1 h-full leading-6 text-xs", children: typeId }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsxRuntime.jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsxRuntime.jsx(react.XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
|
|
951
|
-
? "error"
|
|
952
|
-
: "warning", size: 12, className: "w-3 h-3", title: validation.issues
|
|
953
|
-
.map((v) => `${v.code}: ${v.message}`)
|
|
954
|
-
.join("; ") })), jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }), (isRunning || pct > 0) && (jsxRuntime.jsx("div", { className: "h-1 bg-blue-200 dark:bg-blue-900", children: jsxRuntime.jsx("div", { className: "h-1 bg-blue-500 transition-all", style: { width: `${pct}%` } }) })), inputEntries.map((entry, i) => {
|
|
955
|
-
const vIssues = validation.inputs.filter((v) => v.handle === entry.id);
|
|
956
|
-
const hasAny = vIssues.length > 0;
|
|
957
|
-
const hasErr = vIssues.some((v) => v.level === "error");
|
|
958
|
-
const title = vIssues
|
|
959
|
-
.map((v) => `${v.code}: ${v.message}`)
|
|
960
|
-
.join("; ");
|
|
961
|
-
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8 }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), data.showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: data.toDisplay
|
|
962
|
-
? data.toDisplay(entry.typeId, data.inputValues?.[entry.id])
|
|
963
|
-
: String(data.inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
|
|
964
|
-
}), outputEntries.map((entry, i) => {
|
|
965
|
-
const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
|
|
966
|
-
const hasAny = vIssues.length > 0;
|
|
967
|
-
const hasErr = vIssues.some((v) => v.level === "error");
|
|
968
|
-
const title = vIssues
|
|
969
|
-
.map((v) => `${v.code}: ${v.message}`)
|
|
970
|
-
.join("; ");
|
|
971
|
-
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8, textAlign: "right" }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), data.showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: data.toDisplay
|
|
972
|
-
? data.toDisplay(entry.typeId, data.outputValues?.[entry.id])
|
|
973
|
-
: String(data.outputValues?.[entry.id]) }))] })] }, `out-${entry.id}`));
|
|
974
|
-
})] }));
|
|
975
|
-
});
|
|
976
|
-
DefaultNode.displayName = "DefaultNode";
|
|
977
|
-
|
|
978
|
-
function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
|
|
979
|
-
const { registry } = useWorkbenchContext();
|
|
980
|
-
const rf = ReactFlow.useReactFlow();
|
|
981
|
-
if (!open || !clientPos)
|
|
982
|
-
return null;
|
|
983
|
-
const items = Array.from(registry.nodes.keys());
|
|
984
|
-
const handleClick = (typeId) => {
|
|
985
|
-
const p = rf.project({ x: clientPos.x, y: clientPos.y });
|
|
986
|
-
onAdd(typeId, p);
|
|
987
|
-
onClose();
|
|
988
|
-
};
|
|
989
|
-
return (jsxRuntime.jsxs("div", { className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: clientPos.x, top: clientPos.y }, onMouseLeave: onClose, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Add Node" }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: items.map((id) => (jsxRuntime.jsx("button", { onClick: () => handleClick(id), className: "block w-full text-left px-2 py-1 hover:bg-gray-100 cursor-pointer", children: id }, id))) })] }));
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
function WorkbenchCanvas({ showValues, toDisplay, }) {
|
|
993
|
-
const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
|
|
994
|
-
const ioValues = { inputs: inputsMap, outputs: outputsMap };
|
|
995
|
-
const nodeValidation = validationByNode;
|
|
996
|
-
const edgeValidation = validationByEdge.errors;
|
|
997
|
-
const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
|
|
998
|
-
const { nodeTypes, resolveNodeType } = React.useMemo(() => {
|
|
999
|
-
// Build nodeTypes map using UI extension registry
|
|
1000
|
-
const ui = wb.getUI();
|
|
1001
|
-
const custom = new Map();
|
|
1002
|
-
for (const typeId of Array.from(registry.nodes.keys())) {
|
|
1003
|
-
const renderer = ui.getNodeRenderer(typeId);
|
|
1004
|
-
if (renderer)
|
|
1005
|
-
custom.set(typeId, renderer);
|
|
1006
|
-
}
|
|
1007
|
-
const types = { "@bian-womp/spark:default": DefaultNode };
|
|
1008
|
-
for (const [typeId, comp] of custom.entries()) {
|
|
1009
|
-
types[`spark:${typeId}`] = comp;
|
|
1010
|
-
}
|
|
1011
|
-
const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark:${nodeTypeId}` : "@bian-womp/spark:default";
|
|
1012
|
-
return { nodeTypes: types, resolveNodeType: resolver };
|
|
1013
|
-
// registry is stable; ui renderers expected to be set up before mount
|
|
1014
|
-
}, [wb, registry]);
|
|
1015
|
-
const { nodes, edges } = React.useMemo(() => {
|
|
1016
|
-
const def = wb.export();
|
|
1017
|
-
const sel = wb.getSelection();
|
|
1018
|
-
return toReactFlow(def, wb.getPositions(), registry, new Set(sel.nodes), new Set(sel.edges), {
|
|
1019
|
-
showValues,
|
|
1020
|
-
inputs: ioValues.inputs,
|
|
1021
|
-
outputs: ioValues.outputs,
|
|
1022
|
-
resolveNodeType,
|
|
1023
|
-
toDisplay,
|
|
1024
|
-
nodeStatus,
|
|
1025
|
-
edgeStatus,
|
|
1026
|
-
nodeValidation,
|
|
1027
|
-
edgeValidation,
|
|
1028
|
-
});
|
|
1029
|
-
}, [
|
|
1030
|
-
showValues,
|
|
1031
|
-
ioValues,
|
|
1032
|
-
valuesTick,
|
|
1033
|
-
toDisplay,
|
|
1034
|
-
nodeStatus,
|
|
1035
|
-
edgeStatus,
|
|
1036
|
-
nodeValidation,
|
|
1037
|
-
edgeValidation,
|
|
1110
|
+
validationByEdge,
|
|
1111
|
+
validationGlobal,
|
|
1112
|
+
events,
|
|
1113
|
+
clearEvents,
|
|
1114
|
+
isRunning,
|
|
1115
|
+
engineKind,
|
|
1116
|
+
start,
|
|
1117
|
+
stop,
|
|
1118
|
+
step,
|
|
1119
|
+
flush,
|
|
1120
|
+
runAutoLayout,
|
|
1038
1121
|
]);
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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) })] }) }));
|
|
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" })) }));
|
|
1050
1128
|
}
|
|
1051
1129
|
|
|
1052
1130
|
function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWorkbenchChange, }) {
|
|
@@ -1057,437 +1135,360 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
|
|
|
1057
1135
|
? events.filter((e) => e.source !== "workbench")
|
|
1058
1136
|
: events;
|
|
1059
1137
|
return filtered.slice().reverse();
|
|
1060
|
-
}, [events, hideWorkbench]);
|
|
1061
|
-
React.useEffect(() => {
|
|
1062
|
-
if (!autoScroll)
|
|
1063
|
-
return;
|
|
1064
|
-
const el = scrollRef.current;
|
|
1065
|
-
if (!el)
|
|
1066
|
-
return;
|
|
1067
|
-
el.scrollTop = el.scrollHeight;
|
|
1068
|
-
}, [rows, autoScroll]);
|
|
1069
|
-
const renderPayload = (v) => {
|
|
1070
|
-
try {
|
|
1071
|
-
return JSON.stringify(v, null, 0);
|
|
1072
|
-
}
|
|
1073
|
-
catch {
|
|
1074
|
-
return String(v);
|
|
1075
|
-
}
|
|
1076
|
-
};
|
|
1077
|
-
return (jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Events" }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Hide workbench" })] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Auto scroll" })] }), jsxRuntime.jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsxRuntime.jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev, idx) => (jsxRuntime.jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [jsxRuntime.jsx("span", { className: "w-8 shrink-0 text-right text-gray-500 select-none", children: idx + 1 }), jsxRuntime.jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsxRuntime.jsx("pre", { className: "m-0 whitespace-pre-wrap ml-10", children: renderPayload(ev.payload) })] }, `${ev.at}:${idx}`))) })] }));
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toDisplay, setInput, }) {
|
|
1081
|
-
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
|
|
1082
|
-
const nodeValidationIssues = validationByNode.issues;
|
|
1083
|
-
const edgeValidationIssues = validationByEdge.issues;
|
|
1084
|
-
const nodeValidationHandles = validationByNode;
|
|
1085
|
-
const globalValidationIssues = validationGlobal;
|
|
1086
|
-
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
1087
|
-
const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
|
|
1088
|
-
const selectedDesc = selectedNode
|
|
1089
|
-
? registry.nodes.get(selectedNode.typeId)
|
|
1090
|
-
: undefined;
|
|
1091
|
-
const inputHandles = Object.keys(selectedDesc?.inputs ?? {});
|
|
1092
|
-
const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
|
|
1093
|
-
const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
|
|
1094
|
-
const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
|
|
1095
|
-
const selectedNodeStatus = selectedNodeId
|
|
1096
|
-
? nodeStatus?.[selectedNodeId]
|
|
1097
|
-
: undefined;
|
|
1098
|
-
const selectedNodeValidation = selectedNodeId
|
|
1099
|
-
? nodeValidationIssues?.[selectedNodeId] ?? []
|
|
1100
|
-
: [];
|
|
1101
|
-
const selectedEdgeValidation = selectedEdge
|
|
1102
|
-
? edgeValidationIssues?.[selectedEdge.id] ?? []
|
|
1103
|
-
: [];
|
|
1104
|
-
const selectedNodeHandleValidation = selectedNodeId
|
|
1105
|
-
? {
|
|
1106
|
-
inputs: nodeValidationHandles?.inputs?.[selectedNodeId] ?? [],
|
|
1107
|
-
outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
|
|
1108
|
-
}
|
|
1109
|
-
: { inputs: [], outputs: [] };
|
|
1110
|
-
// Local drafts and originals for commit-on-blur/enter behavior
|
|
1111
|
-
const [drafts, setDrafts] = React.useState({});
|
|
1112
|
-
const [originals, setOriginals] = React.useState({});
|
|
1113
|
-
// Initialize drafts from current inputs whenever selection or valuesTick change,
|
|
1114
|
-
// but do not clobber fields currently being edited (dirty drafts)
|
|
1115
|
-
React.useEffect(() => {
|
|
1116
|
-
const shallowEqual = (a, b) => {
|
|
1117
|
-
const ak = Object.keys(a);
|
|
1118
|
-
const bk = Object.keys(b);
|
|
1119
|
-
if (ak.length !== bk.length)
|
|
1120
|
-
return false;
|
|
1121
|
-
for (const k of ak)
|
|
1122
|
-
if (a[k] !== b[k])
|
|
1123
|
-
return false;
|
|
1124
|
-
return true;
|
|
1125
|
-
};
|
|
1126
|
-
if (!selectedNodeId) {
|
|
1127
|
-
if (Object.keys(drafts).length || Object.keys(originals).length) {
|
|
1128
|
-
setDrafts({});
|
|
1129
|
-
setOriginals({});
|
|
1130
|
-
}
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
const desc = selectedDesc;
|
|
1134
|
-
const handles = Object.keys(desc?.inputs ?? {});
|
|
1135
|
-
const nextDrafts = { ...drafts };
|
|
1136
|
-
const nextOriginals = { ...originals };
|
|
1137
|
-
for (const h of handles) {
|
|
1138
|
-
const typeId = desc?.inputs?.[h];
|
|
1139
|
-
const current = nodeInputs[h];
|
|
1140
|
-
const display = toDisplay(typeId, current);
|
|
1141
|
-
const wasOriginal = originals[h];
|
|
1142
|
-
const isDirty = drafts[h] !== undefined &&
|
|
1143
|
-
wasOriginal !== undefined &&
|
|
1144
|
-
drafts[h] !== wasOriginal;
|
|
1145
|
-
if (!isDirty) {
|
|
1146
|
-
nextDrafts[h] = display;
|
|
1147
|
-
nextOriginals[h] = display;
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
if (!shallowEqual(drafts, nextDrafts))
|
|
1151
|
-
setDrafts(nextDrafts);
|
|
1152
|
-
if (!shallowEqual(originals, nextOriginals))
|
|
1153
|
-
setOriginals(nextOriginals);
|
|
1154
|
-
}, [selectedNodeId, selectedDesc, valuesTick]);
|
|
1155
|
-
const widthClass = debug ? "w-[480px]" : "w-[320px]";
|
|
1156
|
-
return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
|
|
1157
|
-
selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
|
|
1158
|
-
const typeId = (selectedDesc?.inputs ?? {})[h];
|
|
1159
|
-
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
1160
|
-
e.target.handle === h);
|
|
1161
|
-
const commonProps = {
|
|
1162
|
-
style: { flex: 1 },
|
|
1163
|
-
disabled: isLinked,
|
|
1164
|
-
};
|
|
1165
|
-
const current = nodeInputs[h];
|
|
1166
|
-
const value = drafts[h] ?? toDisplay(typeId, current);
|
|
1167
|
-
const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
|
|
1168
|
-
const commit = () => {
|
|
1169
|
-
const draft = drafts[h];
|
|
1170
|
-
if (draft === undefined)
|
|
1171
|
-
return;
|
|
1172
|
-
setInput(h, draft);
|
|
1173
|
-
setOriginals((o) => ({ ...o, [h]: draft }));
|
|
1174
|
-
};
|
|
1175
|
-
const revert = () => {
|
|
1176
|
-
const orig = originals[h] ?? toDisplay(typeId, current);
|
|
1177
|
-
setDrafts((d) => ({ ...d, [h]: orig }));
|
|
1178
|
-
};
|
|
1179
|
-
const isEnum = typeId?.includes("enum:");
|
|
1180
|
-
const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
|
|
1181
|
-
const hasValidation = inIssues.length > 0;
|
|
1182
|
-
const hasErr = inIssues.some((m) => m.level === "error");
|
|
1183
|
-
const title = inIssues
|
|
1184
|
-
.map((v) => `${v.code}: ${v.message}`)
|
|
1185
|
-
.join("; ");
|
|
1186
|
-
return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-28", children: [h, jsxRuntime.jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value: drafts[h] ?? toDisplay(typeId, current), onChange: (e) => {
|
|
1187
|
-
const label = String(e.target.value);
|
|
1188
|
-
const byLabel = registry.enums
|
|
1189
|
-
.get(typeId)
|
|
1190
|
-
?.labelToValue.get(label.toLowerCase());
|
|
1191
|
-
let raw = (byLabel !== undefined ? byLabel : Number(label));
|
|
1192
|
-
if (!Number.isFinite(raw))
|
|
1193
|
-
raw = undefined;
|
|
1194
|
-
setInput(h, raw);
|
|
1195
|
-
const display = toDisplay(typeId, raw);
|
|
1196
|
-
setDrafts((d) => ({ ...d, [h]: display }));
|
|
1197
|
-
setOriginals((o) => ({ ...o, [h]: display }));
|
|
1198
|
-
}, ...commonProps, children: [jsxRuntime.jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsxRuntime.jsx("option", { value: opt.label, children: opt.label }, opt.value)))] })) : (jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", placeholder: isLinked ? "wired" : undefined, value: value, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
|
|
1199
|
-
if (e.key === "Enter")
|
|
1200
|
-
commit();
|
|
1201
|
-
if (e.key === "Escape")
|
|
1202
|
-
revert();
|
|
1203
|
-
}, ...commonProps }))] }, h));
|
|
1204
|
-
}))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsx("label", { className: "w-20", children: h }), jsxRuntime.jsx("div", { className: "flex-1", children: toDisplay(selectedDesc?.outputs?.[h], nodeOutputs[h]) }), (() => {
|
|
1205
|
-
const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
|
|
1206
|
-
if (outIssues.length === 0)
|
|
1207
|
-
return null;
|
|
1208
|
-
const outErr = outIssues.some((m) => m.level === "error");
|
|
1209
|
-
const outTitle = outIssues
|
|
1210
|
-
.map((v) => `${v.code}: ${v.message}`)
|
|
1211
|
-
.join("; ");
|
|
1212
|
-
return (jsxRuntime.jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
|
|
1213
|
-
})()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) }), debug && (jsxRuntime.jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsxRuntime.jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
class GraphRunner {
|
|
1217
|
-
constructor(registry, backend) {
|
|
1218
|
-
this.registry = registry;
|
|
1219
|
-
this.listeners = new Map();
|
|
1220
|
-
this.stagedInputs = {};
|
|
1221
|
-
this.backend = { kind: "local" };
|
|
1222
|
-
if (backend)
|
|
1223
|
-
this.backend = backend;
|
|
1224
|
-
}
|
|
1225
|
-
build(def) {
|
|
1226
|
-
if (this.backend.kind === "local") {
|
|
1227
|
-
const builder = new sparkGraph.GraphBuilder(this.registry);
|
|
1228
|
-
this.runtime = builder.build(def);
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
// Remote: no-op here; build is performed on remote server during launch
|
|
1232
|
-
}
|
|
1233
|
-
update(def) {
|
|
1234
|
-
if (this.backend.kind === "local") {
|
|
1235
|
-
if (!this.runtime)
|
|
1236
|
-
return;
|
|
1237
|
-
this.runtime.update(def, this.registry);
|
|
1238
|
-
this.emit("invalidate", { reason: "graph-updated" });
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
// Remote: forward update; ignore errors (fire-and-forget)
|
|
1242
|
-
void this.ensureRemote().then(async (rc) => {
|
|
1243
|
-
try {
|
|
1244
|
-
await rc.runner.update(def);
|
|
1245
|
-
this.emit("invalidate", { reason: "graph-updated" });
|
|
1246
|
-
}
|
|
1247
|
-
catch { }
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
launch(def, opts) {
|
|
1251
|
-
if (this.engine) {
|
|
1252
|
-
throw new Error("Engine already running. Stop the current engine first.");
|
|
1253
|
-
}
|
|
1254
|
-
if (this.backend.kind === "local") {
|
|
1255
|
-
this.build(def);
|
|
1256
|
-
if (!this.runtime)
|
|
1257
|
-
throw new Error("Runtime not built");
|
|
1258
|
-
const rt = this.runtime;
|
|
1259
|
-
switch (opts.engine) {
|
|
1260
|
-
case "push":
|
|
1261
|
-
this.engine = new sparkGraph.PushEngine(rt);
|
|
1262
|
-
break;
|
|
1263
|
-
case "batched":
|
|
1264
|
-
this.engine = new sparkGraph.BatchedEngine(rt, {
|
|
1265
|
-
flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
|
|
1266
|
-
});
|
|
1267
|
-
break;
|
|
1268
|
-
case "pull":
|
|
1269
|
-
this.engine = new sparkGraph.PullEngine(rt);
|
|
1270
|
-
break;
|
|
1271
|
-
case "hybrid":
|
|
1272
|
-
this.engine = new sparkGraph.HybridEngine(rt, {
|
|
1273
|
-
windowMs: opts.hybrid?.windowMs ?? 250,
|
|
1274
|
-
batchThreshold: opts.hybrid?.batchThreshold ?? 3,
|
|
1275
|
-
});
|
|
1276
|
-
break;
|
|
1277
|
-
case "step":
|
|
1278
|
-
this.engine = new sparkGraph.StepEngine(rt);
|
|
1279
|
-
break;
|
|
1280
|
-
default:
|
|
1281
|
-
throw new Error("Unknown engine kind");
|
|
1282
|
-
}
|
|
1283
|
-
this.engine.on("value", (e) => this.emit("value", e));
|
|
1284
|
-
this.engine.on("error", (e) => this.emit("error", e));
|
|
1285
|
-
this.engine.on("invalidate", (e) => this.emit("invalidate", e));
|
|
1286
|
-
this.engine.on("stats", (e) => this.emit("stats", e));
|
|
1287
|
-
this.engine.launch();
|
|
1288
|
-
this.runningKind = opts.engine;
|
|
1289
|
-
this.emit("status", { running: true, engine: this.runningKind });
|
|
1290
|
-
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
1291
|
-
for (const [handle, value] of Object.entries(map)) {
|
|
1292
|
-
this.engine.setInput(nodeId, handle, value);
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1138
|
+
}, [events, hideWorkbench]);
|
|
1139
|
+
React.useEffect(() => {
|
|
1140
|
+
if (!autoScroll)
|
|
1141
|
+
return;
|
|
1142
|
+
const el = scrollRef.current;
|
|
1143
|
+
if (!el)
|
|
1295
1144
|
return;
|
|
1145
|
+
el.scrollTop = el.scrollHeight;
|
|
1146
|
+
}, [rows, autoScroll]);
|
|
1147
|
+
const renderPayload = (v) => {
|
|
1148
|
+
try {
|
|
1149
|
+
return JSON.stringify(v, null, 0);
|
|
1296
1150
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
await rc.runner.build(def);
|
|
1300
|
-
const eng = rc.runner.getEngine();
|
|
1301
|
-
if (!rc.listenersBound) {
|
|
1302
|
-
eng.on("value", (e) => {
|
|
1303
|
-
rc.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
1304
|
-
io: e.io,
|
|
1305
|
-
value: e.value,
|
|
1306
|
-
});
|
|
1307
|
-
this.emit("value", e);
|
|
1308
|
-
});
|
|
1309
|
-
eng.on("error", (e) => this.emit("error", e));
|
|
1310
|
-
eng.on("invalidate", (e) => this.emit("invalidate", e));
|
|
1311
|
-
eng.on("stats", (e) => this.emit("stats", e));
|
|
1312
|
-
rc.listenersBound = true;
|
|
1313
|
-
}
|
|
1314
|
-
this.engine = eng;
|
|
1315
|
-
this.engine.launch();
|
|
1316
|
-
this.runningKind = "push";
|
|
1317
|
-
this.emit("status", { running: true, engine: this.runningKind });
|
|
1318
|
-
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
1319
|
-
for (const [handle, value] of Object.entries(map)) {
|
|
1320
|
-
this.engine.setInput(nodeId, handle, value);
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
setInput(nodeId, handle, value) {
|
|
1326
|
-
if (!this.stagedInputs[nodeId])
|
|
1327
|
-
this.stagedInputs[nodeId] = {};
|
|
1328
|
-
this.stagedInputs[nodeId][handle] = value;
|
|
1329
|
-
if (this.engine)
|
|
1330
|
-
this.engine.setInput(nodeId, handle, value);
|
|
1331
|
-
}
|
|
1332
|
-
async step() {
|
|
1333
|
-
if (this.backend.kind !== "local")
|
|
1334
|
-
return; // unsupported remotely
|
|
1335
|
-
const eng = this.engine;
|
|
1336
|
-
if (eng instanceof sparkGraph.StepEngine)
|
|
1337
|
-
await eng.step();
|
|
1338
|
-
}
|
|
1339
|
-
async computeNode(nodeId) {
|
|
1340
|
-
if (this.backend.kind !== "local")
|
|
1341
|
-
return; // unsupported remotely
|
|
1342
|
-
const eng = this.engine;
|
|
1343
|
-
if (eng instanceof sparkGraph.PullEngine)
|
|
1344
|
-
await eng.computeNode(nodeId);
|
|
1345
|
-
}
|
|
1346
|
-
flush() {
|
|
1347
|
-
if (this.backend.kind !== "local")
|
|
1348
|
-
return; // unsupported remotely
|
|
1349
|
-
const eng = this.engine;
|
|
1350
|
-
if (eng instanceof sparkGraph.BatchedEngine)
|
|
1351
|
-
eng.flush();
|
|
1352
|
-
}
|
|
1353
|
-
getOutputs(def) {
|
|
1354
|
-
const out = {};
|
|
1355
|
-
if (this.backend.kind === "local") {
|
|
1356
|
-
if (!this.runtime)
|
|
1357
|
-
return out;
|
|
1358
|
-
for (const n of def.nodes) {
|
|
1359
|
-
const desc = this.registry.nodes.get(n.typeId);
|
|
1360
|
-
const handles = Object.keys(desc?.outputs ?? {});
|
|
1361
|
-
for (const h of handles) {
|
|
1362
|
-
const v = this.runtime.getOutput(n.nodeId, h);
|
|
1363
|
-
if (v !== undefined) {
|
|
1364
|
-
if (!out[n.nodeId])
|
|
1365
|
-
out[n.nodeId] = {};
|
|
1366
|
-
out[n.nodeId][h] = v;
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
return out;
|
|
1151
|
+
catch {
|
|
1152
|
+
return String(v);
|
|
1371
1153
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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] ?? [],
|
|
1387
1186
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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({});
|
|
1406
1208
|
}
|
|
1407
|
-
return
|
|
1209
|
+
return;
|
|
1408
1210
|
}
|
|
1409
|
-
const
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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;
|
|
1419
1226
|
}
|
|
1420
|
-
const merged = this.isRunning() ? cur : { ...cur, ...staged };
|
|
1421
|
-
if (Object.keys(merged).length > 0)
|
|
1422
|
-
out[n.nodeId] = merged;
|
|
1423
|
-
}
|
|
1424
|
-
return out;
|
|
1425
|
-
}
|
|
1426
|
-
async whenIdle() {
|
|
1427
|
-
await this.engine?.whenIdle();
|
|
1428
|
-
}
|
|
1429
|
-
on(event, handler) {
|
|
1430
|
-
if (!this.listeners.has(event))
|
|
1431
|
-
this.listeners.set(event, new Set());
|
|
1432
|
-
const set = this.listeners.get(event);
|
|
1433
|
-
set.add(handler);
|
|
1434
|
-
return () => set.delete(handler);
|
|
1435
|
-
}
|
|
1436
|
-
emit(event, payload) {
|
|
1437
|
-
const set = this.listeners.get(event);
|
|
1438
|
-
if (set)
|
|
1439
|
-
for (const h of Array.from(set))
|
|
1440
|
-
h(payload);
|
|
1441
|
-
}
|
|
1442
|
-
dispose() {
|
|
1443
|
-
this.engine?.dispose();
|
|
1444
|
-
this.engine = undefined;
|
|
1445
|
-
this.runtime?.dispose();
|
|
1446
|
-
this.runtime = undefined;
|
|
1447
|
-
this.remote = undefined;
|
|
1448
|
-
if (this.runningKind) {
|
|
1449
|
-
this.runningKind = undefined;
|
|
1450
|
-
this.emit("status", { running: false, engine: undefined });
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
isRunning() {
|
|
1454
|
-
return !!this.engine;
|
|
1455
|
-
}
|
|
1456
|
-
getRunningEngine() {
|
|
1457
|
-
return this.runningKind;
|
|
1458
|
-
}
|
|
1459
|
-
// Ensure remote transport/runner
|
|
1460
|
-
async ensureRemote() {
|
|
1461
|
-
if (this.remote)
|
|
1462
|
-
return this.remote;
|
|
1463
|
-
let transport;
|
|
1464
|
-
if (this.backend.kind === "remote-http") {
|
|
1465
|
-
if (!sparkRemote.HttpPollingTransport)
|
|
1466
|
-
throw new Error("HttpPollingTransport not available");
|
|
1467
|
-
transport = new sparkRemote.HttpPollingTransport(this.backend.baseUrl);
|
|
1468
|
-
await transport.connect();
|
|
1469
1227
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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)),
|
|
1303
|
+
};
|
|
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);
|
|
1475
1443
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1444
|
+
const types = { "spark:default": DefaultNode };
|
|
1445
|
+
for (const [typeId, comp] of custom.entries()) {
|
|
1446
|
+
types[`spark:${typeId}`] = comp;
|
|
1478
1447
|
}
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
return
|
|
1487
|
-
|
|
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
|
|
@@ -1582,7 +1583,16 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1582
1583
|
}
|
|
1583
1584
|
// Coercions (client-side no-op to satisfy validation) if provided
|
|
1584
1585
|
for (const c of desc.coercions) {
|
|
1585
|
-
|
|
1586
|
+
if (c.async) {
|
|
1587
|
+
r.registerAsyncCoercion(c.from, c.to, async (v) => v, {
|
|
1588
|
+
nonTransitive: c.nonTransitive,
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
else {
|
|
1592
|
+
r.registerCoercion(c.from, c.to, (v) => v, {
|
|
1593
|
+
nonTransitive: c.nonTransitive,
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1586
1596
|
}
|
|
1587
1597
|
// Nodes (use no-op impl for compute)
|
|
1588
1598
|
for (const n of desc.nodes) {
|
|
@@ -1644,7 +1654,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1644
1654
|
runAutoLayout();
|
|
1645
1655
|
}
|
|
1646
1656
|
}, [wb, runAutoLayout]);
|
|
1647
|
-
const
|
|
1657
|
+
const baseSetInput = React.useCallback((handle, raw) => {
|
|
1648
1658
|
if (!selectedNodeId)
|
|
1649
1659
|
return;
|
|
1650
1660
|
// If selected input is wired (has inbound edge), ignore user input to respect runtime value
|
|
@@ -1725,7 +1735,17 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1725
1735
|
}
|
|
1726
1736
|
runner.setInput(selectedNodeId, handle, value);
|
|
1727
1737
|
}, [selectedNodeId, def.edges, selectedDesc, runner]);
|
|
1728
|
-
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) => {
|
|
1729
1749
|
if (value === undefined || value === null)
|
|
1730
1750
|
return "";
|
|
1731
1751
|
if (typeId && typeId.includes("enum:")) {
|
|
@@ -1757,6 +1777,21 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1757
1777
|
}
|
|
1758
1778
|
return String(value);
|
|
1759
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]);
|
|
1760
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()
|
|
1761
1796
|
? "Stop engine before switching example"
|
|
1762
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()
|
|
@@ -1774,9 +1809,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1774
1809
|
catch (err) {
|
|
1775
1810
|
alert(String(err?.message ?? err));
|
|
1776
1811
|
}
|
|
1777
|
-
}, 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 })] })] }));
|
|
1778
1813
|
}
|
|
1779
|
-
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, }) {
|
|
1780
1815
|
const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
|
|
1781
1816
|
const [wb] = React.useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
|
|
1782
1817
|
const runner = React.useMemo(() => {
|
|
@@ -1787,35 +1822,36 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
1787
1822
|
: { kind: "local" };
|
|
1788
1823
|
return new GraphRunner(registry, backend);
|
|
1789
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]);
|
|
1790
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) => {
|
|
1791
1832
|
if (runner.isRunning())
|
|
1792
1833
|
runner.dispose();
|
|
1793
1834
|
onBackendKindChange(v);
|
|
1794
|
-
}, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange }) }));
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
function App() {
|
|
1798
|
-
const [engine, setEngine] = useQueryParamString("engine", "");
|
|
1799
|
-
const [example, setExample] = useQueryParamString("example", "simple");
|
|
1800
|
-
const [debug, setDebug] = useQueryParamBoolean("debug", false);
|
|
1801
|
-
const [showValues, setShowValues] = useQueryParamBoolean("values", false);
|
|
1802
|
-
const [hideWorkbench, setHideWorkbench] = useQueryParamBoolean("hideWb", false);
|
|
1803
|
-
const [autoScroll, setAutoScroll] = useQueryParamBoolean("autoScroll", true);
|
|
1804
|
-
// Backend selection via URL params
|
|
1805
|
-
const [backendKind, setBackendKind] = useQueryParamString("backend", "local");
|
|
1806
|
-
const [httpBaseUrl, setHttpBaseUrl] = useQueryParamString("sparkHttp", "http://127.0.0.1:18080");
|
|
1807
|
-
const [wsUrl, setWsUrl] = useQueryParamString("sparkWs", "ws://127.0.0.1:18081");
|
|
1808
|
-
React.useEffect(() => {
|
|
1809
|
-
document.getElementById("loading-screen")?.remove();
|
|
1810
|
-
}, []);
|
|
1811
|
-
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 }) }));
|
|
1812
1836
|
}
|
|
1813
1837
|
|
|
1814
1838
|
exports.AbstractWorkbench = AbstractWorkbench;
|
|
1815
|
-
exports.App = App;
|
|
1816
1839
|
exports.CLIWorkbench = CLIWorkbench;
|
|
1817
1840
|
exports.DefaultUIExtensionRegistry = DefaultUIExtensionRegistry;
|
|
1841
|
+
exports.GraphRunner = GraphRunner;
|
|
1818
1842
|
exports.InMemoryWorkbench = InMemoryWorkbench;
|
|
1843
|
+
exports.Inspector = Inspector;
|
|
1819
1844
|
exports.ReactFlowWorkbench = ReactFlowWorkbench;
|
|
1845
|
+
exports.WorkbenchCanvas = WorkbenchCanvas;
|
|
1846
|
+
exports.WorkbenchContext = WorkbenchContext;
|
|
1847
|
+
exports.WorkbenchProvider = WorkbenchProvider;
|
|
1848
|
+
exports.WorkbenchStudio = WorkbenchStudio;
|
|
1820
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;
|
|
1821
1857
|
//# sourceMappingURL=index.cjs.map
|