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