@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/esm/index.js
CHANGED
|
@@ -317,37 +317,6 @@ class CLIWorkbench {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
function toReactFlow$1(def, positions = {}) {
|
|
321
|
-
const nodes = def.nodes.map((n) => ({
|
|
322
|
-
id: n.nodeId,
|
|
323
|
-
data: { typeId: n.typeId, params: n.params },
|
|
324
|
-
position: positions[n.nodeId] ?? { x: 0, y: 0 },
|
|
325
|
-
}));
|
|
326
|
-
const edges = def.edges.map((e) => ({
|
|
327
|
-
id: e.id,
|
|
328
|
-
source: e.source.nodeId,
|
|
329
|
-
target: e.target.nodeId,
|
|
330
|
-
sourceHandle: e.source.handle,
|
|
331
|
-
targetHandle: e.target.handle,
|
|
332
|
-
}));
|
|
333
|
-
return { nodes, edges };
|
|
334
|
-
}
|
|
335
|
-
class ReactFlowWorkbench {
|
|
336
|
-
constructor(wb) {
|
|
337
|
-
this.wb = wb;
|
|
338
|
-
}
|
|
339
|
-
get actions() {
|
|
340
|
-
return this.wb;
|
|
341
|
-
}
|
|
342
|
-
async load(def) {
|
|
343
|
-
await this.wb.load(def);
|
|
344
|
-
}
|
|
345
|
-
export(def) {
|
|
346
|
-
const d = def ?? this.wb.export();
|
|
347
|
-
return toReactFlow$1(d);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
320
|
class GraphRunner {
|
|
352
321
|
constructor(registry, backend) {
|
|
353
322
|
this.registry = registry;
|
|
@@ -361,6 +330,8 @@ class GraphRunner {
|
|
|
361
330
|
if (this.backend.kind === "local") {
|
|
362
331
|
const builder = new GraphBuilder(this.registry);
|
|
363
332
|
this.runtime = builder.build(def);
|
|
333
|
+
// Signal UI that freshly built graph should be considered invalidated
|
|
334
|
+
this.emit("invalidate", { reason: "graph-built" });
|
|
364
335
|
return;
|
|
365
336
|
}
|
|
366
337
|
// Remote: no-op here; build is performed on remote server during launch
|
|
@@ -369,7 +340,20 @@ class GraphRunner {
|
|
|
369
340
|
if (this.backend.kind === "local") {
|
|
370
341
|
if (!this.runtime)
|
|
371
342
|
return;
|
|
343
|
+
// Prevent mid-run churn while wiring changes are applied
|
|
344
|
+
try {
|
|
345
|
+
this.runtime.pause();
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
console.error("Failed to pause runtime");
|
|
349
|
+
}
|
|
372
350
|
this.runtime.update(def, this.registry);
|
|
351
|
+
try {
|
|
352
|
+
this.runtime.resume();
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
console.error("Failed to resume runtime");
|
|
356
|
+
}
|
|
373
357
|
this.emit("invalidate", { reason: "graph-updated" });
|
|
374
358
|
return;
|
|
375
359
|
}
|
|
@@ -432,6 +416,8 @@ class GraphRunner {
|
|
|
432
416
|
// Remote: build remotely then launch
|
|
433
417
|
void this.ensureRemote().then(async (rc) => {
|
|
434
418
|
await rc.runner.build(def);
|
|
419
|
+
// Signal UI after remote build as well
|
|
420
|
+
this.emit("invalidate", { reason: "graph-built" });
|
|
435
421
|
const eng = rc.runner.getEngine();
|
|
436
422
|
if (!rc.listenersBound) {
|
|
437
423
|
eng.on("value", (e) => {
|
|
@@ -461,8 +447,13 @@ class GraphRunner {
|
|
|
461
447
|
if (!this.stagedInputs[nodeId])
|
|
462
448
|
this.stagedInputs[nodeId] = {};
|
|
463
449
|
this.stagedInputs[nodeId][handle] = value;
|
|
464
|
-
if (this.engine)
|
|
450
|
+
if (this.engine) {
|
|
465
451
|
this.engine.setInput(nodeId, handle, value);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
// Emit a value event so UI updates even when engine isn't running
|
|
455
|
+
this.emit("value", { nodeId, handle, value, io: "input" });
|
|
456
|
+
}
|
|
466
457
|
}
|
|
467
458
|
async step() {
|
|
468
459
|
if (this.backend.kind !== "local")
|
|
@@ -778,6 +769,98 @@ function useQueryParamString(key, defaultValue) {
|
|
|
778
769
|
return [val, set];
|
|
779
770
|
}
|
|
780
771
|
|
|
772
|
+
function toReactFlow(def, positions, registry, opts) {
|
|
773
|
+
const nodeHandleMap = {};
|
|
774
|
+
const nodes = def.nodes.map((n) => {
|
|
775
|
+
const desc = registry.nodes.get(n.typeId);
|
|
776
|
+
const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
777
|
+
const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
778
|
+
nodeHandleMap[n.nodeId] = {
|
|
779
|
+
inputs: new Set(inputHandles.map((h) => h.id)),
|
|
780
|
+
outputs: new Set(outputHandles.map((h) => h.id)),
|
|
781
|
+
};
|
|
782
|
+
return {
|
|
783
|
+
id: n.nodeId,
|
|
784
|
+
data: {
|
|
785
|
+
typeId: n.typeId,
|
|
786
|
+
params: n.params,
|
|
787
|
+
inputHandles,
|
|
788
|
+
outputHandles,
|
|
789
|
+
showValues: opts.showValues,
|
|
790
|
+
inputValues: opts.inputs?.[n.nodeId],
|
|
791
|
+
outputValues: opts.outputs?.[n.nodeId],
|
|
792
|
+
status: opts.nodeStatus?.[n.nodeId],
|
|
793
|
+
validation: {
|
|
794
|
+
inputs: opts.nodeValidation?.inputs?.[n.nodeId] ?? [],
|
|
795
|
+
outputs: opts.nodeValidation?.outputs?.[n.nodeId] ?? [],
|
|
796
|
+
issues: opts.nodeValidation?.issues?.[n.nodeId] ?? [],
|
|
797
|
+
},
|
|
798
|
+
toString: opts.toString,
|
|
799
|
+
toElement: opts.toElement,
|
|
800
|
+
},
|
|
801
|
+
position: positions[n.nodeId] ?? { x: 0, y: 0 },
|
|
802
|
+
type: opts.resolveNodeType?.(n.typeId) ?? "spark-default",
|
|
803
|
+
selected: opts.selectedNodeIds
|
|
804
|
+
? opts.selectedNodeIds.has(n.nodeId)
|
|
805
|
+
: undefined,
|
|
806
|
+
};
|
|
807
|
+
});
|
|
808
|
+
const edges = def.edges
|
|
809
|
+
.filter((e) => {
|
|
810
|
+
const src = nodeHandleMap[e.source.nodeId];
|
|
811
|
+
const dst = nodeHandleMap[e.target.nodeId];
|
|
812
|
+
if (!src || !dst)
|
|
813
|
+
return false;
|
|
814
|
+
return (src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle));
|
|
815
|
+
})
|
|
816
|
+
.map((e) => {
|
|
817
|
+
const st = opts.edgeStatus?.[e.id];
|
|
818
|
+
const isRunning = !!st?.running;
|
|
819
|
+
const hasError = !!st?.lastError;
|
|
820
|
+
const isInvalidEdge = !!opts.edgeValidation?.[e.id];
|
|
821
|
+
const style = hasError || isInvalidEdge
|
|
822
|
+
? { stroke: "#ef4444", strokeWidth: 2 }
|
|
823
|
+
: isRunning
|
|
824
|
+
? { stroke: "#3b82f6" }
|
|
825
|
+
: undefined;
|
|
826
|
+
return {
|
|
827
|
+
id: e.id,
|
|
828
|
+
source: e.source.nodeId,
|
|
829
|
+
target: e.target.nodeId,
|
|
830
|
+
sourceHandle: e.source.handle,
|
|
831
|
+
targetHandle: e.target.handle,
|
|
832
|
+
selected: opts.selectedEdgeIds
|
|
833
|
+
? opts.selectedEdgeIds.has(e.id)
|
|
834
|
+
: undefined,
|
|
835
|
+
animated: isRunning,
|
|
836
|
+
style,
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
return { nodes, edges };
|
|
840
|
+
}
|
|
841
|
+
// Shared node container border class composition for consistent visuals
|
|
842
|
+
function getNodeBorderClassNames(args) {
|
|
843
|
+
const selected = !!args.selected;
|
|
844
|
+
const status = args.status || {};
|
|
845
|
+
const issues = args.validation?.issues ?? [];
|
|
846
|
+
const hasError = !!status.lastError;
|
|
847
|
+
const hasValidationError = issues.some((i) => i?.level === "error");
|
|
848
|
+
const hasValidationWarning = !hasValidationError && issues.length > 0;
|
|
849
|
+
const isRunning = !!status.running;
|
|
850
|
+
const isInvalid = !!status.invalidated && !isRunning && !hasError;
|
|
851
|
+
const borderWidth = selected ? "border-2" : "border";
|
|
852
|
+
const borderStyle = isInvalid ? "border-dashed" : "border-solid";
|
|
853
|
+
const borderColor = hasError || hasValidationError
|
|
854
|
+
? "border-red-500"
|
|
855
|
+
: hasValidationWarning
|
|
856
|
+
? "border-amber-500"
|
|
857
|
+
: isRunning
|
|
858
|
+
? "border-blue-500"
|
|
859
|
+
: "border-gray-500 dark:border-gray-400";
|
|
860
|
+
const ring = isRunning ? " ring-2 ring-blue-200 dark:ring-blue-900" : "";
|
|
861
|
+
return `${borderWidth} ${borderStyle} ${borderColor}${ring}`.trim();
|
|
862
|
+
}
|
|
863
|
+
|
|
781
864
|
const WorkbenchContext = createContext(null);
|
|
782
865
|
function useWorkbenchContext() {
|
|
783
866
|
const ctx = useContext(WorkbenchContext);
|
|
@@ -806,6 +889,19 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
806
889
|
const def = wb.export();
|
|
807
890
|
const inputsMap = useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
|
|
808
891
|
const outputsMap = useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
|
|
892
|
+
// Initialize nodes as invalidated by default until first successful run
|
|
893
|
+
useEffect(() => {
|
|
894
|
+
setNodeStatus((prev) => {
|
|
895
|
+
const next = { ...prev };
|
|
896
|
+
for (const n of def.nodes) {
|
|
897
|
+
const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
|
|
898
|
+
if (cur.invalidated === undefined) {
|
|
899
|
+
next[n.nodeId] = { ...cur, invalidated: true };
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return next;
|
|
903
|
+
});
|
|
904
|
+
}, [def]);
|
|
809
905
|
// Auto layout (simple layered layout)
|
|
810
906
|
const runAutoLayout = useCallback(() => {
|
|
811
907
|
const cur = wb.export();
|
|
@@ -873,20 +969,20 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
873
969
|
const off2 = runner.on("error", (e) => {
|
|
874
970
|
const edgeError = e;
|
|
875
971
|
const nodeError = e;
|
|
876
|
-
if (edgeError
|
|
972
|
+
if (edgeError.kind === "edge-convert") {
|
|
877
973
|
const edgeId = edgeError.edgeId;
|
|
878
974
|
setEdgeStatus((s) => ({
|
|
879
975
|
...s,
|
|
880
976
|
[edgeId]: { ...(s[edgeId] ?? {}), lastError: edgeError.err },
|
|
881
977
|
}));
|
|
882
978
|
}
|
|
883
|
-
else if (nodeError
|
|
884
|
-
const nodeId = nodeError
|
|
979
|
+
else if (nodeError.nodeId) {
|
|
980
|
+
const nodeId = nodeError.nodeId;
|
|
885
981
|
setNodeStatus((s) => ({
|
|
886
982
|
...s,
|
|
887
983
|
[nodeId]: {
|
|
888
984
|
...(s[nodeId] ?? {}),
|
|
889
|
-
lastError: nodeError
|
|
985
|
+
lastError: nodeError.err,
|
|
890
986
|
},
|
|
891
987
|
}));
|
|
892
988
|
}
|
|
@@ -954,6 +1050,17 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
954
1050
|
return add("runner", "stats")(s);
|
|
955
1051
|
});
|
|
956
1052
|
const off4 = wb.on("graphChanged", add("workbench", "graphChanged"));
|
|
1053
|
+
// Ensure newly added nodes start as invalidated until first evaluation
|
|
1054
|
+
const off4c = wb.on("graphChanged", (e) => {
|
|
1055
|
+
const change = e.change;
|
|
1056
|
+
if (change?.type === "addNode" && typeof change.nodeId === "string") {
|
|
1057
|
+
const id = change.nodeId;
|
|
1058
|
+
setNodeStatus((s) => ({
|
|
1059
|
+
...s,
|
|
1060
|
+
[id]: { ...(s[id] ?? {}), invalidated: true },
|
|
1061
|
+
}));
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
957
1064
|
const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
958
1065
|
const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
959
1066
|
const off5b = wb.on("validationChanged", (r) => setValidation(r));
|
|
@@ -970,6 +1077,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
970
1077
|
off3b();
|
|
971
1078
|
off4();
|
|
972
1079
|
off4b();
|
|
1080
|
+
off4c();
|
|
973
1081
|
off5();
|
|
974
1082
|
off5b();
|
|
975
1083
|
off6();
|
|
@@ -992,10 +1100,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
992
1100
|
if (!validation)
|
|
993
1101
|
return { inputs, outputs, issues };
|
|
994
1102
|
for (const is of validation.issues ?? []) {
|
|
995
|
-
const d = is
|
|
996
|
-
const level = is
|
|
997
|
-
const code = String(is
|
|
998
|
-
const message = String(is
|
|
1103
|
+
const d = is.data;
|
|
1104
|
+
const level = is.level;
|
|
1105
|
+
const code = String(is.code ?? "");
|
|
1106
|
+
const message = String(is.message ?? code);
|
|
999
1107
|
if (!d)
|
|
1000
1108
|
continue;
|
|
1001
1109
|
if (d.nodeId) {
|
|
@@ -1024,10 +1132,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1024
1132
|
if (!validation)
|
|
1025
1133
|
return list;
|
|
1026
1134
|
for (const is of validation.issues ?? []) {
|
|
1027
|
-
const d = is
|
|
1028
|
-
const level = is
|
|
1029
|
-
const code = String(is
|
|
1030
|
-
const message = String(is
|
|
1135
|
+
const d = is.data;
|
|
1136
|
+
const level = is.level;
|
|
1137
|
+
const code = String(is.code ?? "");
|
|
1138
|
+
const message = String(is.message ?? code);
|
|
1031
1139
|
if (!d || (!d.nodeId && !d.edgeId)) {
|
|
1032
1140
|
list.push({ level, code, message });
|
|
1033
1141
|
}
|
|
@@ -1040,10 +1148,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1040
1148
|
if (!validation)
|
|
1041
1149
|
return { errors, issues };
|
|
1042
1150
|
for (const is of validation.issues ?? []) {
|
|
1043
|
-
const d = is
|
|
1044
|
-
const level = is
|
|
1045
|
-
const code = String(is
|
|
1046
|
-
const message = String(is
|
|
1151
|
+
const d = is.data;
|
|
1152
|
+
const level = is.level;
|
|
1153
|
+
const code = String(is.code ?? "");
|
|
1154
|
+
const message = String(is.message ?? code);
|
|
1047
1155
|
if (d?.edgeId) {
|
|
1048
1156
|
if (level === "error")
|
|
1049
1157
|
errors[d.edgeId] = true;
|
|
@@ -1154,6 +1262,16 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
|
|
|
1154
1262
|
}
|
|
1155
1263
|
|
|
1156
1264
|
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, setInput, }) {
|
|
1265
|
+
const safeToString = (typeId, value) => {
|
|
1266
|
+
try {
|
|
1267
|
+
return typeof toString === "function"
|
|
1268
|
+
? toString(typeId, value)
|
|
1269
|
+
: String(value ?? "");
|
|
1270
|
+
}
|
|
1271
|
+
catch {
|
|
1272
|
+
return String(value ?? "");
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1157
1275
|
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
|
|
1158
1276
|
const nodeValidationIssues = validationByNode.issues;
|
|
1159
1277
|
const edgeValidationIssues = validationByEdge.issues;
|
|
@@ -1213,7 +1331,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1213
1331
|
for (const h of handles) {
|
|
1214
1332
|
const typeId = desc?.inputs?.[h];
|
|
1215
1333
|
const current = nodeInputs[h];
|
|
1216
|
-
const display =
|
|
1334
|
+
const display = safeToString(typeId, current);
|
|
1217
1335
|
const wasOriginal = originals[h];
|
|
1218
1336
|
const isDirty = drafts[h] !== undefined &&
|
|
1219
1337
|
wasOriginal !== undefined &&
|
|
@@ -1239,7 +1357,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1239
1357
|
disabled: isLinked,
|
|
1240
1358
|
};
|
|
1241
1359
|
const current = nodeInputs[h];
|
|
1242
|
-
const value = drafts[h] ??
|
|
1360
|
+
const value = drafts[h] ?? safeToString(typeId, current);
|
|
1243
1361
|
const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
|
|
1244
1362
|
const commit = () => {
|
|
1245
1363
|
const draft = drafts[h];
|
|
@@ -1249,7 +1367,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1249
1367
|
setOriginals((o) => ({ ...o, [h]: draft }));
|
|
1250
1368
|
};
|
|
1251
1369
|
const revert = () => {
|
|
1252
|
-
const orig = originals[h] ??
|
|
1370
|
+
const orig = originals[h] ?? safeToString(typeId, current);
|
|
1253
1371
|
setDrafts((d) => ({ ...d, [h]: orig }));
|
|
1254
1372
|
};
|
|
1255
1373
|
const isEnum = typeId?.includes("enum:");
|
|
@@ -1259,19 +1377,17 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1259
1377
|
const title = inIssues
|
|
1260
1378
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1261
1379
|
.join("; ");
|
|
1262
|
-
return (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-28", children: [h, jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (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:
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
let raw = (byLabel !== undefined ? byLabel : Number(label));
|
|
1268
|
-
if (!Number.isFinite(raw))
|
|
1269
|
-
raw = undefined;
|
|
1380
|
+
return (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-28", children: [h, jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (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
|
|
1381
|
+
? String(current)
|
|
1382
|
+
: "", onChange: (e) => {
|
|
1383
|
+
const val = e.target.value;
|
|
1384
|
+
const raw = val === "" ? undefined : Number(val);
|
|
1270
1385
|
setInput(h, raw);
|
|
1271
|
-
|
|
1386
|
+
// keep drafts/originals in sync with label for display elsewhere
|
|
1387
|
+
const display = safeToString(typeId, raw);
|
|
1272
1388
|
setDrafts((d) => ({ ...d, [h]: display }));
|
|
1273
1389
|
setOriginals((o) => ({ ...o, [h]: display }));
|
|
1274
|
-
}, ...commonProps, children: [jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsx("option", { value: opt.
|
|
1390
|
+
}, ...commonProps, children: [jsx("option", { value: "", children: "(select)" }), registry.enums.get(typeId)?.options.map((opt) => (jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] })) : isLinked ? (toElement(typeId, current)) : (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) => {
|
|
1275
1391
|
if (e.key === "Enter")
|
|
1276
1392
|
commit();
|
|
1277
1393
|
if (e.key === "Escape")
|
|
@@ -1289,74 +1405,8 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1289
1405
|
})()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) }), debug && (jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
|
|
1290
1406
|
}
|
|
1291
1407
|
|
|
1292
|
-
function toReactFlow(def, positions, registry, selectedNodeIds, selectedEdgeIds, opts) {
|
|
1293
|
-
const nodeHandleMap = {};
|
|
1294
|
-
const nodes = def.nodes.map((n) => {
|
|
1295
|
-
const desc = registry.nodes.get(n.typeId);
|
|
1296
|
-
const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
1297
|
-
const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
1298
|
-
nodeHandleMap[n.nodeId] = {
|
|
1299
|
-
inputs: new Set(inputHandles.map((h) => h.id)),
|
|
1300
|
-
outputs: new Set(outputHandles.map((h) => h.id)),
|
|
1301
|
-
};
|
|
1302
|
-
return {
|
|
1303
|
-
id: n.nodeId,
|
|
1304
|
-
data: {
|
|
1305
|
-
typeId: n.typeId,
|
|
1306
|
-
params: n.params,
|
|
1307
|
-
inputHandles,
|
|
1308
|
-
outputHandles,
|
|
1309
|
-
showValues: opts?.showValues,
|
|
1310
|
-
inputValues: opts?.inputs?.[n.nodeId],
|
|
1311
|
-
outputValues: opts?.outputs?.[n.nodeId],
|
|
1312
|
-
status: opts?.nodeStatus?.[n.nodeId],
|
|
1313
|
-
validation: {
|
|
1314
|
-
inputs: opts?.nodeValidation?.inputs?.[n.nodeId] ?? [],
|
|
1315
|
-
outputs: opts?.nodeValidation?.outputs?.[n.nodeId] ?? [],
|
|
1316
|
-
issues: opts?.nodeValidation?.issues?.[n.nodeId] ?? [],
|
|
1317
|
-
},
|
|
1318
|
-
toString: opts?.toString,
|
|
1319
|
-
toElement: opts?.toElement,
|
|
1320
|
-
},
|
|
1321
|
-
position: positions[n.nodeId] ?? { x: 0, y: 0 },
|
|
1322
|
-
type: opts?.resolveNodeType?.(n.typeId) ?? "spark:default",
|
|
1323
|
-
selected: selectedNodeIds ? selectedNodeIds.has(n.nodeId) : undefined,
|
|
1324
|
-
};
|
|
1325
|
-
});
|
|
1326
|
-
const edges = def.edges
|
|
1327
|
-
.filter((e) => {
|
|
1328
|
-
const src = nodeHandleMap[e.source.nodeId];
|
|
1329
|
-
const dst = nodeHandleMap[e.target.nodeId];
|
|
1330
|
-
if (!src || !dst)
|
|
1331
|
-
return false;
|
|
1332
|
-
return (src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle));
|
|
1333
|
-
})
|
|
1334
|
-
.map((e) => {
|
|
1335
|
-
const st = opts?.edgeStatus?.[e.id];
|
|
1336
|
-
const isRunning = !!st?.running;
|
|
1337
|
-
const hasError = !!st?.lastError;
|
|
1338
|
-
const isInvalidEdge = !!opts?.edgeValidation?.[e.id];
|
|
1339
|
-
const style = hasError || isInvalidEdge
|
|
1340
|
-
? { stroke: "#ef4444", strokeWidth: 2 }
|
|
1341
|
-
: isRunning
|
|
1342
|
-
? { stroke: "#3b82f6" }
|
|
1343
|
-
: undefined;
|
|
1344
|
-
return {
|
|
1345
|
-
id: e.id,
|
|
1346
|
-
source: e.source.nodeId,
|
|
1347
|
-
target: e.target.nodeId,
|
|
1348
|
-
sourceHandle: e.source.handle,
|
|
1349
|
-
targetHandle: e.target.handle,
|
|
1350
|
-
selected: selectedEdgeIds ? selectedEdgeIds.has(e.id) : undefined,
|
|
1351
|
-
animated: isRunning,
|
|
1352
|
-
style,
|
|
1353
|
-
};
|
|
1354
|
-
});
|
|
1355
|
-
return { nodes, edges };
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
1408
|
const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
|
|
1359
|
-
const { typeId, showValues, inputValues, outputValues, toString
|
|
1409
|
+
const { typeId, showValues, inputValues, outputValues, toString } = data;
|
|
1360
1410
|
const inputEntries = data.inputHandles ?? [];
|
|
1361
1411
|
const outputEntries = data.outputHandles ?? [];
|
|
1362
1412
|
const status = data.status ?? {};
|
|
@@ -1372,19 +1422,26 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1372
1422
|
const minWidth = data.showValues ? 320 : 160;
|
|
1373
1423
|
const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
|
|
1374
1424
|
const hasError = !!status.lastError;
|
|
1425
|
+
const hasValidationError = validation.issues.some((i) => i.level === "error");
|
|
1426
|
+
const hasValidationWarning = !hasValidationError && validation.issues.length > 0;
|
|
1375
1427
|
const isRunning = !!status.running;
|
|
1376
1428
|
const isInvalid = !!status.invalidated && !isRunning && !hasError;
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1429
|
+
// Border color encodes severity; thickness encodes selection; style (dashed) encodes invalidated
|
|
1430
|
+
const borderWidth = selected ? "border-2" : "border";
|
|
1431
|
+
const borderStyle = isInvalid ? "border-dashed" : "border-solid";
|
|
1432
|
+
const borderColor = hasError || hasValidationError
|
|
1433
|
+
? "border-red-500"
|
|
1434
|
+
: hasValidationWarning
|
|
1435
|
+
? "border-amber-500"
|
|
1381
1436
|
: isRunning
|
|
1382
|
-
? "border-
|
|
1383
|
-
:
|
|
1384
|
-
|
|
1385
|
-
|
|
1437
|
+
? "border-blue-500"
|
|
1438
|
+
: "border-gray-500 dark:border-gray-400";
|
|
1439
|
+
const ringClasses = isRunning
|
|
1440
|
+
? "ring-2 ring-blue-200 dark:ring-blue-900"
|
|
1441
|
+
: undefined;
|
|
1442
|
+
const borderClasses = cx(borderWidth, borderStyle, borderColor, ringClasses);
|
|
1386
1443
|
const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
|
|
1387
|
-
return (jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900
|
|
1444
|
+
return (jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900", borderClasses), style: { position: "relative", minHeight: minHeight, minWidth }, children: [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: [jsx("strong", { className: "flex-1 h-full leading-6 text-xs", children: typeId }), jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsx(XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
|
|
1388
1445
|
? "error"
|
|
1389
1446
|
: "warning", size: 12, className: "w-3 h-3", title: validation.issues
|
|
1390
1447
|
.map((v) => `${v.code}: ${v.message}`)
|
|
@@ -1395,8 +1452,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1395
1452
|
const title = vIssues
|
|
1396
1453
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1397
1454
|
.join("; ");
|
|
1398
|
-
return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "target", position: 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) } }), 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 && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues &&
|
|
1399
|
-
(toElement ? (toElement(entry.typeId, inputValues?.[entry.id])) : toString ? (jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) })) : (jsx("span", { className: "ml-1 opacity-60", children: String(inputValues?.[entry.id]) })))] })] }, `in-${entry.id}`));
|
|
1455
|
+
return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "target", position: 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) } }), 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 && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
|
|
1400
1456
|
}), outputEntries.map((entry, i) => {
|
|
1401
1457
|
const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
|
|
1402
1458
|
const hasAny = vIssues.length > 0;
|
|
@@ -1404,8 +1460,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1404
1460
|
const title = vIssues
|
|
1405
1461
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1406
1462
|
.join("; ");
|
|
1407
|
-
return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "source", position: 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) } }), 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 && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues &&
|
|
1408
|
-
(toElement ? (toElement(entry.typeId, outputValues?.[entry.id])) : toString ? (jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, outputValues?.[entry.id]) })) : (jsx("span", { className: "ml-1 opacity-60", children: String(outputValues?.[entry.id]) })))] })] }, `out-${entry.id}`));
|
|
1463
|
+
return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "source", position: 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) } }), 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 && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, outputValues?.[entry.id]) }))] })] }, `out-${entry.id}`));
|
|
1409
1464
|
})] }));
|
|
1410
1465
|
});
|
|
1411
1466
|
DefaultNode.displayName = "DefaultNode";
|
|
@@ -1413,15 +1468,135 @@ DefaultNode.displayName = "DefaultNode";
|
|
|
1413
1468
|
function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
|
|
1414
1469
|
const { registry } = useWorkbenchContext();
|
|
1415
1470
|
const rf = useReactFlow();
|
|
1471
|
+
const ids = Array.from(registry.nodes.keys());
|
|
1472
|
+
// Group node ids by the segment before the first '.'
|
|
1473
|
+
const grouped = {};
|
|
1474
|
+
for (const id of ids) {
|
|
1475
|
+
const parts = id.split(".");
|
|
1476
|
+
const cat = parts.length > 1 ? parts[0] : "other";
|
|
1477
|
+
const label = parts.length > 1 ? parts.slice(1).join(".") : id;
|
|
1478
|
+
(grouped[cat] = grouped[cat] || []).push({ id, label });
|
|
1479
|
+
}
|
|
1480
|
+
const cats = Object.keys(grouped).sort((a, b) => a.localeCompare(b));
|
|
1481
|
+
cats.forEach((c) => grouped[c].sort((a, b) => a.label.localeCompare(b.label)));
|
|
1482
|
+
const totalCount = ids.length;
|
|
1483
|
+
// Ref for focus/outside click handling
|
|
1484
|
+
const ref = useRef(null);
|
|
1485
|
+
// Close on outside click and on ESC
|
|
1486
|
+
useEffect(() => {
|
|
1487
|
+
if (!open)
|
|
1488
|
+
return;
|
|
1489
|
+
const onDown = (e) => {
|
|
1490
|
+
if (!ref.current)
|
|
1491
|
+
return;
|
|
1492
|
+
if (!ref.current.contains(e.target))
|
|
1493
|
+
onClose();
|
|
1494
|
+
};
|
|
1495
|
+
const onKey = (e) => {
|
|
1496
|
+
if (e.key === "Escape")
|
|
1497
|
+
onClose();
|
|
1498
|
+
};
|
|
1499
|
+
window.addEventListener("mousedown", onDown, true);
|
|
1500
|
+
window.addEventListener("keydown", onKey);
|
|
1501
|
+
return () => {
|
|
1502
|
+
window.removeEventListener("mousedown", onDown, true);
|
|
1503
|
+
window.removeEventListener("keydown", onKey);
|
|
1504
|
+
};
|
|
1505
|
+
}, [open, onClose]);
|
|
1506
|
+
// Focus for keyboard accessibility
|
|
1507
|
+
useEffect(() => {
|
|
1508
|
+
if (open)
|
|
1509
|
+
ref.current?.focus();
|
|
1510
|
+
}, [open]);
|
|
1416
1511
|
if (!open || !clientPos)
|
|
1417
1512
|
return null;
|
|
1418
|
-
|
|
1513
|
+
// Clamp menu position to viewport
|
|
1514
|
+
const MENU_MIN_WIDTH = 180;
|
|
1515
|
+
const PADDING = 16; // rough padding/shadow
|
|
1516
|
+
const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
|
|
1517
|
+
(MENU_MIN_WIDTH + PADDING));
|
|
1518
|
+
const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
|
|
1419
1519
|
const handleClick = (typeId) => {
|
|
1420
|
-
|
|
1520
|
+
// project() is deprecated; use screenToFlowPosition for screen coordinates
|
|
1521
|
+
const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
|
|
1421
1522
|
onAdd(typeId, p);
|
|
1422
1523
|
onClose();
|
|
1423
1524
|
};
|
|
1424
|
-
return (jsxs("div", { className: "fixed z-[1000] bg-white border border-gray-300 rounded-
|
|
1525
|
+
return (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) => {
|
|
1526
|
+
e.preventDefault();
|
|
1527
|
+
e.stopPropagation();
|
|
1528
|
+
}, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node ", jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsx("div", { className: "max-h-60 overflow-auto", children: cats.map((cat) => (jsxs("div", { className: "py-1", children: [jsxs("div", { className: "px-2 py-1 text-[11px] uppercase tracking-wide text-gray-400", children: [cat, " ", jsxs("span", { className: "opacity-60 normal-case", children: ["(", grouped[cat].length, ")"] })] }), grouped[cat].map(({ id, label }) => (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))) })] }));
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
|
|
1532
|
+
const { wb, runner, engineKind } = useWorkbenchContext();
|
|
1533
|
+
const ref = useRef(null);
|
|
1534
|
+
// outside click + ESC
|
|
1535
|
+
useEffect(() => {
|
|
1536
|
+
if (!open)
|
|
1537
|
+
return;
|
|
1538
|
+
const onDown = (e) => {
|
|
1539
|
+
if (!ref.current)
|
|
1540
|
+
return;
|
|
1541
|
+
if (!ref.current.contains(e.target))
|
|
1542
|
+
onClose();
|
|
1543
|
+
};
|
|
1544
|
+
const onKey = (e) => {
|
|
1545
|
+
if (e.key === "Escape")
|
|
1546
|
+
onClose();
|
|
1547
|
+
};
|
|
1548
|
+
window.addEventListener("mousedown", onDown, true);
|
|
1549
|
+
window.addEventListener("keydown", onKey);
|
|
1550
|
+
return () => {
|
|
1551
|
+
window.removeEventListener("mousedown", onDown, true);
|
|
1552
|
+
window.removeEventListener("keydown", onKey);
|
|
1553
|
+
};
|
|
1554
|
+
}, [open, onClose]);
|
|
1555
|
+
useEffect(() => {
|
|
1556
|
+
if (open)
|
|
1557
|
+
ref.current?.focus();
|
|
1558
|
+
}, [open]);
|
|
1559
|
+
if (!open || !clientPos || !nodeId)
|
|
1560
|
+
return null;
|
|
1561
|
+
// clamp
|
|
1562
|
+
const MENU_MIN_WIDTH = 180;
|
|
1563
|
+
const PADDING = 16;
|
|
1564
|
+
const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
|
|
1565
|
+
(MENU_MIN_WIDTH + PADDING));
|
|
1566
|
+
const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
|
|
1567
|
+
// actions
|
|
1568
|
+
const handleDelete = () => {
|
|
1569
|
+
wb.removeNode(nodeId);
|
|
1570
|
+
onClose();
|
|
1571
|
+
};
|
|
1572
|
+
const handleDuplicate = () => {
|
|
1573
|
+
const def = wb.export();
|
|
1574
|
+
const n = def.nodes.find((n) => n.nodeId === nodeId);
|
|
1575
|
+
if (!n)
|
|
1576
|
+
return onClose();
|
|
1577
|
+
const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
|
|
1578
|
+
wb.addNode({ typeId: n.typeId, params: n.params, position: { x: pos.x + 24, y: pos.y + 24 } });
|
|
1579
|
+
onClose();
|
|
1580
|
+
};
|
|
1581
|
+
const handleCopyId = async () => {
|
|
1582
|
+
try {
|
|
1583
|
+
await navigator.clipboard.writeText(nodeId);
|
|
1584
|
+
}
|
|
1585
|
+
catch { }
|
|
1586
|
+
onClose();
|
|
1587
|
+
};
|
|
1588
|
+
const canRunPull = engineKind()?.toString() === "pull";
|
|
1589
|
+
const handleRunPull = async () => {
|
|
1590
|
+
try {
|
|
1591
|
+
await runner.computeNode(nodeId);
|
|
1592
|
+
}
|
|
1593
|
+
catch { }
|
|
1594
|
+
onClose();
|
|
1595
|
+
};
|
|
1596
|
+
return (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) => {
|
|
1597
|
+
e.preventDefault();
|
|
1598
|
+
e.stopPropagation();
|
|
1599
|
+
}, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDuplicate, children: "Duplicate" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleCopyId, children: "Copy Node ID" })] }));
|
|
1425
1600
|
}
|
|
1426
1601
|
|
|
1427
1602
|
function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
@@ -1439,18 +1614,21 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
|
1439
1614
|
if (renderer)
|
|
1440
1615
|
custom.set(typeId, renderer);
|
|
1441
1616
|
}
|
|
1442
|
-
const types = {
|
|
1617
|
+
const types = {
|
|
1618
|
+
"spark-default": DefaultNode,
|
|
1619
|
+
default: DefaultNode,
|
|
1620
|
+
};
|
|
1443
1621
|
for (const [typeId, comp] of custom.entries()) {
|
|
1444
|
-
types[`spark
|
|
1622
|
+
types[`spark-${typeId}`] = comp;
|
|
1445
1623
|
}
|
|
1446
|
-
const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark
|
|
1624
|
+
const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark-${nodeTypeId}` : "spark-default";
|
|
1447
1625
|
return { nodeTypes: types, resolveNodeType: resolver };
|
|
1448
1626
|
// registry is stable; ui renderers expected to be set up before mount
|
|
1449
1627
|
}, [wb, registry]);
|
|
1450
1628
|
const { nodes, edges } = useMemo(() => {
|
|
1451
1629
|
const def = wb.export();
|
|
1452
1630
|
const sel = wb.getSelection();
|
|
1453
|
-
return toReactFlow(def, wb.getPositions(), registry,
|
|
1631
|
+
return toReactFlow(def, wb.getPositions(), registry, {
|
|
1454
1632
|
showValues,
|
|
1455
1633
|
inputs: ioValues.inputs,
|
|
1456
1634
|
outputs: ioValues.outputs,
|
|
@@ -1461,6 +1639,8 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
|
1461
1639
|
edgeStatus,
|
|
1462
1640
|
nodeValidation,
|
|
1463
1641
|
edgeValidation,
|
|
1642
|
+
selectedNodeIds: new Set(sel.nodes),
|
|
1643
|
+
selectedEdgeIds: new Set(sel.edges),
|
|
1464
1644
|
});
|
|
1465
1645
|
}, [
|
|
1466
1646
|
showValues,
|
|
@@ -1475,15 +1655,31 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
|
1475
1655
|
]);
|
|
1476
1656
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
1477
1657
|
const [menuPos, setMenuPos] = useState(null);
|
|
1658
|
+
const [nodeMenuOpen, setNodeMenuOpen] = useState(false);
|
|
1659
|
+
const [nodeMenuPos, setNodeMenuPos] = useState(null);
|
|
1660
|
+
const [nodeAtMenu, setNodeAtMenu] = useState(null);
|
|
1478
1661
|
const onContextMenu = (e) => {
|
|
1479
1662
|
e.preventDefault();
|
|
1480
|
-
|
|
1481
|
-
|
|
1663
|
+
// Determine if right-clicked over a node by hit-testing selection
|
|
1664
|
+
const target = e.target?.closest(".react-flow__node");
|
|
1665
|
+
if (target) {
|
|
1666
|
+
// Resolve node id from data-id attribute React Flow sets
|
|
1667
|
+
const nodeId = target.getAttribute("data-id");
|
|
1668
|
+
setNodeAtMenu(nodeId);
|
|
1669
|
+
setNodeMenuPos({ x: e.clientX, y: e.clientY });
|
|
1670
|
+
setNodeMenuOpen(true);
|
|
1671
|
+
setMenuOpen(false);
|
|
1672
|
+
}
|
|
1673
|
+
else {
|
|
1674
|
+
setMenuPos({ x: e.clientX, y: e.clientY });
|
|
1675
|
+
setMenuOpen(true);
|
|
1676
|
+
setNodeMenuOpen(false);
|
|
1677
|
+
}
|
|
1482
1678
|
};
|
|
1483
1679
|
const addNodeAt = (typeId, pos) => {
|
|
1484
1680
|
wb.addNode({ typeId, position: pos });
|
|
1485
1681
|
};
|
|
1486
|
-
return (jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: 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: [jsx(Background, {}), jsx(MiniMap, {}), jsx(Controls, {}), jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) })] }) }));
|
|
1682
|
+
return (jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: 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: [jsx(Background, {}), jsx(MiniMap, {}), jsx(Controls, {}), jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) }), jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: () => setNodeMenuOpen(false) })] }) }));
|
|
1487
1683
|
}
|
|
1488
1684
|
|
|
1489
1685
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, }) {
|
|
@@ -1776,7 +1972,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1776
1972
|
return String(value);
|
|
1777
1973
|
}, [registry]);
|
|
1778
1974
|
const baseToElement = useCallback((typeId, value) => {
|
|
1779
|
-
return jsx("span", { children: baseToString(typeId, value) });
|
|
1975
|
+
return (jsx("span", { className: "ml-1 opacity-60", children: baseToString(typeId, value) }));
|
|
1780
1976
|
}, [baseToString]);
|
|
1781
1977
|
const toString = useMemo(() => {
|
|
1782
1978
|
if (overrides?.toString)
|
|
@@ -1807,7 +2003,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1807
2003
|
catch (err) {
|
|
1808
2004
|
alert(String(err?.message ?? err));
|
|
1809
2005
|
}
|
|
1810
|
-
}, disabled: !engine, children: "Start" })), jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx("span", { children: "Debug events" })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx("span", { children: "Show values in nodes" })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { showValues: showValues, toString: toString, toElement: toElement }
|
|
2006
|
+
}, disabled: !engine, children: "Start" })), jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx("span", { children: "Debug events" })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx("span", { children: "Show values in nodes" })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { showValues: showValues, toString: toString, toElement: toElement }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
|
|
1811
2007
|
}
|
|
1812
2008
|
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
|
|
1813
2009
|
const [registry, setRegistry] = useState(createSimpleGraphRegistry());
|
|
@@ -1833,5 +2029,5 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
1833
2029
|
}, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, overrides: overrides }) }));
|
|
1834
2030
|
}
|
|
1835
2031
|
|
|
1836
|
-
export { AbstractWorkbench, CLIWorkbench, DefaultUIExtensionRegistry, GraphRunner, InMemoryWorkbench, Inspector,
|
|
2032
|
+
export { AbstractWorkbench, CLIWorkbench, DefaultUIExtensionRegistry, GraphRunner, InMemoryWorkbench, Inspector, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, getNodeBorderClassNames, toReactFlow, useQueryParamBoolean, useQueryParamString, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
|
|
1837
2033
|
//# sourceMappingURL=index.js.map
|