@bian-womp/spark-workbench 0.1.10 → 0.1.12
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 +426 -178
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/contracts.d.ts +2 -2
- package/lib/cjs/src/core/contracts.d.ts.map +1 -1
- package/lib/cjs/src/index.d.ts +1 -1
- 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.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 +2 -2
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/cjs/src/misc/mapping.d.ts +35 -4
- package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
- package/lib/cjs/src/runtime/GraphRunner.d.ts +1 -0
- package/lib/cjs/src/runtime/GraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +426 -178
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/contracts.d.ts +2 -2
- package/lib/esm/src/core/contracts.d.ts.map +1 -1
- package/lib/esm/src/index.d.ts +1 -1
- 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.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 +2 -2
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/esm/src/misc/mapping.d.ts +35 -4
- package/lib/esm/src/misc/mapping.d.ts.map +1 -1
- package/lib/esm/src/runtime/GraphRunner.d.ts +1 -0
- package/lib/esm/src/runtime/GraphRunner.d.ts.map +1 -1
- package/package.json +4 -4
- package/lib/cjs/src/adapters/react-flow/index.d.ts +0 -31
- package/lib/cjs/src/adapters/react-flow/index.d.ts.map +0 -1
- package/lib/esm/src/adapters/react-flow/index.d.ts +0 -31
- package/lib/esm/src/adapters/react-flow/index.d.ts.map +0 -1
package/lib/cjs/index.cjs
CHANGED
|
@@ -319,37 +319,6 @@ class CLIWorkbench {
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
function toReactFlow$1(def, positions = {}) {
|
|
323
|
-
const nodes = def.nodes.map((n) => ({
|
|
324
|
-
id: n.nodeId,
|
|
325
|
-
data: { typeId: n.typeId, params: n.params },
|
|
326
|
-
position: positions[n.nodeId] ?? { x: 0, y: 0 },
|
|
327
|
-
}));
|
|
328
|
-
const edges = def.edges.map((e) => ({
|
|
329
|
-
id: e.id,
|
|
330
|
-
source: e.source.nodeId,
|
|
331
|
-
target: e.target.nodeId,
|
|
332
|
-
sourceHandle: e.source.handle,
|
|
333
|
-
targetHandle: e.target.handle,
|
|
334
|
-
}));
|
|
335
|
-
return { nodes, edges };
|
|
336
|
-
}
|
|
337
|
-
class ReactFlowWorkbench {
|
|
338
|
-
constructor(wb) {
|
|
339
|
-
this.wb = wb;
|
|
340
|
-
}
|
|
341
|
-
get actions() {
|
|
342
|
-
return this.wb;
|
|
343
|
-
}
|
|
344
|
-
async load(def) {
|
|
345
|
-
await this.wb.load(def);
|
|
346
|
-
}
|
|
347
|
-
export(def) {
|
|
348
|
-
const d = def ?? this.wb.export();
|
|
349
|
-
return toReactFlow$1(d);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
322
|
class GraphRunner {
|
|
354
323
|
constructor(registry, backend) {
|
|
355
324
|
this.registry = registry;
|
|
@@ -363,6 +332,8 @@ class GraphRunner {
|
|
|
363
332
|
if (this.backend.kind === "local") {
|
|
364
333
|
const builder = new sparkGraph.GraphBuilder(this.registry);
|
|
365
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
338
|
}
|
|
368
339
|
// Remote: no-op here; build is performed on remote server during launch
|
|
@@ -371,7 +342,10 @@ class GraphRunner {
|
|
|
371
342
|
if (this.backend.kind === "local") {
|
|
372
343
|
if (!this.runtime)
|
|
373
344
|
return;
|
|
345
|
+
// Prevent mid-run churn while wiring changes are applied
|
|
346
|
+
this.runtime.pause();
|
|
374
347
|
this.runtime.update(def, this.registry);
|
|
348
|
+
this.runtime.resume();
|
|
375
349
|
this.emit("invalidate", { reason: "graph-updated" });
|
|
376
350
|
return;
|
|
377
351
|
}
|
|
@@ -434,6 +408,8 @@ class GraphRunner {
|
|
|
434
408
|
// Remote: build remotely then launch
|
|
435
409
|
void this.ensureRemote().then(async (rc) => {
|
|
436
410
|
await rc.runner.build(def);
|
|
411
|
+
// Signal UI after remote build as well
|
|
412
|
+
this.emit("invalidate", { reason: "graph-built" });
|
|
437
413
|
const eng = rc.runner.getEngine();
|
|
438
414
|
if (!rc.listenersBound) {
|
|
439
415
|
eng.on("value", (e) => {
|
|
@@ -463,8 +439,58 @@ class GraphRunner {
|
|
|
463
439
|
if (!this.stagedInputs[nodeId])
|
|
464
440
|
this.stagedInputs[nodeId] = {};
|
|
465
441
|
this.stagedInputs[nodeId][handle] = value;
|
|
466
|
-
if (this.engine)
|
|
442
|
+
if (this.engine) {
|
|
467
443
|
this.engine.setInput(nodeId, handle, value);
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
// Emit a value event so UI updates even when engine isn't running
|
|
447
|
+
this.emit("value", { nodeId, handle, value, io: "input" });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Batch update multiple inputs on a node and trigger a single run
|
|
451
|
+
setInputs(nodeId, inputs) {
|
|
452
|
+
if (!inputs)
|
|
453
|
+
return;
|
|
454
|
+
if (!this.stagedInputs[nodeId])
|
|
455
|
+
this.stagedInputs[nodeId] = {};
|
|
456
|
+
Object.assign(this.stagedInputs[nodeId], inputs);
|
|
457
|
+
// Local running: pause, set all inputs, resume, schedule a single recompute
|
|
458
|
+
if (this.backend.kind === "local" && this.engine && this.runtime) {
|
|
459
|
+
this.runtime.pause();
|
|
460
|
+
try {
|
|
461
|
+
for (const [handle, value] of Object.entries(inputs)) {
|
|
462
|
+
this.engine.setInput(nodeId, handle, value);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
this.runtime.resume();
|
|
467
|
+
try {
|
|
468
|
+
this.runtime.__unsafe_scheduleInputsChanged(nodeId);
|
|
469
|
+
}
|
|
470
|
+
catch { }
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Remote running: forward inputs individually (no batch API available)
|
|
474
|
+
else if (this.engine && this.backend.kind !== "local") {
|
|
475
|
+
// Prefer batch if supported by remote engine
|
|
476
|
+
if (this.engine instanceof sparkRemote.RemoteEngine) {
|
|
477
|
+
this.engine.setInputs(nodeId, inputs);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
console.warn("Remote engine does not support setInputs");
|
|
481
|
+
for (const [handle, value] of Object.entries(inputs)) {
|
|
482
|
+
this.engine.setInput(nodeId, handle, value);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Not running: emit value events so UI reflects staged values
|
|
487
|
+
else if (!this.engine) {
|
|
488
|
+
// Not running: emit a single synthetic value event per handle; UI will coalesce
|
|
489
|
+
console.warn("Remote engine does not exists");
|
|
490
|
+
for (const [handle, value] of Object.entries(inputs)) {
|
|
491
|
+
this.emit("value", { nodeId, handle, value, io: "input" });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
468
494
|
}
|
|
469
495
|
async step() {
|
|
470
496
|
if (this.backend.kind !== "local")
|
|
@@ -780,6 +806,98 @@ function useQueryParamString(key, defaultValue) {
|
|
|
780
806
|
return [val, set];
|
|
781
807
|
}
|
|
782
808
|
|
|
809
|
+
function toReactFlow(def, positions, registry, opts) {
|
|
810
|
+
const nodeHandleMap = {};
|
|
811
|
+
const nodes = def.nodes.map((n) => {
|
|
812
|
+
const desc = registry.nodes.get(n.typeId);
|
|
813
|
+
const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
814
|
+
const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
815
|
+
nodeHandleMap[n.nodeId] = {
|
|
816
|
+
inputs: new Set(inputHandles.map((h) => h.id)),
|
|
817
|
+
outputs: new Set(outputHandles.map((h) => h.id)),
|
|
818
|
+
};
|
|
819
|
+
return {
|
|
820
|
+
id: n.nodeId,
|
|
821
|
+
data: {
|
|
822
|
+
typeId: n.typeId,
|
|
823
|
+
params: n.params,
|
|
824
|
+
inputHandles,
|
|
825
|
+
outputHandles,
|
|
826
|
+
showValues: opts.showValues,
|
|
827
|
+
inputValues: opts.inputs?.[n.nodeId],
|
|
828
|
+
outputValues: opts.outputs?.[n.nodeId],
|
|
829
|
+
status: opts.nodeStatus?.[n.nodeId],
|
|
830
|
+
validation: {
|
|
831
|
+
inputs: opts.nodeValidation?.inputs?.[n.nodeId] ?? [],
|
|
832
|
+
outputs: opts.nodeValidation?.outputs?.[n.nodeId] ?? [],
|
|
833
|
+
issues: opts.nodeValidation?.issues?.[n.nodeId] ?? [],
|
|
834
|
+
},
|
|
835
|
+
toString: opts.toString,
|
|
836
|
+
toElement: opts.toElement,
|
|
837
|
+
},
|
|
838
|
+
position: positions[n.nodeId] ?? { x: 0, y: 0 },
|
|
839
|
+
type: opts.resolveNodeType?.(n.typeId) ?? "spark-default",
|
|
840
|
+
selected: opts.selectedNodeIds
|
|
841
|
+
? opts.selectedNodeIds.has(n.nodeId)
|
|
842
|
+
: undefined,
|
|
843
|
+
};
|
|
844
|
+
});
|
|
845
|
+
const edges = def.edges
|
|
846
|
+
.filter((e) => {
|
|
847
|
+
const src = nodeHandleMap[e.source.nodeId];
|
|
848
|
+
const dst = nodeHandleMap[e.target.nodeId];
|
|
849
|
+
if (!src || !dst)
|
|
850
|
+
return false;
|
|
851
|
+
return (src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle));
|
|
852
|
+
})
|
|
853
|
+
.map((e) => {
|
|
854
|
+
const st = opts.edgeStatus?.[e.id];
|
|
855
|
+
const isRunning = !!st?.activeRuns;
|
|
856
|
+
const hasError = !!st?.lastError;
|
|
857
|
+
const isInvalidEdge = !!opts.edgeValidation?.[e.id];
|
|
858
|
+
const style = hasError || isInvalidEdge
|
|
859
|
+
? { stroke: "#ef4444", strokeWidth: 2 }
|
|
860
|
+
: isRunning
|
|
861
|
+
? { stroke: "#3b82f6" }
|
|
862
|
+
: undefined;
|
|
863
|
+
return {
|
|
864
|
+
id: e.id,
|
|
865
|
+
source: e.source.nodeId,
|
|
866
|
+
target: e.target.nodeId,
|
|
867
|
+
sourceHandle: e.source.handle,
|
|
868
|
+
targetHandle: e.target.handle,
|
|
869
|
+
selected: opts.selectedEdgeIds
|
|
870
|
+
? opts.selectedEdgeIds.has(e.id)
|
|
871
|
+
: undefined,
|
|
872
|
+
animated: isRunning,
|
|
873
|
+
style,
|
|
874
|
+
};
|
|
875
|
+
});
|
|
876
|
+
return { nodes, edges };
|
|
877
|
+
}
|
|
878
|
+
// Shared node container border class composition for consistent visuals
|
|
879
|
+
function getNodeBorderClassNames(args) {
|
|
880
|
+
const selected = !!args.selected;
|
|
881
|
+
const status = args.status || {};
|
|
882
|
+
const issues = args.validation?.issues ?? [];
|
|
883
|
+
const hasError = !!status.lastError;
|
|
884
|
+
const hasValidationError = issues.some((i) => i?.level === "error");
|
|
885
|
+
const hasValidationWarning = !hasValidationError && issues.length > 0;
|
|
886
|
+
const isRunning = !!status.activeRuns;
|
|
887
|
+
const isInvalid = !!status.invalidated && !isRunning && !hasError;
|
|
888
|
+
const borderWidth = selected ? "border-2" : "border";
|
|
889
|
+
const borderStyle = isInvalid ? "border-dashed" : "border-solid";
|
|
890
|
+
const borderColor = hasError || hasValidationError
|
|
891
|
+
? "border-red-500"
|
|
892
|
+
: hasValidationWarning
|
|
893
|
+
? "border-amber-500"
|
|
894
|
+
: isRunning
|
|
895
|
+
? "border-blue-500"
|
|
896
|
+
: "border-gray-500 dark:border-gray-400";
|
|
897
|
+
const ring = isRunning ? " ring-2 ring-blue-200 dark:ring-blue-900" : "";
|
|
898
|
+
return `${borderWidth} ${borderStyle} ${borderColor}${ring}`.trim();
|
|
899
|
+
}
|
|
900
|
+
|
|
783
901
|
const WorkbenchContext = React.createContext(null);
|
|
784
902
|
function useWorkbenchContext() {
|
|
785
903
|
const ctx = React.useContext(WorkbenchContext);
|
|
@@ -808,6 +926,19 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
808
926
|
const def = wb.export();
|
|
809
927
|
const inputsMap = React.useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
|
|
810
928
|
const outputsMap = React.useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
|
|
929
|
+
// Initialize nodes as invalidated by default until first successful run
|
|
930
|
+
React.useEffect(() => {
|
|
931
|
+
setNodeStatus((prev) => {
|
|
932
|
+
const next = { ...prev };
|
|
933
|
+
for (const n of def.nodes) {
|
|
934
|
+
const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
|
|
935
|
+
if (cur.invalidated === undefined) {
|
|
936
|
+
next[n.nodeId] = { ...cur, invalidated: true };
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return next;
|
|
940
|
+
});
|
|
941
|
+
}, [def]);
|
|
811
942
|
// Auto layout (simple layered layout)
|
|
812
943
|
const runAutoLayout = React.useCallback(() => {
|
|
813
944
|
const cur = wb.export();
|
|
@@ -875,20 +1006,20 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
875
1006
|
const off2 = runner.on("error", (e) => {
|
|
876
1007
|
const edgeError = e;
|
|
877
1008
|
const nodeError = e;
|
|
878
|
-
if (edgeError
|
|
1009
|
+
if (edgeError.kind === "edge-convert") {
|
|
879
1010
|
const edgeId = edgeError.edgeId;
|
|
880
1011
|
setEdgeStatus((s) => ({
|
|
881
1012
|
...s,
|
|
882
1013
|
[edgeId]: { ...(s[edgeId] ?? {}), lastError: edgeError.err },
|
|
883
1014
|
}));
|
|
884
1015
|
}
|
|
885
|
-
else if (nodeError
|
|
886
|
-
const nodeId = nodeError
|
|
1016
|
+
else if (nodeError.nodeId) {
|
|
1017
|
+
const nodeId = nodeError.nodeId;
|
|
887
1018
|
setNodeStatus((s) => ({
|
|
888
1019
|
...s,
|
|
889
1020
|
[nodeId]: {
|
|
890
1021
|
...(s[nodeId] ?? {}),
|
|
891
|
-
lastError: nodeError
|
|
1022
|
+
lastError: nodeError.err,
|
|
892
1023
|
},
|
|
893
1024
|
}));
|
|
894
1025
|
}
|
|
@@ -911,15 +1042,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
911
1042
|
return;
|
|
912
1043
|
if (s.kind === "node-start") {
|
|
913
1044
|
const id = s.nodeId;
|
|
914
|
-
setNodeStatus((prev) =>
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
...
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1045
|
+
setNodeStatus((prev) => {
|
|
1046
|
+
const active = Math.max(1, (prev[id]?.activeRuns ?? 0) + 1);
|
|
1047
|
+
return {
|
|
1048
|
+
...prev,
|
|
1049
|
+
[id]: {
|
|
1050
|
+
...(prev[id] ?? {}),
|
|
1051
|
+
activeRuns: active,
|
|
1052
|
+
progress: 0,
|
|
1053
|
+
invalidated: false,
|
|
1054
|
+
},
|
|
1055
|
+
};
|
|
1056
|
+
});
|
|
923
1057
|
}
|
|
924
1058
|
else if (s.kind === "node-progress") {
|
|
925
1059
|
const id = s.nodeId;
|
|
@@ -927,35 +1061,60 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
927
1061
|
...prev,
|
|
928
1062
|
[id]: {
|
|
929
1063
|
...(prev[id] ?? {}),
|
|
930
|
-
running: true,
|
|
931
1064
|
progress: Number(s.progress) || 0,
|
|
932
1065
|
},
|
|
933
1066
|
}));
|
|
934
1067
|
}
|
|
935
1068
|
else if (s.kind === "node-done") {
|
|
936
1069
|
const id = s.nodeId;
|
|
937
|
-
setNodeStatus((prev) =>
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1070
|
+
setNodeStatus((prev) => {
|
|
1071
|
+
const current = prev[id]?.activeRuns ?? 1;
|
|
1072
|
+
const nextActive = Math.max(0, current - 1);
|
|
1073
|
+
return {
|
|
1074
|
+
...prev,
|
|
1075
|
+
[id]: {
|
|
1076
|
+
...(prev[id] ?? {}),
|
|
1077
|
+
activeRuns: nextActive,
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
});
|
|
941
1081
|
}
|
|
942
1082
|
else if (s.kind === "edge-start") {
|
|
943
1083
|
const id = s.edgeId;
|
|
944
|
-
setEdgeStatus((prev) =>
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1084
|
+
setEdgeStatus((prev) => {
|
|
1085
|
+
const current = prev[id]?.activeRuns ?? 1;
|
|
1086
|
+
const nextActive = Math.max(0, current + 1);
|
|
1087
|
+
return {
|
|
1088
|
+
...prev,
|
|
1089
|
+
[id]: { ...(prev[id] ?? {}), activeRuns: nextActive },
|
|
1090
|
+
};
|
|
1091
|
+
});
|
|
948
1092
|
}
|
|
949
1093
|
else if (s.kind === "edge-done") {
|
|
950
1094
|
const id = s.edgeId;
|
|
951
|
-
setEdgeStatus((prev) =>
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1095
|
+
setEdgeStatus((prev) => {
|
|
1096
|
+
const current = prev[id]?.activeRuns ?? 1;
|
|
1097
|
+
const nextActive = Math.max(0, current - 1);
|
|
1098
|
+
return {
|
|
1099
|
+
...prev,
|
|
1100
|
+
[id]: { ...(prev[id] ?? {}), activeRuns: nextActive },
|
|
1101
|
+
};
|
|
1102
|
+
});
|
|
955
1103
|
}
|
|
956
1104
|
return add("runner", "stats")(s);
|
|
957
1105
|
});
|
|
958
1106
|
const off4 = wb.on("graphChanged", add("workbench", "graphChanged"));
|
|
1107
|
+
// Ensure newly added nodes start as invalidated until first evaluation
|
|
1108
|
+
const off4c = wb.on("graphChanged", (e) => {
|
|
1109
|
+
const change = e.change;
|
|
1110
|
+
if (change?.type === "addNode" && typeof change.nodeId === "string") {
|
|
1111
|
+
const id = change.nodeId;
|
|
1112
|
+
setNodeStatus((s) => ({
|
|
1113
|
+
...s,
|
|
1114
|
+
[id]: { ...(s[id] ?? {}), invalidated: true },
|
|
1115
|
+
}));
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
959
1118
|
const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
960
1119
|
const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
961
1120
|
const off5b = wb.on("validationChanged", (r) => setValidation(r));
|
|
@@ -972,6 +1131,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
972
1131
|
off3b();
|
|
973
1132
|
off4();
|
|
974
1133
|
off4b();
|
|
1134
|
+
off4c();
|
|
975
1135
|
off5();
|
|
976
1136
|
off5b();
|
|
977
1137
|
off6();
|
|
@@ -994,10 +1154,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
994
1154
|
if (!validation)
|
|
995
1155
|
return { inputs, outputs, issues };
|
|
996
1156
|
for (const is of validation.issues ?? []) {
|
|
997
|
-
const d = is
|
|
998
|
-
const level = is
|
|
999
|
-
const code = String(is
|
|
1000
|
-
const message = String(is
|
|
1157
|
+
const d = is.data;
|
|
1158
|
+
const level = is.level;
|
|
1159
|
+
const code = String(is.code ?? "");
|
|
1160
|
+
const message = String(is.message ?? code);
|
|
1001
1161
|
if (!d)
|
|
1002
1162
|
continue;
|
|
1003
1163
|
if (d.nodeId) {
|
|
@@ -1026,10 +1186,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1026
1186
|
if (!validation)
|
|
1027
1187
|
return list;
|
|
1028
1188
|
for (const is of validation.issues ?? []) {
|
|
1029
|
-
const d = is
|
|
1030
|
-
const level = is
|
|
1031
|
-
const code = String(is
|
|
1032
|
-
const message = String(is
|
|
1189
|
+
const d = is.data;
|
|
1190
|
+
const level = is.level;
|
|
1191
|
+
const code = String(is.code ?? "");
|
|
1192
|
+
const message = String(is.message ?? code);
|
|
1033
1193
|
if (!d || (!d.nodeId && !d.edgeId)) {
|
|
1034
1194
|
list.push({ level, code, message });
|
|
1035
1195
|
}
|
|
@@ -1042,10 +1202,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1042
1202
|
if (!validation)
|
|
1043
1203
|
return { errors, issues };
|
|
1044
1204
|
for (const is of validation.issues ?? []) {
|
|
1045
|
-
const d = is
|
|
1046
|
-
const level = is
|
|
1047
|
-
const code = String(is
|
|
1048
|
-
const message = String(is
|
|
1205
|
+
const d = is.data;
|
|
1206
|
+
const level = is.level;
|
|
1207
|
+
const code = String(is.code ?? "");
|
|
1208
|
+
const message = String(is.message ?? code);
|
|
1049
1209
|
if (d?.edgeId) {
|
|
1050
1210
|
if (level === "error")
|
|
1051
1211
|
errors[d.edgeId] = true;
|
|
@@ -1156,6 +1316,16 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
|
|
|
1156
1316
|
}
|
|
1157
1317
|
|
|
1158
1318
|
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, setInput, }) {
|
|
1319
|
+
const safeToString = (typeId, value) => {
|
|
1320
|
+
try {
|
|
1321
|
+
return typeof toString === "function"
|
|
1322
|
+
? toString(typeId, value)
|
|
1323
|
+
: String(value ?? "");
|
|
1324
|
+
}
|
|
1325
|
+
catch {
|
|
1326
|
+
return String(value ?? "");
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1159
1329
|
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
|
|
1160
1330
|
const nodeValidationIssues = validationByNode.issues;
|
|
1161
1331
|
const edgeValidationIssues = validationByEdge.issues;
|
|
@@ -1215,7 +1385,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1215
1385
|
for (const h of handles) {
|
|
1216
1386
|
const typeId = desc?.inputs?.[h];
|
|
1217
1387
|
const current = nodeInputs[h];
|
|
1218
|
-
const display =
|
|
1388
|
+
const display = safeToString(typeId, current);
|
|
1219
1389
|
const wasOriginal = originals[h];
|
|
1220
1390
|
const isDirty = drafts[h] !== undefined &&
|
|
1221
1391
|
wasOriginal !== undefined &&
|
|
@@ -1241,7 +1411,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1241
1411
|
disabled: isLinked,
|
|
1242
1412
|
};
|
|
1243
1413
|
const current = nodeInputs[h];
|
|
1244
|
-
const value = drafts[h] ??
|
|
1414
|
+
const value = drafts[h] ?? safeToString(typeId, current);
|
|
1245
1415
|
const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
|
|
1246
1416
|
const commit = () => {
|
|
1247
1417
|
const draft = drafts[h];
|
|
@@ -1251,7 +1421,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1251
1421
|
setOriginals((o) => ({ ...o, [h]: draft }));
|
|
1252
1422
|
};
|
|
1253
1423
|
const revert = () => {
|
|
1254
|
-
const orig = originals[h] ??
|
|
1424
|
+
const orig = originals[h] ?? safeToString(typeId, current);
|
|
1255
1425
|
setDrafts((d) => ({ ...d, [h]: orig }));
|
|
1256
1426
|
};
|
|
1257
1427
|
const isEnum = typeId?.includes("enum:");
|
|
@@ -1261,19 +1431,17 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1261
1431
|
const title = inIssues
|
|
1262
1432
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1263
1433
|
.join("; ");
|
|
1264
|
-
return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-28", children: [h, jsxRuntime.jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value:
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
let raw = (byLabel !== undefined ? byLabel : Number(label));
|
|
1270
|
-
if (!Number.isFinite(raw))
|
|
1271
|
-
raw = undefined;
|
|
1434
|
+
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
|
|
1435
|
+
? String(current)
|
|
1436
|
+
: "", onChange: (e) => {
|
|
1437
|
+
const val = e.target.value;
|
|
1438
|
+
const raw = val === "" ? undefined : Number(val);
|
|
1272
1439
|
setInput(h, raw);
|
|
1273
|
-
|
|
1440
|
+
// keep drafts/originals in sync with label for display elsewhere
|
|
1441
|
+
const display = safeToString(typeId, raw);
|
|
1274
1442
|
setDrafts((d) => ({ ...d, [h]: display }));
|
|
1275
1443
|
setOriginals((o) => ({ ...o, [h]: display }));
|
|
1276
|
-
}, ...commonProps, children: [jsxRuntime.jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsxRuntime.jsx("option", { value: opt.
|
|
1444
|
+
}, ...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) => {
|
|
1277
1445
|
if (e.key === "Enter")
|
|
1278
1446
|
commit();
|
|
1279
1447
|
if (e.key === "Escape")
|
|
@@ -1291,74 +1459,8 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1291
1459
|
})()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) }), debug && (jsxRuntime.jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsxRuntime.jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
|
|
1292
1460
|
}
|
|
1293
1461
|
|
|
1294
|
-
function toReactFlow(def, positions, registry, selectedNodeIds, selectedEdgeIds, opts) {
|
|
1295
|
-
const nodeHandleMap = {};
|
|
1296
|
-
const nodes = def.nodes.map((n) => {
|
|
1297
|
-
const desc = registry.nodes.get(n.typeId);
|
|
1298
|
-
const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
1299
|
-
const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
1300
|
-
nodeHandleMap[n.nodeId] = {
|
|
1301
|
-
inputs: new Set(inputHandles.map((h) => h.id)),
|
|
1302
|
-
outputs: new Set(outputHandles.map((h) => h.id)),
|
|
1303
|
-
};
|
|
1304
|
-
return {
|
|
1305
|
-
id: n.nodeId,
|
|
1306
|
-
data: {
|
|
1307
|
-
typeId: n.typeId,
|
|
1308
|
-
params: n.params,
|
|
1309
|
-
inputHandles,
|
|
1310
|
-
outputHandles,
|
|
1311
|
-
showValues: opts?.showValues,
|
|
1312
|
-
inputValues: opts?.inputs?.[n.nodeId],
|
|
1313
|
-
outputValues: opts?.outputs?.[n.nodeId],
|
|
1314
|
-
status: opts?.nodeStatus?.[n.nodeId],
|
|
1315
|
-
validation: {
|
|
1316
|
-
inputs: opts?.nodeValidation?.inputs?.[n.nodeId] ?? [],
|
|
1317
|
-
outputs: opts?.nodeValidation?.outputs?.[n.nodeId] ?? [],
|
|
1318
|
-
issues: opts?.nodeValidation?.issues?.[n.nodeId] ?? [],
|
|
1319
|
-
},
|
|
1320
|
-
toString: opts?.toString,
|
|
1321
|
-
toElement: opts?.toElement,
|
|
1322
|
-
},
|
|
1323
|
-
position: positions[n.nodeId] ?? { x: 0, y: 0 },
|
|
1324
|
-
type: opts?.resolveNodeType?.(n.typeId) ?? "spark:default",
|
|
1325
|
-
selected: selectedNodeIds ? selectedNodeIds.has(n.nodeId) : undefined,
|
|
1326
|
-
};
|
|
1327
|
-
});
|
|
1328
|
-
const edges = def.edges
|
|
1329
|
-
.filter((e) => {
|
|
1330
|
-
const src = nodeHandleMap[e.source.nodeId];
|
|
1331
|
-
const dst = nodeHandleMap[e.target.nodeId];
|
|
1332
|
-
if (!src || !dst)
|
|
1333
|
-
return false;
|
|
1334
|
-
return (src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle));
|
|
1335
|
-
})
|
|
1336
|
-
.map((e) => {
|
|
1337
|
-
const st = opts?.edgeStatus?.[e.id];
|
|
1338
|
-
const isRunning = !!st?.running;
|
|
1339
|
-
const hasError = !!st?.lastError;
|
|
1340
|
-
const isInvalidEdge = !!opts?.edgeValidation?.[e.id];
|
|
1341
|
-
const style = hasError || isInvalidEdge
|
|
1342
|
-
? { stroke: "#ef4444", strokeWidth: 2 }
|
|
1343
|
-
: isRunning
|
|
1344
|
-
? { stroke: "#3b82f6" }
|
|
1345
|
-
: undefined;
|
|
1346
|
-
return {
|
|
1347
|
-
id: e.id,
|
|
1348
|
-
source: e.source.nodeId,
|
|
1349
|
-
target: e.target.nodeId,
|
|
1350
|
-
sourceHandle: e.source.handle,
|
|
1351
|
-
targetHandle: e.target.handle,
|
|
1352
|
-
selected: selectedEdgeIds ? selectedEdgeIds.has(e.id) : undefined,
|
|
1353
|
-
animated: isRunning,
|
|
1354
|
-
style,
|
|
1355
|
-
};
|
|
1356
|
-
});
|
|
1357
|
-
return { nodes, edges };
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
1462
|
const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
|
|
1361
|
-
const { typeId, showValues, inputValues, outputValues, toString
|
|
1463
|
+
const { typeId, showValues, inputValues, outputValues, toString } = data;
|
|
1362
1464
|
const inputEntries = data.inputHandles ?? [];
|
|
1363
1465
|
const outputEntries = data.outputHandles ?? [];
|
|
1364
1466
|
const status = data.status ?? {};
|
|
@@ -1374,19 +1476,26 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1374
1476
|
const minWidth = data.showValues ? 320 : 160;
|
|
1375
1477
|
const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
|
|
1376
1478
|
const hasError = !!status.lastError;
|
|
1377
|
-
const
|
|
1479
|
+
const hasValidationError = validation.issues.some((i) => i.level === "error");
|
|
1480
|
+
const hasValidationWarning = !hasValidationError && validation.issues.length > 0;
|
|
1481
|
+
const isRunning = !!status.activeRuns;
|
|
1378
1482
|
const isInvalid = !!status.invalidated && !isRunning && !hasError;
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1483
|
+
// Border color encodes severity; thickness encodes selection; style (dashed) encodes invalidated
|
|
1484
|
+
const borderWidth = selected ? "border-2" : "border";
|
|
1485
|
+
const borderStyle = isInvalid ? "border-dashed" : "border-solid";
|
|
1486
|
+
const borderColor = hasError || hasValidationError
|
|
1487
|
+
? "border-red-500"
|
|
1488
|
+
: hasValidationWarning
|
|
1489
|
+
? "border-amber-500"
|
|
1383
1490
|
: isRunning
|
|
1384
|
-
? "border-
|
|
1385
|
-
:
|
|
1386
|
-
|
|
1387
|
-
|
|
1491
|
+
? "border-blue-500"
|
|
1492
|
+
: "border-gray-500 dark:border-gray-400";
|
|
1493
|
+
const ringClasses = isRunning
|
|
1494
|
+
? "ring-2 ring-blue-200 dark:ring-blue-900"
|
|
1495
|
+
: undefined;
|
|
1496
|
+
const borderClasses = cx(borderWidth, borderStyle, borderColor, ringClasses);
|
|
1388
1497
|
const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
|
|
1389
|
-
return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900
|
|
1498
|
+
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")
|
|
1390
1499
|
? "error"
|
|
1391
1500
|
: "warning", size: 12, className: "w-3 h-3", title: validation.issues
|
|
1392
1501
|
.map((v) => `${v.code}: ${v.message}`)
|
|
@@ -1397,8 +1506,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1397
1506
|
const title = vIssues
|
|
1398
1507
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1399
1508
|
.join("; ");
|
|
1400
|
-
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8 }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues &&
|
|
1401
|
-
(toElement ? (toElement(entry.typeId, inputValues?.[entry.id])) : toString ? (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) })) : (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: String(inputValues?.[entry.id]) })))] })] }, `in-${entry.id}`));
|
|
1509
|
+
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}`));
|
|
1402
1510
|
}), outputEntries.map((entry, i) => {
|
|
1403
1511
|
const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
|
|
1404
1512
|
const hasAny = vIssues.length > 0;
|
|
@@ -1406,8 +1514,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1406
1514
|
const title = vIssues
|
|
1407
1515
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1408
1516
|
.join("; ");
|
|
1409
|
-
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8, textAlign: "right" }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues &&
|
|
1410
|
-
(toElement ? (toElement(entry.typeId, outputValues?.[entry.id])) : toString ? (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, outputValues?.[entry.id]) })) : (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: String(outputValues?.[entry.id]) })))] })] }, `out-${entry.id}`));
|
|
1517
|
+
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}`));
|
|
1411
1518
|
})] }));
|
|
1412
1519
|
});
|
|
1413
1520
|
DefaultNode.displayName = "DefaultNode";
|
|
@@ -1415,15 +1522,135 @@ DefaultNode.displayName = "DefaultNode";
|
|
|
1415
1522
|
function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
|
|
1416
1523
|
const { registry } = useWorkbenchContext();
|
|
1417
1524
|
const rf = ReactFlow.useReactFlow();
|
|
1525
|
+
const ids = Array.from(registry.nodes.keys());
|
|
1526
|
+
// Group node ids by the segment before the first '.'
|
|
1527
|
+
const grouped = {};
|
|
1528
|
+
for (const id of ids) {
|
|
1529
|
+
const parts = id.split(".");
|
|
1530
|
+
const cat = parts.length > 1 ? parts[0] : "other";
|
|
1531
|
+
const label = parts.length > 1 ? parts.slice(1).join(".") : id;
|
|
1532
|
+
(grouped[cat] = grouped[cat] || []).push({ id, label });
|
|
1533
|
+
}
|
|
1534
|
+
const cats = Object.keys(grouped).sort((a, b) => a.localeCompare(b));
|
|
1535
|
+
cats.forEach((c) => grouped[c].sort((a, b) => a.label.localeCompare(b.label)));
|
|
1536
|
+
const totalCount = ids.length;
|
|
1537
|
+
// Ref for focus/outside click handling
|
|
1538
|
+
const ref = React.useRef(null);
|
|
1539
|
+
// Close on outside click and on ESC
|
|
1540
|
+
React.useEffect(() => {
|
|
1541
|
+
if (!open)
|
|
1542
|
+
return;
|
|
1543
|
+
const onDown = (e) => {
|
|
1544
|
+
if (!ref.current)
|
|
1545
|
+
return;
|
|
1546
|
+
if (!ref.current.contains(e.target))
|
|
1547
|
+
onClose();
|
|
1548
|
+
};
|
|
1549
|
+
const onKey = (e) => {
|
|
1550
|
+
if (e.key === "Escape")
|
|
1551
|
+
onClose();
|
|
1552
|
+
};
|
|
1553
|
+
window.addEventListener("mousedown", onDown, true);
|
|
1554
|
+
window.addEventListener("keydown", onKey);
|
|
1555
|
+
return () => {
|
|
1556
|
+
window.removeEventListener("mousedown", onDown, true);
|
|
1557
|
+
window.removeEventListener("keydown", onKey);
|
|
1558
|
+
};
|
|
1559
|
+
}, [open, onClose]);
|
|
1560
|
+
// Focus for keyboard accessibility
|
|
1561
|
+
React.useEffect(() => {
|
|
1562
|
+
if (open)
|
|
1563
|
+
ref.current?.focus();
|
|
1564
|
+
}, [open]);
|
|
1418
1565
|
if (!open || !clientPos)
|
|
1419
1566
|
return null;
|
|
1420
|
-
|
|
1567
|
+
// Clamp menu position to viewport
|
|
1568
|
+
const MENU_MIN_WIDTH = 180;
|
|
1569
|
+
const PADDING = 16; // rough padding/shadow
|
|
1570
|
+
const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
|
|
1571
|
+
(MENU_MIN_WIDTH + PADDING));
|
|
1572
|
+
const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
|
|
1421
1573
|
const handleClick = (typeId) => {
|
|
1422
|
-
|
|
1574
|
+
// project() is deprecated; use screenToFlowPosition for screen coordinates
|
|
1575
|
+
const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
|
|
1423
1576
|
onAdd(typeId, p);
|
|
1424
1577
|
onClose();
|
|
1425
1578
|
};
|
|
1426
|
-
return (jsxRuntime.jsxs("div", { className: "fixed z-[1000] bg-white border border-gray-300 rounded-
|
|
1579
|
+
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) => {
|
|
1580
|
+
e.preventDefault();
|
|
1581
|
+
e.stopPropagation();
|
|
1582
|
+
}, 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))) })] }));
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
|
|
1586
|
+
const { wb, runner, engineKind } = useWorkbenchContext();
|
|
1587
|
+
const ref = React.useRef(null);
|
|
1588
|
+
// outside click + ESC
|
|
1589
|
+
React.useEffect(() => {
|
|
1590
|
+
if (!open)
|
|
1591
|
+
return;
|
|
1592
|
+
const onDown = (e) => {
|
|
1593
|
+
if (!ref.current)
|
|
1594
|
+
return;
|
|
1595
|
+
if (!ref.current.contains(e.target))
|
|
1596
|
+
onClose();
|
|
1597
|
+
};
|
|
1598
|
+
const onKey = (e) => {
|
|
1599
|
+
if (e.key === "Escape")
|
|
1600
|
+
onClose();
|
|
1601
|
+
};
|
|
1602
|
+
window.addEventListener("mousedown", onDown, true);
|
|
1603
|
+
window.addEventListener("keydown", onKey);
|
|
1604
|
+
return () => {
|
|
1605
|
+
window.removeEventListener("mousedown", onDown, true);
|
|
1606
|
+
window.removeEventListener("keydown", onKey);
|
|
1607
|
+
};
|
|
1608
|
+
}, [open, onClose]);
|
|
1609
|
+
React.useEffect(() => {
|
|
1610
|
+
if (open)
|
|
1611
|
+
ref.current?.focus();
|
|
1612
|
+
}, [open]);
|
|
1613
|
+
if (!open || !clientPos || !nodeId)
|
|
1614
|
+
return null;
|
|
1615
|
+
// clamp
|
|
1616
|
+
const MENU_MIN_WIDTH = 180;
|
|
1617
|
+
const PADDING = 16;
|
|
1618
|
+
const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
|
|
1619
|
+
(MENU_MIN_WIDTH + PADDING));
|
|
1620
|
+
const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
|
|
1621
|
+
// actions
|
|
1622
|
+
const handleDelete = () => {
|
|
1623
|
+
wb.removeNode(nodeId);
|
|
1624
|
+
onClose();
|
|
1625
|
+
};
|
|
1626
|
+
const handleDuplicate = () => {
|
|
1627
|
+
const def = wb.export();
|
|
1628
|
+
const n = def.nodes.find((n) => n.nodeId === nodeId);
|
|
1629
|
+
if (!n)
|
|
1630
|
+
return onClose();
|
|
1631
|
+
const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
|
|
1632
|
+
wb.addNode({ typeId: n.typeId, params: n.params, position: { x: pos.x + 24, y: pos.y + 24 } });
|
|
1633
|
+
onClose();
|
|
1634
|
+
};
|
|
1635
|
+
const handleCopyId = async () => {
|
|
1636
|
+
try {
|
|
1637
|
+
await navigator.clipboard.writeText(nodeId);
|
|
1638
|
+
}
|
|
1639
|
+
catch { }
|
|
1640
|
+
onClose();
|
|
1641
|
+
};
|
|
1642
|
+
const canRunPull = engineKind()?.toString() === "pull";
|
|
1643
|
+
const handleRunPull = async () => {
|
|
1644
|
+
try {
|
|
1645
|
+
await runner.computeNode(nodeId);
|
|
1646
|
+
}
|
|
1647
|
+
catch { }
|
|
1648
|
+
onClose();
|
|
1649
|
+
};
|
|
1650
|
+
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) => {
|
|
1651
|
+
e.preventDefault();
|
|
1652
|
+
e.stopPropagation();
|
|
1653
|
+
}, 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" })] }));
|
|
1427
1654
|
}
|
|
1428
1655
|
|
|
1429
1656
|
function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
@@ -1441,18 +1668,21 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
|
1441
1668
|
if (renderer)
|
|
1442
1669
|
custom.set(typeId, renderer);
|
|
1443
1670
|
}
|
|
1444
|
-
const types = {
|
|
1671
|
+
const types = {
|
|
1672
|
+
"spark-default": DefaultNode,
|
|
1673
|
+
default: DefaultNode,
|
|
1674
|
+
};
|
|
1445
1675
|
for (const [typeId, comp] of custom.entries()) {
|
|
1446
|
-
types[`spark
|
|
1676
|
+
types[`spark-${typeId}`] = comp;
|
|
1447
1677
|
}
|
|
1448
|
-
const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark
|
|
1678
|
+
const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark-${nodeTypeId}` : "spark-default";
|
|
1449
1679
|
return { nodeTypes: types, resolveNodeType: resolver };
|
|
1450
1680
|
// registry is stable; ui renderers expected to be set up before mount
|
|
1451
1681
|
}, [wb, registry]);
|
|
1452
1682
|
const { nodes, edges } = React.useMemo(() => {
|
|
1453
1683
|
const def = wb.export();
|
|
1454
1684
|
const sel = wb.getSelection();
|
|
1455
|
-
return toReactFlow(def, wb.getPositions(), registry,
|
|
1685
|
+
return toReactFlow(def, wb.getPositions(), registry, {
|
|
1456
1686
|
showValues,
|
|
1457
1687
|
inputs: ioValues.inputs,
|
|
1458
1688
|
outputs: ioValues.outputs,
|
|
@@ -1463,6 +1693,8 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
|
1463
1693
|
edgeStatus,
|
|
1464
1694
|
nodeValidation,
|
|
1465
1695
|
edgeValidation,
|
|
1696
|
+
selectedNodeIds: new Set(sel.nodes),
|
|
1697
|
+
selectedEdgeIds: new Set(sel.edges),
|
|
1466
1698
|
});
|
|
1467
1699
|
}, [
|
|
1468
1700
|
showValues,
|
|
@@ -1477,15 +1709,31 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
|
1477
1709
|
]);
|
|
1478
1710
|
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
1479
1711
|
const [menuPos, setMenuPos] = React.useState(null);
|
|
1712
|
+
const [nodeMenuOpen, setNodeMenuOpen] = React.useState(false);
|
|
1713
|
+
const [nodeMenuPos, setNodeMenuPos] = React.useState(null);
|
|
1714
|
+
const [nodeAtMenu, setNodeAtMenu] = React.useState(null);
|
|
1480
1715
|
const onContextMenu = (e) => {
|
|
1481
1716
|
e.preventDefault();
|
|
1482
|
-
|
|
1483
|
-
|
|
1717
|
+
// Determine if right-clicked over a node by hit-testing selection
|
|
1718
|
+
const target = e.target?.closest(".react-flow__node");
|
|
1719
|
+
if (target) {
|
|
1720
|
+
// Resolve node id from data-id attribute React Flow sets
|
|
1721
|
+
const nodeId = target.getAttribute("data-id");
|
|
1722
|
+
setNodeAtMenu(nodeId);
|
|
1723
|
+
setNodeMenuPos({ x: e.clientX, y: e.clientY });
|
|
1724
|
+
setNodeMenuOpen(true);
|
|
1725
|
+
setMenuOpen(false);
|
|
1726
|
+
}
|
|
1727
|
+
else {
|
|
1728
|
+
setMenuPos({ x: e.clientX, y: e.clientY });
|
|
1729
|
+
setMenuOpen(true);
|
|
1730
|
+
setNodeMenuOpen(false);
|
|
1731
|
+
}
|
|
1484
1732
|
};
|
|
1485
1733
|
const addNodeAt = (typeId, pos) => {
|
|
1486
1734
|
wb.addNode({ typeId, position: pos });
|
|
1487
1735
|
};
|
|
1488
|
-
return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, selectionOnDrag: true, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onSelectionChange: onSelectionChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, children: [jsxRuntime.jsx(ReactFlow.Background, {}), jsxRuntime.jsx(ReactFlow.MiniMap, {}), jsxRuntime.jsx(ReactFlow.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) })] }) }));
|
|
1736
|
+
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) })] }) }));
|
|
1489
1737
|
}
|
|
1490
1738
|
|
|
1491
1739
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, }) {
|
|
@@ -1778,7 +2026,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1778
2026
|
return String(value);
|
|
1779
2027
|
}, [registry]);
|
|
1780
2028
|
const baseToElement = React.useCallback((typeId, value) => {
|
|
1781
|
-
return jsxRuntime.jsx("span", { children: baseToString(typeId, value) });
|
|
2029
|
+
return (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: baseToString(typeId, value) }));
|
|
1782
2030
|
}, [baseToString]);
|
|
1783
2031
|
const toString = React.useMemo(() => {
|
|
1784
2032
|
if (overrides?.toString)
|
|
@@ -1809,7 +2057,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1809
2057
|
catch (err) {
|
|
1810
2058
|
alert(String(err?.message ?? err));
|
|
1811
2059
|
}
|
|
1812
|
-
}, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { showValues: showValues, toString: toString, toElement: toElement }
|
|
2060
|
+
}, 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 })] })] }));
|
|
1813
2061
|
}
|
|
1814
2062
|
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
|
|
1815
2063
|
const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
|
|
@@ -1841,12 +2089,12 @@ exports.DefaultUIExtensionRegistry = DefaultUIExtensionRegistry;
|
|
|
1841
2089
|
exports.GraphRunner = GraphRunner;
|
|
1842
2090
|
exports.InMemoryWorkbench = InMemoryWorkbench;
|
|
1843
2091
|
exports.Inspector = Inspector;
|
|
1844
|
-
exports.ReactFlowWorkbench = ReactFlowWorkbench;
|
|
1845
2092
|
exports.WorkbenchCanvas = WorkbenchCanvas;
|
|
1846
2093
|
exports.WorkbenchContext = WorkbenchContext;
|
|
1847
2094
|
exports.WorkbenchProvider = WorkbenchProvider;
|
|
1848
2095
|
exports.WorkbenchStudio = WorkbenchStudio;
|
|
1849
|
-
exports.
|
|
2096
|
+
exports.getNodeBorderClassNames = getNodeBorderClassNames;
|
|
2097
|
+
exports.toReactFlow = toReactFlow;
|
|
1850
2098
|
exports.useQueryParamBoolean = useQueryParamBoolean;
|
|
1851
2099
|
exports.useQueryParamString = useQueryParamString;
|
|
1852
2100
|
exports.useWorkbenchBridge = useWorkbenchBridge;
|