@bian-womp/spark-workbench 0.1.10 → 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 +351 -155
- package/lib/cjs/index.cjs.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.map +1 -1
- package/lib/esm/index.js +350 -154
- package/lib/esm/index.js.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.map +1 -1
- package/package.json +4 -4
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,20 @@ 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
|
+
try {
|
|
347
|
+
this.runtime.pause();
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
console.error("Failed to pause runtime");
|
|
351
|
+
}
|
|
374
352
|
this.runtime.update(def, this.registry);
|
|
353
|
+
try {
|
|
354
|
+
this.runtime.resume();
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
console.error("Failed to resume runtime");
|
|
358
|
+
}
|
|
375
359
|
this.emit("invalidate", { reason: "graph-updated" });
|
|
376
360
|
return;
|
|
377
361
|
}
|
|
@@ -434,6 +418,8 @@ class GraphRunner {
|
|
|
434
418
|
// Remote: build remotely then launch
|
|
435
419
|
void this.ensureRemote().then(async (rc) => {
|
|
436
420
|
await rc.runner.build(def);
|
|
421
|
+
// Signal UI after remote build as well
|
|
422
|
+
this.emit("invalidate", { reason: "graph-built" });
|
|
437
423
|
const eng = rc.runner.getEngine();
|
|
438
424
|
if (!rc.listenersBound) {
|
|
439
425
|
eng.on("value", (e) => {
|
|
@@ -463,8 +449,13 @@ class GraphRunner {
|
|
|
463
449
|
if (!this.stagedInputs[nodeId])
|
|
464
450
|
this.stagedInputs[nodeId] = {};
|
|
465
451
|
this.stagedInputs[nodeId][handle] = value;
|
|
466
|
-
if (this.engine)
|
|
452
|
+
if (this.engine) {
|
|
467
453
|
this.engine.setInput(nodeId, handle, value);
|
|
454
|
+
}
|
|
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" });
|
|
458
|
+
}
|
|
468
459
|
}
|
|
469
460
|
async step() {
|
|
470
461
|
if (this.backend.kind !== "local")
|
|
@@ -780,6 +771,98 @@ function useQueryParamString(key, defaultValue) {
|
|
|
780
771
|
return [val, set];
|
|
781
772
|
}
|
|
782
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
|
+
|
|
783
866
|
const WorkbenchContext = React.createContext(null);
|
|
784
867
|
function useWorkbenchContext() {
|
|
785
868
|
const ctx = React.useContext(WorkbenchContext);
|
|
@@ -808,6 +891,19 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
808
891
|
const def = wb.export();
|
|
809
892
|
const inputsMap = React.useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
|
|
810
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]);
|
|
811
907
|
// Auto layout (simple layered layout)
|
|
812
908
|
const runAutoLayout = React.useCallback(() => {
|
|
813
909
|
const cur = wb.export();
|
|
@@ -875,20 +971,20 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
875
971
|
const off2 = runner.on("error", (e) => {
|
|
876
972
|
const edgeError = e;
|
|
877
973
|
const nodeError = e;
|
|
878
|
-
if (edgeError
|
|
974
|
+
if (edgeError.kind === "edge-convert") {
|
|
879
975
|
const edgeId = edgeError.edgeId;
|
|
880
976
|
setEdgeStatus((s) => ({
|
|
881
977
|
...s,
|
|
882
978
|
[edgeId]: { ...(s[edgeId] ?? {}), lastError: edgeError.err },
|
|
883
979
|
}));
|
|
884
980
|
}
|
|
885
|
-
else if (nodeError
|
|
886
|
-
const nodeId = nodeError
|
|
981
|
+
else if (nodeError.nodeId) {
|
|
982
|
+
const nodeId = nodeError.nodeId;
|
|
887
983
|
setNodeStatus((s) => ({
|
|
888
984
|
...s,
|
|
889
985
|
[nodeId]: {
|
|
890
986
|
...(s[nodeId] ?? {}),
|
|
891
|
-
lastError: nodeError
|
|
987
|
+
lastError: nodeError.err,
|
|
892
988
|
},
|
|
893
989
|
}));
|
|
894
990
|
}
|
|
@@ -956,6 +1052,17 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
956
1052
|
return add("runner", "stats")(s);
|
|
957
1053
|
});
|
|
958
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
|
+
});
|
|
959
1066
|
const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
960
1067
|
const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
961
1068
|
const off5b = wb.on("validationChanged", (r) => setValidation(r));
|
|
@@ -972,6 +1079,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
972
1079
|
off3b();
|
|
973
1080
|
off4();
|
|
974
1081
|
off4b();
|
|
1082
|
+
off4c();
|
|
975
1083
|
off5();
|
|
976
1084
|
off5b();
|
|
977
1085
|
off6();
|
|
@@ -994,10 +1102,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
994
1102
|
if (!validation)
|
|
995
1103
|
return { inputs, outputs, issues };
|
|
996
1104
|
for (const is of validation.issues ?? []) {
|
|
997
|
-
const d = is
|
|
998
|
-
const level = is
|
|
999
|
-
const code = String(is
|
|
1000
|
-
const message = String(is
|
|
1105
|
+
const d = is.data;
|
|
1106
|
+
const level = is.level;
|
|
1107
|
+
const code = String(is.code ?? "");
|
|
1108
|
+
const message = String(is.message ?? code);
|
|
1001
1109
|
if (!d)
|
|
1002
1110
|
continue;
|
|
1003
1111
|
if (d.nodeId) {
|
|
@@ -1026,10 +1134,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1026
1134
|
if (!validation)
|
|
1027
1135
|
return list;
|
|
1028
1136
|
for (const is of validation.issues ?? []) {
|
|
1029
|
-
const d = is
|
|
1030
|
-
const level = is
|
|
1031
|
-
const code = String(is
|
|
1032
|
-
const message = String(is
|
|
1137
|
+
const d = is.data;
|
|
1138
|
+
const level = is.level;
|
|
1139
|
+
const code = String(is.code ?? "");
|
|
1140
|
+
const message = String(is.message ?? code);
|
|
1033
1141
|
if (!d || (!d.nodeId && !d.edgeId)) {
|
|
1034
1142
|
list.push({ level, code, message });
|
|
1035
1143
|
}
|
|
@@ -1042,10 +1150,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1042
1150
|
if (!validation)
|
|
1043
1151
|
return { errors, issues };
|
|
1044
1152
|
for (const is of validation.issues ?? []) {
|
|
1045
|
-
const d = is
|
|
1046
|
-
const level = is
|
|
1047
|
-
const code = String(is
|
|
1048
|
-
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);
|
|
1049
1157
|
if (d?.edgeId) {
|
|
1050
1158
|
if (level === "error")
|
|
1051
1159
|
errors[d.edgeId] = true;
|
|
@@ -1156,6 +1264,16 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
|
|
|
1156
1264
|
}
|
|
1157
1265
|
|
|
1158
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 ?? "");
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
return String(value ?? "");
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1159
1277
|
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
|
|
1160
1278
|
const nodeValidationIssues = validationByNode.issues;
|
|
1161
1279
|
const edgeValidationIssues = validationByEdge.issues;
|
|
@@ -1215,7 +1333,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1215
1333
|
for (const h of handles) {
|
|
1216
1334
|
const typeId = desc?.inputs?.[h];
|
|
1217
1335
|
const current = nodeInputs[h];
|
|
1218
|
-
const display =
|
|
1336
|
+
const display = safeToString(typeId, current);
|
|
1219
1337
|
const wasOriginal = originals[h];
|
|
1220
1338
|
const isDirty = drafts[h] !== undefined &&
|
|
1221
1339
|
wasOriginal !== undefined &&
|
|
@@ -1241,7 +1359,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1241
1359
|
disabled: isLinked,
|
|
1242
1360
|
};
|
|
1243
1361
|
const current = nodeInputs[h];
|
|
1244
|
-
const value = drafts[h] ??
|
|
1362
|
+
const value = drafts[h] ?? safeToString(typeId, current);
|
|
1245
1363
|
const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
|
|
1246
1364
|
const commit = () => {
|
|
1247
1365
|
const draft = drafts[h];
|
|
@@ -1251,7 +1369,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1251
1369
|
setOriginals((o) => ({ ...o, [h]: draft }));
|
|
1252
1370
|
};
|
|
1253
1371
|
const revert = () => {
|
|
1254
|
-
const orig = originals[h] ??
|
|
1372
|
+
const orig = originals[h] ?? safeToString(typeId, current);
|
|
1255
1373
|
setDrafts((d) => ({ ...d, [h]: orig }));
|
|
1256
1374
|
};
|
|
1257
1375
|
const isEnum = typeId?.includes("enum:");
|
|
@@ -1261,19 +1379,17 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1261
1379
|
const title = inIssues
|
|
1262
1380
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1263
1381
|
.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;
|
|
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);
|
|
1272
1387
|
setInput(h, raw);
|
|
1273
|
-
|
|
1388
|
+
// keep drafts/originals in sync with label for display elsewhere
|
|
1389
|
+
const display = safeToString(typeId, raw);
|
|
1274
1390
|
setDrafts((d) => ({ ...d, [h]: display }));
|
|
1275
1391
|
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.
|
|
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) => {
|
|
1277
1393
|
if (e.key === "Enter")
|
|
1278
1394
|
commit();
|
|
1279
1395
|
if (e.key === "Escape")
|
|
@@ -1291,74 +1407,8 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1291
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 }) }))] }));
|
|
1292
1408
|
}
|
|
1293
1409
|
|
|
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
1410
|
const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
|
|
1361
|
-
const { typeId, showValues, inputValues, outputValues, toString
|
|
1411
|
+
const { typeId, showValues, inputValues, outputValues, toString } = data;
|
|
1362
1412
|
const inputEntries = data.inputHandles ?? [];
|
|
1363
1413
|
const outputEntries = data.outputHandles ?? [];
|
|
1364
1414
|
const status = data.status ?? {};
|
|
@@ -1374,19 +1424,26 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1374
1424
|
const minWidth = data.showValues ? 320 : 160;
|
|
1375
1425
|
const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
|
|
1376
1426
|
const hasError = !!status.lastError;
|
|
1427
|
+
const hasValidationError = validation.issues.some((i) => i.level === "error");
|
|
1428
|
+
const hasValidationWarning = !hasValidationError && validation.issues.length > 0;
|
|
1377
1429
|
const isRunning = !!status.running;
|
|
1378
1430
|
const isInvalid = !!status.invalidated && !isRunning && !hasError;
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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"
|
|
1383
1438
|
: isRunning
|
|
1384
|
-
? "border-
|
|
1385
|
-
:
|
|
1386
|
-
|
|
1387
|
-
|
|
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);
|
|
1388
1445
|
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
|
|
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")
|
|
1390
1447
|
? "error"
|
|
1391
1448
|
: "warning", size: 12, className: "w-3 h-3", title: validation.issues
|
|
1392
1449
|
.map((v) => `${v.code}: ${v.message}`)
|
|
@@ -1397,8 +1454,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1397
1454
|
const title = vIssues
|
|
1398
1455
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1399
1456
|
.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}`));
|
|
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}`));
|
|
1402
1458
|
}), outputEntries.map((entry, i) => {
|
|
1403
1459
|
const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
|
|
1404
1460
|
const hasAny = vIssues.length > 0;
|
|
@@ -1406,8 +1462,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1406
1462
|
const title = vIssues
|
|
1407
1463
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1408
1464
|
.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}`));
|
|
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}`));
|
|
1411
1466
|
})] }));
|
|
1412
1467
|
});
|
|
1413
1468
|
DefaultNode.displayName = "DefaultNode";
|
|
@@ -1415,15 +1470,135 @@ DefaultNode.displayName = "DefaultNode";
|
|
|
1415
1470
|
function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
|
|
1416
1471
|
const { registry } = useWorkbenchContext();
|
|
1417
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]);
|
|
1418
1513
|
if (!open || !clientPos)
|
|
1419
1514
|
return null;
|
|
1420
|
-
|
|
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);
|
|
1421
1521
|
const handleClick = (typeId) => {
|
|
1422
|
-
|
|
1522
|
+
// project() is deprecated; use screenToFlowPosition for screen coordinates
|
|
1523
|
+
const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
|
|
1423
1524
|
onAdd(typeId, p);
|
|
1424
1525
|
onClose();
|
|
1425
1526
|
};
|
|
1426
|
-
return (jsxRuntime.jsxs("div", { className: "fixed z-[1000] bg-white border border-gray-300 rounded-
|
|
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);
|
|
1586
|
+
}
|
|
1587
|
+
catch { }
|
|
1588
|
+
onClose();
|
|
1589
|
+
};
|
|
1590
|
+
const canRunPull = engineKind()?.toString() === "pull";
|
|
1591
|
+
const handleRunPull = async () => {
|
|
1592
|
+
try {
|
|
1593
|
+
await runner.computeNode(nodeId);
|
|
1594
|
+
}
|
|
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" })] }));
|
|
1427
1602
|
}
|
|
1428
1603
|
|
|
1429
1604
|
function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
@@ -1441,18 +1616,21 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
|
1441
1616
|
if (renderer)
|
|
1442
1617
|
custom.set(typeId, renderer);
|
|
1443
1618
|
}
|
|
1444
|
-
const types = {
|
|
1619
|
+
const types = {
|
|
1620
|
+
"spark-default": DefaultNode,
|
|
1621
|
+
default: DefaultNode,
|
|
1622
|
+
};
|
|
1445
1623
|
for (const [typeId, comp] of custom.entries()) {
|
|
1446
|
-
types[`spark
|
|
1624
|
+
types[`spark-${typeId}`] = comp;
|
|
1447
1625
|
}
|
|
1448
|
-
const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark
|
|
1626
|
+
const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark-${nodeTypeId}` : "spark-default";
|
|
1449
1627
|
return { nodeTypes: types, resolveNodeType: resolver };
|
|
1450
1628
|
// registry is stable; ui renderers expected to be set up before mount
|
|
1451
1629
|
}, [wb, registry]);
|
|
1452
1630
|
const { nodes, edges } = React.useMemo(() => {
|
|
1453
1631
|
const def = wb.export();
|
|
1454
1632
|
const sel = wb.getSelection();
|
|
1455
|
-
return toReactFlow(def, wb.getPositions(), registry,
|
|
1633
|
+
return toReactFlow(def, wb.getPositions(), registry, {
|
|
1456
1634
|
showValues,
|
|
1457
1635
|
inputs: ioValues.inputs,
|
|
1458
1636
|
outputs: ioValues.outputs,
|
|
@@ -1463,6 +1641,8 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
|
1463
1641
|
edgeStatus,
|
|
1464
1642
|
nodeValidation,
|
|
1465
1643
|
edgeValidation,
|
|
1644
|
+
selectedNodeIds: new Set(sel.nodes),
|
|
1645
|
+
selectedEdgeIds: new Set(sel.edges),
|
|
1466
1646
|
});
|
|
1467
1647
|
}, [
|
|
1468
1648
|
showValues,
|
|
@@ -1477,15 +1657,31 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
|
1477
1657
|
]);
|
|
1478
1658
|
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
1479
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);
|
|
1480
1663
|
const onContextMenu = (e) => {
|
|
1481
1664
|
e.preventDefault();
|
|
1482
|
-
|
|
1483
|
-
|
|
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);
|
|
1674
|
+
}
|
|
1675
|
+
else {
|
|
1676
|
+
setMenuPos({ x: e.clientX, y: e.clientY });
|
|
1677
|
+
setMenuOpen(true);
|
|
1678
|
+
setNodeMenuOpen(false);
|
|
1679
|
+
}
|
|
1484
1680
|
};
|
|
1485
1681
|
const addNodeAt = (typeId, pos) => {
|
|
1486
1682
|
wb.addNode({ typeId, position: pos });
|
|
1487
1683
|
};
|
|
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) })] }) }));
|
|
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) })] }) }));
|
|
1489
1685
|
}
|
|
1490
1686
|
|
|
1491
1687
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, }) {
|
|
@@ -1778,7 +1974,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1778
1974
|
return String(value);
|
|
1779
1975
|
}, [registry]);
|
|
1780
1976
|
const baseToElement = React.useCallback((typeId, value) => {
|
|
1781
|
-
return jsxRuntime.jsx("span", { children: baseToString(typeId, value) });
|
|
1977
|
+
return (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: baseToString(typeId, value) }));
|
|
1782
1978
|
}, [baseToString]);
|
|
1783
1979
|
const toString = React.useMemo(() => {
|
|
1784
1980
|
if (overrides?.toString)
|
|
@@ -1809,7 +2005,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1809
2005
|
catch (err) {
|
|
1810
2006
|
alert(String(err?.message ?? err));
|
|
1811
2007
|
}
|
|
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 }
|
|
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 })] })] }));
|
|
1813
2009
|
}
|
|
1814
2010
|
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
|
|
1815
2011
|
const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
|
|
@@ -1841,12 +2037,12 @@ exports.DefaultUIExtensionRegistry = DefaultUIExtensionRegistry;
|
|
|
1841
2037
|
exports.GraphRunner = GraphRunner;
|
|
1842
2038
|
exports.InMemoryWorkbench = InMemoryWorkbench;
|
|
1843
2039
|
exports.Inspector = Inspector;
|
|
1844
|
-
exports.ReactFlowWorkbench = ReactFlowWorkbench;
|
|
1845
2040
|
exports.WorkbenchCanvas = WorkbenchCanvas;
|
|
1846
2041
|
exports.WorkbenchContext = WorkbenchContext;
|
|
1847
2042
|
exports.WorkbenchProvider = WorkbenchProvider;
|
|
1848
2043
|
exports.WorkbenchStudio = WorkbenchStudio;
|
|
1849
|
-
exports.
|
|
2044
|
+
exports.getNodeBorderClassNames = getNodeBorderClassNames;
|
|
2045
|
+
exports.toReactFlow = toReactFlow;
|
|
1850
2046
|
exports.useQueryParamBoolean = useQueryParamBoolean;
|
|
1851
2047
|
exports.useQueryParamString = useQueryParamString;
|
|
1852
2048
|
exports.useWorkbenchBridge = useWorkbenchBridge;
|