@bian-womp/spark-workbench 0.2.77 → 0.2.79
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 +363 -330
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/adapters/cli/index.d.ts +1 -1
- package/lib/cjs/src/adapters/cli/index.d.ts.map +1 -1
- package/lib/cjs/src/core/AbstractWorkbench.d.ts +1 -1
- package/lib/cjs/src/core/AbstractWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts +50 -2
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/core/contracts.d.ts +8 -2
- package/lib/cjs/src/core/contracts.d.ts.map +1 -1
- package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
- 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.d.ts +0 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/cjs/src/misc/context-menu/ContextMenuHelpers.d.ts.map +1 -1
- package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
- package/lib/cjs/src/misc/load.d.ts.map +1 -1
- package/lib/esm/index.js +364 -331
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/adapters/cli/index.d.ts +1 -1
- package/lib/esm/src/adapters/cli/index.d.ts.map +1 -1
- package/lib/esm/src/core/AbstractWorkbench.d.ts +1 -1
- package/lib/esm/src/core/AbstractWorkbench.d.ts.map +1 -1
- package/lib/esm/src/core/InMemoryWorkbench.d.ts +50 -2
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/esm/src/core/contracts.d.ts +8 -2
- package/lib/esm/src/core/contracts.d.ts.map +1 -1
- package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
- 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.d.ts +0 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/esm/src/misc/context-menu/ContextMenuHelpers.d.ts.map +1 -1
- package/lib/esm/src/misc/hooks.d.ts.map +1 -1
- package/lib/esm/src/misc/load.d.ts.map +1 -1
- package/package.json +4 -4
package/lib/cjs/index.cjs
CHANGED
|
@@ -123,7 +123,7 @@ class AbstractWorkbench {
|
|
|
123
123
|
class InMemoryWorkbench extends AbstractWorkbench {
|
|
124
124
|
constructor() {
|
|
125
125
|
super(...arguments);
|
|
126
|
-
this.
|
|
126
|
+
this._def = { nodes: [], edges: [] };
|
|
127
127
|
this.listeners = new Map();
|
|
128
128
|
this.positions = {};
|
|
129
129
|
this.selection = {
|
|
@@ -135,40 +135,40 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
135
135
|
this.historyState = undefined;
|
|
136
136
|
this.copiedData = null;
|
|
137
137
|
}
|
|
138
|
+
get def() {
|
|
139
|
+
return this._def;
|
|
140
|
+
}
|
|
138
141
|
setRegistry(registry) {
|
|
139
142
|
this.registry = registry;
|
|
140
143
|
}
|
|
141
144
|
async load(def) {
|
|
142
|
-
this.
|
|
145
|
+
this._def = { nodes: [...def.nodes], edges: [...def.edges] };
|
|
143
146
|
if (this.layout) {
|
|
144
|
-
const { positions } = await this.layout.layout(this.
|
|
147
|
+
const { positions } = await this.layout.layout(this._def);
|
|
145
148
|
this.positions = positions;
|
|
146
149
|
}
|
|
147
|
-
const defNodeIds = new Set(this.
|
|
148
|
-
const defEdgeIds = new Set(this.
|
|
150
|
+
const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
|
|
151
|
+
const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
|
|
149
152
|
const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
|
|
150
153
|
const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
|
|
151
154
|
const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
|
|
152
155
|
this.positions = filteredPositions;
|
|
153
156
|
this.selection = { nodes: filteredNodes, edges: filteredEdges };
|
|
154
|
-
this.emit("graphChanged", { def: this.
|
|
157
|
+
this.emit("graphChanged", { def: this._def });
|
|
155
158
|
this.refreshValidation();
|
|
156
159
|
}
|
|
157
|
-
export() {
|
|
158
|
-
return this.def;
|
|
159
|
-
}
|
|
160
160
|
refreshValidation() {
|
|
161
161
|
this.emit("validationChanged", this.validate());
|
|
162
162
|
}
|
|
163
163
|
validate() {
|
|
164
164
|
if (this.registry) {
|
|
165
165
|
const builder = new sparkGraph.GraphBuilder(this.registry);
|
|
166
|
-
const report = builder.validate(this.
|
|
166
|
+
const report = builder.validate(this._def);
|
|
167
167
|
return report;
|
|
168
168
|
}
|
|
169
169
|
const issues = [];
|
|
170
170
|
const nodeIds = new Set();
|
|
171
|
-
for (const n of this.
|
|
171
|
+
for (const n of this._def.nodes) {
|
|
172
172
|
if (nodeIds.has(n.nodeId)) {
|
|
173
173
|
issues.push({
|
|
174
174
|
level: "error",
|
|
@@ -180,7 +180,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
180
180
|
nodeIds.add(n.nodeId);
|
|
181
181
|
}
|
|
182
182
|
const edgeIds = new Set();
|
|
183
|
-
for (const e of this.
|
|
183
|
+
for (const e of this._def.edges) {
|
|
184
184
|
if (edgeIds.has(e.id)) {
|
|
185
185
|
issues.push({
|
|
186
186
|
level: "error",
|
|
@@ -193,10 +193,17 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
193
193
|
}
|
|
194
194
|
return { ok: issues.every((i) => i.level !== "error"), issues };
|
|
195
195
|
}
|
|
196
|
+
setInputs(nodeId, inputs, options) {
|
|
197
|
+
this.emit("graphChanged", {
|
|
198
|
+
def: this._def,
|
|
199
|
+
change: { type: "setInputs", nodeId, inputs },
|
|
200
|
+
...options,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
196
203
|
addNode(node, options) {
|
|
197
204
|
const id = node.nodeId ??
|
|
198
|
-
this.genId("n", new Set(this.
|
|
199
|
-
this.
|
|
205
|
+
this.genId("n", new Set(this._def.nodes.map((n) => n.nodeId)));
|
|
206
|
+
this._def.nodes.push({
|
|
200
207
|
nodeId: id,
|
|
201
208
|
typeId: node.typeId,
|
|
202
209
|
params: node.params,
|
|
@@ -205,7 +212,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
205
212
|
if (node.position)
|
|
206
213
|
this.positions[id] = node.position;
|
|
207
214
|
this.emit("graphChanged", {
|
|
208
|
-
def: this.
|
|
215
|
+
def: this._def,
|
|
209
216
|
change: {
|
|
210
217
|
type: "addNode",
|
|
211
218
|
nodeId: id,
|
|
@@ -218,26 +225,26 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
218
225
|
return id;
|
|
219
226
|
}
|
|
220
227
|
removeNode(nodeId, options) {
|
|
221
|
-
this.
|
|
222
|
-
this.
|
|
228
|
+
this._def.nodes = this._def.nodes.filter((n) => n.nodeId !== nodeId);
|
|
229
|
+
this._def.edges = this._def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
|
|
223
230
|
delete this.positions[nodeId];
|
|
224
231
|
this.emit("graphChanged", {
|
|
225
|
-
def: this.
|
|
232
|
+
def: this._def,
|
|
226
233
|
change: { type: "removeNode", nodeId },
|
|
227
234
|
...options,
|
|
228
235
|
});
|
|
229
236
|
this.refreshValidation();
|
|
230
237
|
}
|
|
231
238
|
connect(edge, options) {
|
|
232
|
-
const id = edge.id ?? this.genId("e", new Set(this.
|
|
233
|
-
this.
|
|
239
|
+
const id = edge.id ?? this.genId("e", new Set(this._def.edges.map((e) => e.id)));
|
|
240
|
+
this._def.edges.push({
|
|
234
241
|
id,
|
|
235
242
|
source: { ...edge.source },
|
|
236
243
|
target: { ...edge.target },
|
|
237
244
|
typeId: edge.typeId,
|
|
238
245
|
});
|
|
239
246
|
this.emit("graphChanged", {
|
|
240
|
-
def: this.
|
|
247
|
+
def: this._def,
|
|
241
248
|
change: { type: "connect", edgeId: id },
|
|
242
249
|
...options,
|
|
243
250
|
});
|
|
@@ -245,16 +252,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
245
252
|
return id;
|
|
246
253
|
}
|
|
247
254
|
disconnect(edgeId, options) {
|
|
248
|
-
this.
|
|
255
|
+
this._def.edges = this._def.edges.filter((e) => e.id !== edgeId);
|
|
249
256
|
this.emit("graphChanged", {
|
|
250
|
-
def: this.
|
|
257
|
+
def: this._def,
|
|
251
258
|
change: { type: "disconnect", edgeId },
|
|
252
259
|
...options,
|
|
253
260
|
});
|
|
254
261
|
this.emit("validationChanged", this.validate());
|
|
255
262
|
}
|
|
256
263
|
updateEdgeType(edgeId, typeId) {
|
|
257
|
-
const e = this.
|
|
264
|
+
const e = this._def.edges.find((x) => x.id === edgeId);
|
|
258
265
|
if (!e)
|
|
259
266
|
return;
|
|
260
267
|
if (!typeId)
|
|
@@ -262,18 +269,18 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
262
269
|
else
|
|
263
270
|
e.typeId = typeId;
|
|
264
271
|
this.emit("graphChanged", {
|
|
265
|
-
def: this.
|
|
272
|
+
def: this._def,
|
|
266
273
|
change: { type: "updateEdgeType", edgeId, typeId },
|
|
267
274
|
});
|
|
268
275
|
this.refreshValidation();
|
|
269
276
|
}
|
|
270
277
|
updateParams(nodeId, params) {
|
|
271
|
-
const n = this.
|
|
278
|
+
const n = this._def.nodes.find((n) => n.nodeId === nodeId);
|
|
272
279
|
if (!n)
|
|
273
280
|
return;
|
|
274
281
|
n.params = { ...(n.params ?? {}), ...params };
|
|
275
282
|
this.emit("graphChanged", {
|
|
276
|
-
def: this.
|
|
283
|
+
def: this._def,
|
|
277
284
|
change: { type: "updateParams", nodeId },
|
|
278
285
|
});
|
|
279
286
|
}
|
|
@@ -281,7 +288,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
281
288
|
setPositions(map, options) {
|
|
282
289
|
this.positions = { ...this.positions, ...map };
|
|
283
290
|
this.emit("graphUiChanged", {
|
|
284
|
-
def: this.
|
|
291
|
+
def: this._def,
|
|
285
292
|
change: { type: "moveNodes" },
|
|
286
293
|
...options,
|
|
287
294
|
});
|
|
@@ -289,17 +296,20 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
289
296
|
getPositions() {
|
|
290
297
|
return { ...this.positions };
|
|
291
298
|
}
|
|
292
|
-
|
|
299
|
+
setSelectionInternal(sel, options) {
|
|
293
300
|
if (lod.isEqual(this.selection, sel))
|
|
294
301
|
return;
|
|
295
302
|
this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
|
|
296
303
|
this.emit("selectionChanged", this.selection);
|
|
297
304
|
this.emit("graphUiChanged", {
|
|
298
|
-
def: this.
|
|
305
|
+
def: this._def,
|
|
299
306
|
change: { type: "selection" },
|
|
300
307
|
...options,
|
|
301
308
|
});
|
|
302
309
|
}
|
|
310
|
+
setSelection(sel, options) {
|
|
311
|
+
this.setSelectionInternal(sel, options);
|
|
312
|
+
}
|
|
303
313
|
getSelection() {
|
|
304
314
|
return {
|
|
305
315
|
nodes: [...this.selection.nodes],
|
|
@@ -320,14 +330,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
320
330
|
this.disconnect(edgeId);
|
|
321
331
|
}
|
|
322
332
|
// Clear selection
|
|
323
|
-
this.
|
|
333
|
+
this.setSelectionInternal({ nodes: [], edges: [] }, options);
|
|
324
334
|
}
|
|
325
335
|
setViewport(viewport) {
|
|
326
336
|
if (lod.isEqual(this.viewport, viewport))
|
|
327
337
|
return;
|
|
328
338
|
this.viewport = { ...viewport };
|
|
329
339
|
this.emit("graphUiChanged", {
|
|
330
|
-
def: this.
|
|
340
|
+
def: this._def,
|
|
331
341
|
change: { type: "viewport" },
|
|
332
342
|
});
|
|
333
343
|
}
|
|
@@ -335,8 +345,8 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
335
345
|
return this.viewport ? { ...this.viewport } : null;
|
|
336
346
|
}
|
|
337
347
|
getUIState() {
|
|
338
|
-
const defNodeIds = new Set(this.
|
|
339
|
-
const defEdgeIds = new Set(this.
|
|
348
|
+
const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
|
|
349
|
+
const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
|
|
340
350
|
const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
|
|
341
351
|
const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
|
|
342
352
|
const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
|
|
@@ -375,6 +385,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
375
385
|
}
|
|
376
386
|
setRuntimeState(runtime) {
|
|
377
387
|
this.runtimeState = runtime ? { ...runtime } : null;
|
|
388
|
+
this.emit("runtimeMetadataChanged", {});
|
|
378
389
|
}
|
|
379
390
|
getHistory() {
|
|
380
391
|
return this.historyState;
|
|
@@ -391,6 +402,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
391
402
|
const nodeMeta = current.nodes[nodeId] ?? {};
|
|
392
403
|
const updated = updater({ ...nodeMeta });
|
|
393
404
|
this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
|
|
405
|
+
this.emit("runtimeMetadataChanged", { nodeId });
|
|
394
406
|
}
|
|
395
407
|
on(event, handler) {
|
|
396
408
|
if (!this.listeners.has(event))
|
|
@@ -415,11 +427,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
415
427
|
const selection = this.getSelection();
|
|
416
428
|
if (selection.nodes.length === 0)
|
|
417
429
|
return null;
|
|
418
|
-
const def = this.export();
|
|
419
430
|
const positions = this.getPositions();
|
|
420
431
|
const selectedNodeSet = new Set(selection.nodes);
|
|
421
432
|
// Collect nodes to copy
|
|
422
|
-
const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
|
|
433
|
+
const nodesToCopy = this.def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
|
|
423
434
|
if (nodesToCopy.length === 0)
|
|
424
435
|
return null;
|
|
425
436
|
// Calculate bounds
|
|
@@ -443,12 +454,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
443
454
|
const centerY = (bounds.minY + bounds.maxY) / 2;
|
|
444
455
|
// Get inputs for each node
|
|
445
456
|
// Include values from inbound edges if those edges are selected
|
|
446
|
-
const allInputs = runner.getInputs(def);
|
|
457
|
+
const allInputs = runner.getInputs(this.def);
|
|
447
458
|
const selectedEdgeSet = new Set(selection.edges);
|
|
448
459
|
const copiedNodes = nodesToCopy.map((node) => {
|
|
449
460
|
const pos = positions[node.nodeId] || { x: 0, y: 0 };
|
|
450
461
|
// Get all inbound edges for this node
|
|
451
|
-
const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
|
|
462
|
+
const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === node.nodeId);
|
|
452
463
|
// Build set of handles that have inbound edges
|
|
453
464
|
// But only exclude handles whose edges are NOT selected
|
|
454
465
|
const inboundHandlesToExclude = new Set(inboundEdges
|
|
@@ -472,7 +483,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
472
483
|
};
|
|
473
484
|
});
|
|
474
485
|
// Collect edges between copied nodes
|
|
475
|
-
const copiedEdges = def.edges
|
|
486
|
+
const copiedEdges = this.def.edges
|
|
476
487
|
.filter((edge) => {
|
|
477
488
|
return (selectedNodeSet.has(edge.source.nodeId) &&
|
|
478
489
|
selectedNodeSet.has(edge.target.nodeId));
|
|
@@ -490,6 +501,193 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
490
501
|
bounds,
|
|
491
502
|
};
|
|
492
503
|
}
|
|
504
|
+
/**
|
|
505
|
+
* Duplicate all selected nodes.
|
|
506
|
+
* Returns the list of newly created node IDs.
|
|
507
|
+
* Each duplicated node is offset by 24px in both x and y directions.
|
|
508
|
+
* Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
|
|
509
|
+
*/
|
|
510
|
+
duplicateSelection(runner, options) {
|
|
511
|
+
const selection = this.getSelection();
|
|
512
|
+
if (selection.nodes.length === 0)
|
|
513
|
+
return [];
|
|
514
|
+
const positions = this.getPositions();
|
|
515
|
+
const newNodes = [];
|
|
516
|
+
// Get inputs without bindings (literal values only)
|
|
517
|
+
const allInputs = runner.getInputs(this.def) || {};
|
|
518
|
+
// Duplicate each selected node
|
|
519
|
+
for (const nodeId of selection.nodes) {
|
|
520
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
521
|
+
if (!n)
|
|
522
|
+
continue;
|
|
523
|
+
const pos = positions[nodeId] || { x: 0, y: 0 };
|
|
524
|
+
const inboundHandles = new Set(this.def.edges
|
|
525
|
+
.filter((e) => e.target.nodeId === nodeId)
|
|
526
|
+
.map((e) => e.target.handle));
|
|
527
|
+
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
528
|
+
const newNodeId = this.addNode({
|
|
529
|
+
typeId: n.typeId,
|
|
530
|
+
params: n.params,
|
|
531
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
532
|
+
resolvedHandles: n.resolvedHandles,
|
|
533
|
+
}, {
|
|
534
|
+
inputs: inputsWithoutBindings,
|
|
535
|
+
copyOutputsFrom: nodeId,
|
|
536
|
+
dry: true,
|
|
537
|
+
});
|
|
538
|
+
newNodes.push(newNodeId);
|
|
539
|
+
}
|
|
540
|
+
// Select all newly duplicated nodes
|
|
541
|
+
if (newNodes.length > 0) {
|
|
542
|
+
this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
|
|
543
|
+
}
|
|
544
|
+
return newNodes;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Bake an output value from a node into a new node.
|
|
548
|
+
* Creates a new node based on the output type's bakeTarget configuration.
|
|
549
|
+
* Returns the ID of the last created node (or undefined if none created).
|
|
550
|
+
*/
|
|
551
|
+
async bake(registry, runner, outputValue, outputTypeId, nodePosition, options) {
|
|
552
|
+
try {
|
|
553
|
+
if (!outputTypeId || outputValue === undefined)
|
|
554
|
+
return undefined;
|
|
555
|
+
const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
|
|
556
|
+
const coerceIfNeeded = async (fromType, toType, value) => {
|
|
557
|
+
if (!toType || toType === fromType || !runner?.coerce)
|
|
558
|
+
return value;
|
|
559
|
+
try {
|
|
560
|
+
return await runner.coerce(fromType, toType, value);
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return value;
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
const pos = nodePosition;
|
|
567
|
+
const isArray = outputTypeId.endsWith("[]");
|
|
568
|
+
const baseTypeId = isArray ? outputTypeId.slice(0, -2) : outputTypeId;
|
|
569
|
+
const tArr = isArray ? registry.types.get(outputTypeId) : undefined;
|
|
570
|
+
const tElem = registry.types.get(baseTypeId);
|
|
571
|
+
const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
|
|
572
|
+
const arrTarget = isArray ? tArr?.bakeTarget : undefined;
|
|
573
|
+
const elemTarget = isArray ? tElem?.bakeTarget : undefined;
|
|
574
|
+
let newNodeId;
|
|
575
|
+
if (singleTarget) {
|
|
576
|
+
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
577
|
+
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
578
|
+
const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
|
|
579
|
+
newNodeId = this.addNode({
|
|
580
|
+
typeId: singleTarget.nodeTypeId,
|
|
581
|
+
position: { x: pos.x + 180, y: pos.y },
|
|
582
|
+
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
583
|
+
}
|
|
584
|
+
else if (isArray && arrTarget) {
|
|
585
|
+
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
586
|
+
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
587
|
+
const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
|
|
588
|
+
newNodeId = this.addNode({
|
|
589
|
+
typeId: arrTarget.nodeTypeId,
|
|
590
|
+
position: { x: pos.x + 180, y: pos.y },
|
|
591
|
+
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
592
|
+
}
|
|
593
|
+
else if (isArray && elemTarget) {
|
|
594
|
+
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
595
|
+
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
596
|
+
const src = unwrap(outputValue);
|
|
597
|
+
const items = Array.isArray(src) ? src : [src];
|
|
598
|
+
const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
|
|
599
|
+
const COLS = 4;
|
|
600
|
+
const DX = 180;
|
|
601
|
+
const DY = 160;
|
|
602
|
+
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
603
|
+
const col = idx % COLS;
|
|
604
|
+
const row = Math.floor(idx / COLS);
|
|
605
|
+
newNodeId = this.addNode({
|
|
606
|
+
typeId: elemTarget.nodeTypeId,
|
|
607
|
+
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
608
|
+
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (newNodeId) {
|
|
612
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "bake" });
|
|
613
|
+
}
|
|
614
|
+
return newNodeId;
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Duplicate a single node.
|
|
622
|
+
* Returns the ID of the newly created node.
|
|
623
|
+
* The duplicated node is offset by 24px in both x and y directions.
|
|
624
|
+
* Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
|
|
625
|
+
*/
|
|
626
|
+
duplicateNode(nodeId, runner, options) {
|
|
627
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
628
|
+
if (!n)
|
|
629
|
+
return undefined;
|
|
630
|
+
const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
631
|
+
// Get inputs without bindings (literal values only)
|
|
632
|
+
const allInputs = runner.getInputs(this.def)[nodeId] || {};
|
|
633
|
+
const inboundHandles = new Set(this.def.edges
|
|
634
|
+
.filter((e) => e.target.nodeId === nodeId)
|
|
635
|
+
.map((e) => e.target.handle));
|
|
636
|
+
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
637
|
+
const newNodeId = this.addNode({
|
|
638
|
+
typeId: n.typeId,
|
|
639
|
+
params: n.params,
|
|
640
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
641
|
+
resolvedHandles: n.resolvedHandles,
|
|
642
|
+
}, {
|
|
643
|
+
inputs: inputsWithoutBindings,
|
|
644
|
+
copyOutputsFrom: nodeId,
|
|
645
|
+
dry: true,
|
|
646
|
+
});
|
|
647
|
+
// Select the newly duplicated node
|
|
648
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node" });
|
|
649
|
+
return newNodeId;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Duplicate a node and all its incoming edges.
|
|
653
|
+
* Returns the ID of the newly created node.
|
|
654
|
+
* The duplicated node is offset by 24px in both x and y directions.
|
|
655
|
+
* All incoming edges are duplicated to point to the new node.
|
|
656
|
+
*/
|
|
657
|
+
duplicateNodeWithEdges(nodeId, runner, options) {
|
|
658
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
659
|
+
if (!n)
|
|
660
|
+
return undefined;
|
|
661
|
+
const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
662
|
+
// Get all inputs (including those with bindings, since edges will be duplicated)
|
|
663
|
+
const inputs = runner.getInputs(this.def)[nodeId] || {};
|
|
664
|
+
// Add the duplicated node
|
|
665
|
+
const newNodeId = this.addNode({
|
|
666
|
+
typeId: n.typeId,
|
|
667
|
+
params: n.params,
|
|
668
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
669
|
+
resolvedHandles: n.resolvedHandles,
|
|
670
|
+
}, {
|
|
671
|
+
inputs,
|
|
672
|
+
copyOutputsFrom: nodeId,
|
|
673
|
+
dry: true,
|
|
674
|
+
});
|
|
675
|
+
// Find all incoming edges (edges where target is the original node)
|
|
676
|
+
const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
677
|
+
// Duplicate each incoming edge to point to the new node
|
|
678
|
+
for (const edge of incomingEdges) {
|
|
679
|
+
this.connect({
|
|
680
|
+
source: edge.source, // Keep the same source
|
|
681
|
+
target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
|
|
682
|
+
typeId: edge.typeId,
|
|
683
|
+
}, { dry: true });
|
|
684
|
+
}
|
|
685
|
+
// Select the newly duplicated node
|
|
686
|
+
if (newNodeId) {
|
|
687
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node-with-edges" });
|
|
688
|
+
}
|
|
689
|
+
return newNodeId;
|
|
690
|
+
}
|
|
493
691
|
/**
|
|
494
692
|
* Paste copied graph data at the specified center position.
|
|
495
693
|
* Returns the mapping from original node IDs to new node IDs.
|
|
@@ -535,7 +733,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
535
733
|
}
|
|
536
734
|
}
|
|
537
735
|
// Select the newly pasted nodes
|
|
538
|
-
this.
|
|
736
|
+
this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
539
737
|
return { nodeIdMap, edgeIds };
|
|
540
738
|
}
|
|
541
739
|
/**
|
|
@@ -560,8 +758,8 @@ class CLIWorkbench {
|
|
|
560
758
|
async load(def) {
|
|
561
759
|
await this.wb.load(def);
|
|
562
760
|
}
|
|
563
|
-
print(
|
|
564
|
-
const d =
|
|
761
|
+
print(options) {
|
|
762
|
+
const d = this.wb.def;
|
|
565
763
|
const detail = !!options?.detail;
|
|
566
764
|
const lines = [];
|
|
567
765
|
lines.push(`Nodes (${d.nodes.length})`);
|
|
@@ -1930,8 +2128,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1930
2128
|
if (!params.sourceHandle || !params.targetHandle)
|
|
1931
2129
|
return;
|
|
1932
2130
|
// Prevent duplicate edges between the same endpoints
|
|
1933
|
-
const
|
|
1934
|
-
const exists = def.edges.some((e) => e.source.nodeId === params.source &&
|
|
2131
|
+
const exists = wb.def.edges.some((e) => e.source.nodeId === params.source &&
|
|
1935
2132
|
e.source.handle === params.sourceHandle &&
|
|
1936
2133
|
e.target.nodeId === params.target &&
|
|
1937
2134
|
e.target.handle === params.targetHandle);
|
|
@@ -1981,7 +2178,10 @@ function useWorkbenchBridge(wb) {
|
|
|
1981
2178
|
}
|
|
1982
2179
|
}
|
|
1983
2180
|
if (selectionChanged) {
|
|
1984
|
-
wb.setSelection({
|
|
2181
|
+
wb.setSelection({
|
|
2182
|
+
nodes: Array.from(nextNodeIds),
|
|
2183
|
+
edges: current.edges,
|
|
2184
|
+
});
|
|
1985
2185
|
}
|
|
1986
2186
|
if (Object.keys(positions).length > 0) {
|
|
1987
2187
|
wb.setPositions(positions, { commit });
|
|
@@ -2016,7 +2216,10 @@ function useWorkbenchBridge(wb) {
|
|
|
2016
2216
|
}
|
|
2017
2217
|
}
|
|
2018
2218
|
if (selectionChanged) {
|
|
2019
|
-
wb.setSelection({
|
|
2219
|
+
wb.setSelection({
|
|
2220
|
+
nodes: current.nodes,
|
|
2221
|
+
edges: Array.from(nextEdgeIds),
|
|
2222
|
+
});
|
|
2020
2223
|
}
|
|
2021
2224
|
}, [wb]);
|
|
2022
2225
|
const onNodesDelete = React.useCallback((nodes) => {
|
|
@@ -2462,7 +2665,6 @@ function isSnapshotPayload(parsed) {
|
|
|
2462
2665
|
}
|
|
2463
2666
|
async function download(wb, runner) {
|
|
2464
2667
|
try {
|
|
2465
|
-
const def = wb.export();
|
|
2466
2668
|
const fullUiState = wb.getUIState();
|
|
2467
2669
|
const uiState = excludeViewportFromUIState(fullUiState);
|
|
2468
2670
|
const runtimeState = wb.getRuntimeState();
|
|
@@ -2471,7 +2673,7 @@ async function download(wb, runner) {
|
|
|
2471
2673
|
const fullSnapshot = await runner.snapshotFull();
|
|
2472
2674
|
snapshot = {
|
|
2473
2675
|
...fullSnapshot,
|
|
2474
|
-
def,
|
|
2676
|
+
def: wb.def,
|
|
2475
2677
|
extData: {
|
|
2476
2678
|
...(fullSnapshot.extData || {}),
|
|
2477
2679
|
ui: Object.keys(uiState || {}).length > 0 ? uiState : undefined,
|
|
@@ -2480,9 +2682,9 @@ async function download(wb, runner) {
|
|
|
2480
2682
|
};
|
|
2481
2683
|
}
|
|
2482
2684
|
else {
|
|
2483
|
-
const inputs = runner.getInputs(def);
|
|
2685
|
+
const inputs = runner.getInputs(wb.def);
|
|
2484
2686
|
snapshot = {
|
|
2485
|
-
def,
|
|
2687
|
+
def: wb.def,
|
|
2486
2688
|
inputs,
|
|
2487
2689
|
outputs: {},
|
|
2488
2690
|
environment: {},
|
|
@@ -2521,7 +2723,7 @@ async function upload(parsed, wb, runner) {
|
|
|
2521
2723
|
}
|
|
2522
2724
|
if (runner.isRunning()) {
|
|
2523
2725
|
await runner.applySnapshotFull({
|
|
2524
|
-
def,
|
|
2726
|
+
def: wb.def,
|
|
2525
2727
|
environment,
|
|
2526
2728
|
inputs,
|
|
2527
2729
|
outputs,
|
|
@@ -2529,10 +2731,10 @@ async function upload(parsed, wb, runner) {
|
|
|
2529
2731
|
});
|
|
2530
2732
|
}
|
|
2531
2733
|
else {
|
|
2532
|
-
runner.build(wb.
|
|
2734
|
+
runner.build(wb.def);
|
|
2533
2735
|
if (inputs && typeof inputs === "object") {
|
|
2534
2736
|
for (const [nodeId, map] of Object.entries(inputs)) {
|
|
2535
|
-
runner.setInputs(nodeId, map);
|
|
2737
|
+
runner.setInputs(nodeId, map, { dry: true });
|
|
2536
2738
|
}
|
|
2537
2739
|
}
|
|
2538
2740
|
}
|
|
@@ -2625,14 +2827,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2625
2827
|
const versionTick = useWorkbenchVersionTick(runner);
|
|
2626
2828
|
const valuesTick = versionTick + graphTick + graphUiTick;
|
|
2627
2829
|
// Def and IO values
|
|
2628
|
-
const
|
|
2629
|
-
const
|
|
2630
|
-
const
|
|
2631
|
-
const outputsMap = React.useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
|
|
2830
|
+
const inputsMap = React.useMemo(() => runner.getInputs(wb.def), [runner, wb, wb.def, valuesTick]);
|
|
2831
|
+
const inputDefaultsMap = React.useMemo(() => runner.getInputDefaults(wb.def), [runner, wb, wb.def, valuesTick]);
|
|
2832
|
+
const outputsMap = React.useMemo(() => runner.getOutputs(wb.def), [runner, wb, wb.def, valuesTick]);
|
|
2632
2833
|
const outputTypesMap = React.useMemo(() => {
|
|
2633
2834
|
const out = {};
|
|
2634
2835
|
// Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
|
|
2635
|
-
for (const n of def.nodes) {
|
|
2836
|
+
for (const n of wb.def.nodes) {
|
|
2636
2837
|
const effectiveHandles = computeEffectiveHandles(n, registry);
|
|
2637
2838
|
const outputsDecl = effectiveHandles.outputs;
|
|
2638
2839
|
const handles = Object.keys(outputsDecl);
|
|
@@ -2647,14 +2848,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2647
2848
|
out[n.nodeId] = cur;
|
|
2648
2849
|
}
|
|
2649
2850
|
return out;
|
|
2650
|
-
}, [def, outputsMap, registry]);
|
|
2851
|
+
}, [wb, wb.def, outputsMap, registry]);
|
|
2651
2852
|
// Initialize nodes and derive invalidated status from persisted metadata
|
|
2652
2853
|
React.useEffect(() => {
|
|
2653
2854
|
const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
|
|
2654
2855
|
setNodeStatus((prev) => {
|
|
2655
2856
|
const next = { ...prev };
|
|
2656
2857
|
const metadata = workbenchRuntimeState;
|
|
2657
|
-
for (const n of def.nodes) {
|
|
2858
|
+
for (const n of wb.def.nodes) {
|
|
2658
2859
|
const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
|
|
2659
2860
|
const nodeMeta = metadata.nodes[n.nodeId];
|
|
2660
2861
|
const updates = {};
|
|
@@ -2676,18 +2877,17 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2676
2877
|
}
|
|
2677
2878
|
return next;
|
|
2678
2879
|
});
|
|
2679
|
-
}, [def, wb]);
|
|
2880
|
+
}, [wb.def, wb]);
|
|
2680
2881
|
// Auto layout (simple layered layout)
|
|
2681
2882
|
const runAutoLayout = React.useCallback(() => {
|
|
2682
|
-
const cur = wb.export();
|
|
2683
2883
|
// Build DAG layers by indegree
|
|
2684
2884
|
const indegree = {};
|
|
2685
2885
|
const adj = {};
|
|
2686
|
-
for (const n of
|
|
2886
|
+
for (const n of wb.def.nodes) {
|
|
2687
2887
|
indegree[n.nodeId] = 0;
|
|
2688
2888
|
adj[n.nodeId] = [];
|
|
2689
2889
|
}
|
|
2690
|
-
for (const e of
|
|
2890
|
+
for (const e of wb.def.edges) {
|
|
2691
2891
|
indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
|
|
2692
2892
|
adj[e.source.nodeId].push(e.target.nodeId);
|
|
2693
2893
|
}
|
|
@@ -2718,7 +2918,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2718
2918
|
let maxWidth = 0;
|
|
2719
2919
|
const heights = {};
|
|
2720
2920
|
for (const id of layer) {
|
|
2721
|
-
const node =
|
|
2921
|
+
const node = wb.def.nodes.find((n) => n.nodeId === id);
|
|
2722
2922
|
if (!node)
|
|
2723
2923
|
continue;
|
|
2724
2924
|
// Prefer showValues sizing similar to node rendering
|
|
@@ -2744,26 +2944,26 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2744
2944
|
curX += maxWidth + H_GAP;
|
|
2745
2945
|
}
|
|
2746
2946
|
wb.setPositions(pos, { commit: true, reason: "auto-layout" });
|
|
2747
|
-
}, [wb, registry, overrides?.getDefaultNodeSize]);
|
|
2947
|
+
}, [wb, wb.def, registry, overrides?.getDefaultNodeSize]);
|
|
2748
2948
|
const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
|
|
2749
2949
|
const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
|
|
2750
2950
|
// Helper to save runtime metadata and UI state to extData
|
|
2751
|
-
const saveUiRuntimeMetadata = React.useCallback(async () => {
|
|
2951
|
+
const saveUiRuntimeMetadata = React.useCallback(async (workbench, graphRunner) => {
|
|
2752
2952
|
try {
|
|
2753
|
-
const current =
|
|
2953
|
+
const current = workbench.getRuntimeState() ?? { nodes: {} };
|
|
2754
2954
|
const metadata = { nodes: { ...current.nodes } };
|
|
2755
2955
|
// Clean up metadata for nodes that no longer exist
|
|
2756
|
-
const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
|
|
2956
|
+
const nodeIds = new Set(workbench.def.nodes.map((n) => n.nodeId));
|
|
2757
2957
|
for (const nodeId of Object.keys(metadata.nodes)) {
|
|
2758
2958
|
if (!nodeIds.has(nodeId)) {
|
|
2759
2959
|
delete metadata.nodes[nodeId];
|
|
2760
2960
|
}
|
|
2761
2961
|
}
|
|
2762
2962
|
// Save cleaned metadata to workbench state
|
|
2763
|
-
|
|
2764
|
-
const fullUiState =
|
|
2963
|
+
workbench.setRuntimeState(metadata);
|
|
2964
|
+
const fullUiState = workbench.getUIState();
|
|
2765
2965
|
const uiWithoutViewport = excludeViewportFromUIState(fullUiState);
|
|
2766
|
-
await
|
|
2966
|
+
await graphRunner.setExtData?.({
|
|
2767
2967
|
...(Object.keys(uiWithoutViewport || {}).length > 0
|
|
2768
2968
|
? { ui: uiWithoutViewport }
|
|
2769
2969
|
: {}),
|
|
@@ -2773,7 +2973,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2773
2973
|
catch (err) {
|
|
2774
2974
|
console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
|
|
2775
2975
|
}
|
|
2776
|
-
}, [
|
|
2976
|
+
}, []);
|
|
2777
2977
|
// Subscribe to runner/workbench events
|
|
2778
2978
|
React.useEffect(() => {
|
|
2779
2979
|
const add = (source, type) => (payload) => setEvents((prev) => {
|
|
@@ -2799,9 +2999,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2799
2999
|
});
|
|
2800
3000
|
// Helper to apply resolved handles from event payload to workbench
|
|
2801
3001
|
const applyResolvedHandles = (resolvedHandles) => {
|
|
2802
|
-
const cur = wb.export();
|
|
2803
3002
|
let changed = false;
|
|
2804
|
-
for (const n of
|
|
3003
|
+
for (const n of wb.def.nodes) {
|
|
2805
3004
|
const updated = resolvedHandles[n.nodeId];
|
|
2806
3005
|
if (updated) {
|
|
2807
3006
|
const before = JSON.stringify(n.resolvedHandles || {});
|
|
@@ -2828,10 +3027,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2828
3027
|
[handle]: now,
|
|
2829
3028
|
},
|
|
2830
3029
|
}));
|
|
2831
|
-
setNodeStatus((s) => ({
|
|
2832
|
-
...s,
|
|
2833
|
-
[nodeId]: { ...s[nodeId], invalidated: true },
|
|
2834
|
-
}));
|
|
2835
3030
|
// Clear validation errors for this input when a valid value is set
|
|
2836
3031
|
setInputValidationErrors((prev) => prev.filter((err) => !(err.nodeId === nodeId && err.handle === handle)));
|
|
2837
3032
|
}
|
|
@@ -2937,30 +3132,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2937
3132
|
// If resolvedHandles are included in the event, use them directly (more efficient)
|
|
2938
3133
|
if (e?.resolvedHandles && Object.keys(e.resolvedHandles).length > 0) {
|
|
2939
3134
|
applyResolvedHandles(e.resolvedHandles);
|
|
2940
|
-
// Mark nodes whose handles changed as invalid
|
|
2941
|
-
const affectedNodeIds = Object.keys(e.resolvedHandles);
|
|
2942
|
-
if (affectedNodeIds.length > 0) {
|
|
2943
|
-
setNodeStatus((prev) => {
|
|
2944
|
-
const next = { ...prev };
|
|
2945
|
-
for (const id of affectedNodeIds) {
|
|
2946
|
-
const cur = next[id] ?? (next[id] = { activeRuns: 0, activeRunIds: [] });
|
|
2947
|
-
next[id] = { ...cur, invalidated: true };
|
|
2948
|
-
}
|
|
2949
|
-
return next;
|
|
2950
|
-
});
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
2953
|
-
// For broader invalidations (e.g. registry-changed, graph-updated), mark all nodes invalid
|
|
2954
|
-
if (e?.reason === "registry-changed" || e?.reason === "graph-updated") {
|
|
2955
|
-
setNodeStatus((prev) => {
|
|
2956
|
-
const next = { ...prev };
|
|
2957
|
-
for (const n of def.nodes) {
|
|
2958
|
-
const cur = next[n.nodeId] ??
|
|
2959
|
-
(next[n.nodeId] = { activeRuns: 0, activeRunIds: [] });
|
|
2960
|
-
next[n.nodeId] = { ...cur, invalidated: true };
|
|
2961
|
-
}
|
|
2962
|
-
return next;
|
|
2963
|
-
});
|
|
2964
3135
|
}
|
|
2965
3136
|
return add("runner", "invalidate")(e);
|
|
2966
3137
|
});
|
|
@@ -2993,7 +3164,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2993
3164
|
? [...currentRunIds, runId]
|
|
2994
3165
|
: currentRunIds,
|
|
2995
3166
|
progress: 0,
|
|
2996
|
-
invalidated: false,
|
|
2997
3167
|
},
|
|
2998
3168
|
};
|
|
2999
3169
|
});
|
|
@@ -3096,16 +3266,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3096
3266
|
const offWbGraphUiChangedForLog = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
3097
3267
|
const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
3098
3268
|
// Ensure newly added nodes start as invalidated until first evaluation
|
|
3099
|
-
const offWbAddNode = wb.on("graphChanged", (e) => {
|
|
3100
|
-
const change = e.change;
|
|
3101
|
-
if (change?.type === "addNode" && typeof change.nodeId === "string") {
|
|
3102
|
-
const id = change.nodeId;
|
|
3103
|
-
setNodeStatus((s) => ({
|
|
3104
|
-
...s,
|
|
3105
|
-
[id]: { ...s[id], invalidated: true },
|
|
3106
|
-
}));
|
|
3107
|
-
}
|
|
3108
|
-
});
|
|
3109
3269
|
const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
|
|
3110
3270
|
// Build detailed reason from change type
|
|
3111
3271
|
let reason = "graph-changed";
|
|
@@ -3129,17 +3289,23 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3129
3289
|
else if (changeType === "updateEdgeType") {
|
|
3130
3290
|
reason = "update-edge-type";
|
|
3131
3291
|
}
|
|
3292
|
+
else if (changeType === "setInputs") {
|
|
3293
|
+
reason = "set-inputs";
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
if (event.change?.type === "setInputs") {
|
|
3297
|
+
const { nodeId, inputs } = event.change;
|
|
3298
|
+
await runner.setInputs(nodeId, inputs, { dry: event.dry });
|
|
3132
3299
|
}
|
|
3133
3300
|
if (!runner.isRunning()) {
|
|
3134
3301
|
if (event.commit) {
|
|
3135
|
-
await saveUiRuntimeMetadata();
|
|
3302
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3136
3303
|
const history = await runner.commit(reason).catch((err) => {
|
|
3137
3304
|
console.error("[WorkbenchContext] Error committing:", err);
|
|
3138
3305
|
return undefined;
|
|
3139
3306
|
});
|
|
3140
|
-
if (history)
|
|
3307
|
+
if (history)
|
|
3141
3308
|
wb.setHistory(history);
|
|
3142
|
-
}
|
|
3143
3309
|
}
|
|
3144
3310
|
return;
|
|
3145
3311
|
}
|
|
@@ -3162,11 +3328,11 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3162
3328
|
}
|
|
3163
3329
|
}
|
|
3164
3330
|
}
|
|
3165
|
-
else {
|
|
3331
|
+
else if (event.change?.type !== "setInputs") {
|
|
3166
3332
|
await runner.update(event.def, { dry: event.dry });
|
|
3167
3333
|
}
|
|
3168
3334
|
if (event.commit) {
|
|
3169
|
-
await saveUiRuntimeMetadata();
|
|
3335
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3170
3336
|
const history = await runner
|
|
3171
3337
|
.commit(event.reason ?? reason)
|
|
3172
3338
|
.catch((err) => {
|
|
@@ -3187,7 +3353,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3187
3353
|
setSelectedNodeId(sel.nodes?.[0]);
|
|
3188
3354
|
setSelectedEdgeId(sel.edges?.[0]);
|
|
3189
3355
|
if (sel.commit) {
|
|
3190
|
-
await saveUiRuntimeMetadata();
|
|
3356
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3191
3357
|
const history = await runner
|
|
3192
3358
|
.commit(sel.reason ?? "selection")
|
|
3193
3359
|
.catch((err) => {
|
|
@@ -3226,7 +3392,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3226
3392
|
reason = "selection";
|
|
3227
3393
|
}
|
|
3228
3394
|
}
|
|
3229
|
-
await saveUiRuntimeMetadata();
|
|
3395
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3230
3396
|
const history = await runner
|
|
3231
3397
|
.commit(event.reason ?? reason)
|
|
3232
3398
|
.catch((err) => {
|
|
@@ -3246,7 +3412,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3246
3412
|
wb.setRegistry(newReg);
|
|
3247
3413
|
// Trigger a graph update so the UI revalidates with new types/enums/nodes
|
|
3248
3414
|
try {
|
|
3249
|
-
await runner.update(wb.
|
|
3415
|
+
await runner.update(wb.def);
|
|
3250
3416
|
}
|
|
3251
3417
|
catch {
|
|
3252
3418
|
console.error("Failed to update graph definition after registry changed");
|
|
@@ -3269,7 +3435,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3269
3435
|
setNodeStatus(() => {
|
|
3270
3436
|
const next = {};
|
|
3271
3437
|
const metadata = wb.getRuntimeState() ?? { nodes: {} };
|
|
3272
|
-
for (const n of def.nodes) {
|
|
3438
|
+
for (const n of wb.def.nodes) {
|
|
3273
3439
|
const nodeMeta = metadata.nodes[n.nodeId];
|
|
3274
3440
|
next[n.nodeId] = {
|
|
3275
3441
|
activeRuns: 0,
|
|
@@ -3284,6 +3450,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3284
3450
|
errorRunsRef.current = {};
|
|
3285
3451
|
}
|
|
3286
3452
|
});
|
|
3453
|
+
const offWbRuntimeMetadataChanged = wb.on("runtimeMetadataChanged", (event) => {
|
|
3454
|
+
const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
|
|
3455
|
+
setNodeStatus((prev) => {
|
|
3456
|
+
const next = { ...prev };
|
|
3457
|
+
const metadata = workbenchRuntimeState;
|
|
3458
|
+
let changed = false;
|
|
3459
|
+
// If nodeId is specified, only update that node; otherwise update all nodes
|
|
3460
|
+
const nodesToUpdate = event.nodeId
|
|
3461
|
+
? wb.def.nodes.filter((n) => n.nodeId === event.nodeId)
|
|
3462
|
+
: wb.def.nodes;
|
|
3463
|
+
for (const n of nodesToUpdate) {
|
|
3464
|
+
const cur = next[n.nodeId];
|
|
3465
|
+
if (!cur)
|
|
3466
|
+
continue;
|
|
3467
|
+
const nodeMeta = metadata.nodes[n.nodeId];
|
|
3468
|
+
const newInvalidated = computeInvalidatedFromMetadata(nodeMeta);
|
|
3469
|
+
if (cur.invalidated !== newInvalidated) {
|
|
3470
|
+
next[n.nodeId] = { ...cur, invalidated: newInvalidated };
|
|
3471
|
+
changed = true;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
return changed ? next : prev;
|
|
3475
|
+
});
|
|
3476
|
+
});
|
|
3287
3477
|
wb.refreshValidation();
|
|
3288
3478
|
return () => {
|
|
3289
3479
|
offRunnerValue();
|
|
@@ -3295,20 +3485,20 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3295
3485
|
offWbGraphUiChanged();
|
|
3296
3486
|
offWbValidationChanged();
|
|
3297
3487
|
offWbError();
|
|
3298
|
-
offWbAddNode();
|
|
3299
3488
|
offWbGraphChangedForUpdate();
|
|
3300
3489
|
offWbdSetValidation();
|
|
3301
3490
|
offWbSelectionChanged();
|
|
3302
3491
|
offRunnerRegistry();
|
|
3303
3492
|
offRunnerTransport();
|
|
3304
3493
|
offFlowViewport();
|
|
3494
|
+
offWbRuntimeMetadataChanged();
|
|
3305
3495
|
};
|
|
3306
3496
|
}, [runner, wb, setRegistry]);
|
|
3307
3497
|
const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
|
|
3308
3498
|
const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
|
|
3309
3499
|
const start = React.useCallback((engine) => {
|
|
3310
3500
|
try {
|
|
3311
|
-
runner.launch(wb.
|
|
3501
|
+
runner.launch(wb.def, { engine });
|
|
3312
3502
|
}
|
|
3313
3503
|
catch { }
|
|
3314
3504
|
}, [runner, wb]);
|
|
@@ -3383,7 +3573,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3383
3573
|
runner,
|
|
3384
3574
|
registry,
|
|
3385
3575
|
setRegistry,
|
|
3386
|
-
def,
|
|
3387
3576
|
selectedNodeId,
|
|
3388
3577
|
selectedEdgeId,
|
|
3389
3578
|
setSelection,
|
|
@@ -3424,7 +3613,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3424
3613
|
runner,
|
|
3425
3614
|
registry,
|
|
3426
3615
|
setRegistry,
|
|
3427
|
-
def,
|
|
3428
3616
|
selectedNodeId,
|
|
3429
3617
|
selectedEdgeId,
|
|
3430
3618
|
setSelection,
|
|
@@ -3464,137 +3652,23 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3464
3652
|
}
|
|
3465
3653
|
|
|
3466
3654
|
function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
|
|
3467
|
-
const doBake = async (handleId) => {
|
|
3468
|
-
try {
|
|
3469
|
-
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
3470
|
-
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
3471
|
-
let newNodeId;
|
|
3472
|
-
if (!typeId || raw === undefined)
|
|
3473
|
-
return;
|
|
3474
|
-
const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
|
|
3475
|
-
const coerceIfNeeded = async (fromType, toType, value) => {
|
|
3476
|
-
if (!toType || toType === fromType || !runner?.coerce)
|
|
3477
|
-
return value;
|
|
3478
|
-
try {
|
|
3479
|
-
return await runner.coerce(fromType, toType, value);
|
|
3480
|
-
}
|
|
3481
|
-
catch {
|
|
3482
|
-
return value;
|
|
3483
|
-
}
|
|
3484
|
-
};
|
|
3485
|
-
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3486
|
-
const isArray = typeId.endsWith("[]");
|
|
3487
|
-
const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
|
|
3488
|
-
const tArr = isArray ? registry.types.get(typeId) : undefined;
|
|
3489
|
-
const tElem = registry.types.get(baseTypeId);
|
|
3490
|
-
const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
|
|
3491
|
-
const arrTarget = isArray ? tArr?.bakeTarget : undefined;
|
|
3492
|
-
const elemTarget = isArray ? tElem?.bakeTarget : undefined;
|
|
3493
|
-
if (singleTarget) {
|
|
3494
|
-
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
3495
|
-
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
3496
|
-
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3497
|
-
newNodeId = wb.addNode({
|
|
3498
|
-
typeId: singleTarget.nodeTypeId,
|
|
3499
|
-
position: { x: pos.x + 180, y: pos.y },
|
|
3500
|
-
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
3501
|
-
}
|
|
3502
|
-
else if (isArray && arrTarget) {
|
|
3503
|
-
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
3504
|
-
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
3505
|
-
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3506
|
-
newNodeId = wb.addNode({
|
|
3507
|
-
typeId: arrTarget.nodeTypeId,
|
|
3508
|
-
position: { x: pos.x + 180, y: pos.y },
|
|
3509
|
-
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
3510
|
-
}
|
|
3511
|
-
else if (isArray && elemTarget) {
|
|
3512
|
-
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
3513
|
-
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
3514
|
-
const src = unwrap(raw);
|
|
3515
|
-
const items = Array.isArray(src) ? src : [src];
|
|
3516
|
-
const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
|
|
3517
|
-
const COLS = 4;
|
|
3518
|
-
const DX = 180;
|
|
3519
|
-
const DY = 160;
|
|
3520
|
-
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
3521
|
-
const col = idx % COLS;
|
|
3522
|
-
const row = Math.floor(idx / COLS);
|
|
3523
|
-
newNodeId = wb.addNode({
|
|
3524
|
-
typeId: elemTarget.nodeTypeId,
|
|
3525
|
-
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
3526
|
-
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
3527
|
-
}
|
|
3528
|
-
}
|
|
3529
|
-
if (newNodeId) {
|
|
3530
|
-
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "bake" });
|
|
3531
|
-
}
|
|
3532
|
-
}
|
|
3533
|
-
catch { }
|
|
3534
|
-
};
|
|
3535
3655
|
return {
|
|
3536
3656
|
onDelete: () => {
|
|
3537
3657
|
wb.removeNode(nodeId, { commit: true });
|
|
3538
3658
|
onClose();
|
|
3539
3659
|
},
|
|
3540
3660
|
onDuplicate: async () => {
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
return onClose();
|
|
3545
|
-
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3546
|
-
const inboundHandles = new Set(def.edges
|
|
3547
|
-
.filter((e) => e.target.nodeId === nodeId)
|
|
3548
|
-
.map((e) => e.target.handle));
|
|
3549
|
-
const allInputs = runner.getInputs(def)[nodeId] || {};
|
|
3550
|
-
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
3551
|
-
const newNodeId = wb.addNode({
|
|
3552
|
-
typeId: n.typeId,
|
|
3553
|
-
params: n.params,
|
|
3554
|
-
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3555
|
-
resolvedHandles: n.resolvedHandles,
|
|
3556
|
-
}, {
|
|
3557
|
-
inputs: inputsWithoutBindings,
|
|
3558
|
-
copyOutputsFrom: nodeId,
|
|
3559
|
-
dry: true,
|
|
3661
|
+
wb.duplicateNode(nodeId, runner, {
|
|
3662
|
+
commit: true,
|
|
3663
|
+
reason: "duplicate-node",
|
|
3560
3664
|
});
|
|
3561
|
-
// Select the newly duplicated node
|
|
3562
|
-
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
|
|
3563
3665
|
onClose();
|
|
3564
3666
|
},
|
|
3565
3667
|
onDuplicateWithEdges: async () => {
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
return onClose();
|
|
3570
|
-
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3571
|
-
// Get inputs without bindings (literal values only)
|
|
3572
|
-
const inputs = runner.getInputs(def)[nodeId] || {};
|
|
3573
|
-
// Add the duplicated node
|
|
3574
|
-
const newNodeId = wb.addNode({
|
|
3575
|
-
typeId: n.typeId,
|
|
3576
|
-
params: n.params,
|
|
3577
|
-
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3578
|
-
resolvedHandles: n.resolvedHandles,
|
|
3579
|
-
}, {
|
|
3580
|
-
inputs,
|
|
3581
|
-
copyOutputsFrom: nodeId,
|
|
3582
|
-
dry: true,
|
|
3668
|
+
wb.duplicateNodeWithEdges(nodeId, runner, {
|
|
3669
|
+
commit: true,
|
|
3670
|
+
reason: "duplicate-node-with-edges",
|
|
3583
3671
|
});
|
|
3584
|
-
// Find all incoming edges (edges where target is the original node)
|
|
3585
|
-
const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
3586
|
-
// Duplicate each incoming edge to point to the new node
|
|
3587
|
-
for (const edge of incomingEdges) {
|
|
3588
|
-
wb.connect({
|
|
3589
|
-
source: edge.source, // Keep the same source
|
|
3590
|
-
target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
|
|
3591
|
-
typeId: edge.typeId,
|
|
3592
|
-
}, { dry: true });
|
|
3593
|
-
}
|
|
3594
|
-
// Select the newly duplicated node and edges
|
|
3595
|
-
if (newNodeId) {
|
|
3596
|
-
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate-with-edges" });
|
|
3597
|
-
}
|
|
3598
3672
|
onClose();
|
|
3599
3673
|
},
|
|
3600
3674
|
onRunPull: async () => {
|
|
@@ -3605,7 +3679,13 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3605
3679
|
onClose();
|
|
3606
3680
|
},
|
|
3607
3681
|
onBake: async (handleId) => {
|
|
3608
|
-
|
|
3682
|
+
const nodePosition = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3683
|
+
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
3684
|
+
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
3685
|
+
await wb.bake(registry, runner, raw, typeId || "", nodePosition, {
|
|
3686
|
+
commit: true,
|
|
3687
|
+
reason: "bake",
|
|
3688
|
+
});
|
|
3609
3689
|
onClose();
|
|
3610
3690
|
},
|
|
3611
3691
|
onCopy: () => {
|
|
@@ -3662,42 +3742,10 @@ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyRes
|
|
|
3662
3742
|
function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
|
|
3663
3743
|
const onDuplicate = runner
|
|
3664
3744
|
? () => {
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
}
|
|
3670
|
-
const def = wb.export();
|
|
3671
|
-
const positions = wb.getPositions();
|
|
3672
|
-
const newNodes = [];
|
|
3673
|
-
// Duplicate each selected node
|
|
3674
|
-
for (const nodeId of selection.nodes) {
|
|
3675
|
-
const n = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3676
|
-
if (!n)
|
|
3677
|
-
continue;
|
|
3678
|
-
const pos = positions[nodeId] || { x: 0, y: 0 };
|
|
3679
|
-
// Get inputs without bindings (literal values only)
|
|
3680
|
-
const allInputs = runner.getInputs(def)[nodeId] || {};
|
|
3681
|
-
const inboundHandles = new Set(def.edges
|
|
3682
|
-
.filter((e) => e.target.nodeId === nodeId)
|
|
3683
|
-
.map((e) => e.target.handle));
|
|
3684
|
-
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
3685
|
-
const newNodeId = wb.addNode({
|
|
3686
|
-
typeId: n.typeId,
|
|
3687
|
-
params: n.params,
|
|
3688
|
-
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3689
|
-
resolvedHandles: n.resolvedHandles,
|
|
3690
|
-
}, {
|
|
3691
|
-
inputs: inputsWithoutBindings,
|
|
3692
|
-
copyOutputsFrom: nodeId,
|
|
3693
|
-
dry: true,
|
|
3694
|
-
});
|
|
3695
|
-
newNodes.push(newNodeId);
|
|
3696
|
-
}
|
|
3697
|
-
// Select all newly duplicated nodes
|
|
3698
|
-
if (newNodes.length > 0) {
|
|
3699
|
-
wb.setSelection({ nodes: newNodes, edges: [] }, { commit: true, reason: "duplicate-selection" });
|
|
3700
|
-
}
|
|
3745
|
+
wb.duplicateSelection(runner, {
|
|
3746
|
+
commit: true,
|
|
3747
|
+
reason: "duplicate-selection",
|
|
3748
|
+
});
|
|
3701
3749
|
onClose();
|
|
3702
3750
|
}
|
|
3703
3751
|
: undefined;
|
|
@@ -3732,9 +3780,8 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
|
|
|
3732
3780
|
const canRedo = history ? history.redoCount > 0 : undefined;
|
|
3733
3781
|
const onSelectAll = wb
|
|
3734
3782
|
? () => {
|
|
3735
|
-
const
|
|
3736
|
-
const
|
|
3737
|
-
const allEdgeIds = def.edges.map((e) => e.id);
|
|
3783
|
+
const allNodeIds = wb.def.nodes.map((n) => n.nodeId);
|
|
3784
|
+
const allEdgeIds = wb.def.edges.map((e) => e.id);
|
|
3738
3785
|
wb.setSelection({ nodes: allNodeIds, edges: allEdgeIds }, { commit: true, reason: "select-all" });
|
|
3739
3786
|
onClose();
|
|
3740
3787
|
}
|
|
@@ -3753,8 +3800,7 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
|
|
|
3753
3800
|
}
|
|
3754
3801
|
function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
|
|
3755
3802
|
try {
|
|
3756
|
-
const
|
|
3757
|
-
const node = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3803
|
+
const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
|
|
3758
3804
|
if (!node)
|
|
3759
3805
|
return [];
|
|
3760
3806
|
const desc = registry.nodes.get(node.typeId);
|
|
@@ -3853,13 +3899,13 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3853
3899
|
return String(value ?? "");
|
|
3854
3900
|
}
|
|
3855
3901
|
};
|
|
3856
|
-
const {
|
|
3902
|
+
const { wb, registry, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, inputValidationErrors, clearSystemErrors, clearRegistryErrors, clearInputValidationErrors, removeSystemError, removeRegistryError, removeInputValidationError, } = useWorkbenchContext();
|
|
3857
3903
|
const nodeValidationIssues = validationByNode.issues;
|
|
3858
3904
|
const edgeValidationIssues = validationByEdge.issues;
|
|
3859
3905
|
const nodeValidationHandles = validationByNode;
|
|
3860
3906
|
const globalValidationIssues = validationGlobal;
|
|
3861
|
-
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
3862
|
-
const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
|
|
3907
|
+
const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
3908
|
+
const selectedEdge = wb.def.edges.find((e) => e.id === selectedEdgeId);
|
|
3863
3909
|
// Use computeEffectiveHandles to merge registry defaults with dynamically resolved handles
|
|
3864
3910
|
const effectiveHandles = selectedNode
|
|
3865
3911
|
? computeEffectiveHandles(selectedNode, registry)
|
|
@@ -3991,7 +4037,6 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3991
4037
|
setOriginals(nextOriginals);
|
|
3992
4038
|
}, [selectedNodeId, selectedNode, registry, valuesTick]);
|
|
3993
4039
|
const widthClass = debug ? "w-[480px]" : "w-[320px]";
|
|
3994
|
-
const { wb } = useWorkbenchContext();
|
|
3995
4040
|
const deleteEdgeById = (edgeId) => {
|
|
3996
4041
|
if (!edgeId)
|
|
3997
4042
|
return;
|
|
@@ -4018,9 +4063,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
4018
4063
|
selectedNodeStatus.activeRunIds.length > 0 ? (jsxRuntime.jsxs("div", { className: "mt-1", children: [jsxRuntime.jsx("div", { className: "text-[10px] text-blue-600", children: "RunIds:" }), jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 mt-1", children: selectedNodeStatus.activeRunIds.map((runId, idx) => (jsxRuntime.jsx("span", { className: "text-[10px] px-1.5 py-0.5 bg-blue-100 border border-blue-300 rounded font-mono", children: runId }, idx))) })] })) : (jsxRuntime.jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "RunIds not available (some runs may have started without runId)" }))] })), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
|
|
4019
4064
|
selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
|
|
4020
4065
|
const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
|
|
4021
|
-
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
4066
|
+
const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
4022
4067
|
e.target.handle === h);
|
|
4023
|
-
const inbound = new Set(def.edges
|
|
4068
|
+
const inbound = new Set(wb.def.edges
|
|
4024
4069
|
.filter((e) => e.target.nodeId === selectedNodeId &&
|
|
4025
4070
|
e.target.handle === h)
|
|
4026
4071
|
.map((e) => e.target.handle));
|
|
@@ -4596,10 +4641,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4596
4641
|
const { nodeTypes, resolveNodeType } = React.useMemo(() => {
|
|
4597
4642
|
// Build nodeTypes map using UI extension registry
|
|
4598
4643
|
const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
|
|
4599
|
-
const def = wb.export();
|
|
4600
4644
|
const ids = new Set([
|
|
4601
4645
|
...Array.from(registry.nodes.keys()),
|
|
4602
|
-
...def.nodes.map((n) => n.typeId),
|
|
4646
|
+
...wb.def.nodes.map((n) => n.typeId),
|
|
4603
4647
|
]);
|
|
4604
4648
|
for (const typeId of ids) {
|
|
4605
4649
|
const renderer = ui.getNodeRenderer(typeId);
|
|
@@ -4618,14 +4662,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4618
4662
|
// Include uiVersion to recompute when custom renderers are registered
|
|
4619
4663
|
}, [wb, registry, uiVersion, ui]);
|
|
4620
4664
|
const { nodes, edges } = React.useMemo(() => {
|
|
4621
|
-
const def = wb.export();
|
|
4622
4665
|
const sel = wb.getSelection();
|
|
4623
4666
|
// Merge defaults with inputs for node display (defaults shown in lighter gray)
|
|
4624
4667
|
const inputsWithDefaults = {};
|
|
4625
|
-
for (const n of def.nodes) {
|
|
4668
|
+
for (const n of wb.def.nodes) {
|
|
4626
4669
|
const nodeInputs = inputsMap[n.nodeId] ?? {};
|
|
4627
4670
|
const nodeDefaults = inputDefaultsMap[n.nodeId] ?? {};
|
|
4628
|
-
const inbound = new Set(def.edges
|
|
4671
|
+
const inbound = new Set(wb.def.edges
|
|
4629
4672
|
.filter((e) => e.target.nodeId === n.nodeId)
|
|
4630
4673
|
.map((e) => e.target.handle));
|
|
4631
4674
|
const merged = { ...nodeInputs };
|
|
@@ -4638,7 +4681,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4638
4681
|
inputsWithDefaults[n.nodeId] = merged;
|
|
4639
4682
|
}
|
|
4640
4683
|
}
|
|
4641
|
-
const out = toReactFlow(def, wb.getPositions(), registry, {
|
|
4684
|
+
const out = toReactFlow(wb.def, wb.getPositions(), registry, {
|
|
4642
4685
|
showValues,
|
|
4643
4686
|
inputs: inputsWithDefaults,
|
|
4644
4687
|
inputDefaults: inputDefaultsMap,
|
|
@@ -5162,11 +5205,11 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
5162
5205
|
});
|
|
5163
5206
|
|
|
5164
5207
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
|
|
5165
|
-
const { wb, runner, registry,
|
|
5208
|
+
const { wb, runner, registry, selectedNodeId, runAutoLayout } = useWorkbenchContext();
|
|
5166
5209
|
const [transportStatus, setTransportStatus] = React.useState({
|
|
5167
5210
|
state: "local",
|
|
5168
5211
|
});
|
|
5169
|
-
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
5212
|
+
const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
5170
5213
|
const effectiveHandles = selectedNode
|
|
5171
5214
|
? computeEffectiveHandles(selectedNode, registry)
|
|
5172
5215
|
: { inputs: {}, outputs: {}, inputDefaults: {} };
|
|
@@ -5194,7 +5237,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5194
5237
|
if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
|
|
5195
5238
|
return;
|
|
5196
5239
|
try {
|
|
5197
|
-
runner.launch(wb.
|
|
5240
|
+
runner.launch(wb.def, {
|
|
5198
5241
|
engine: kind,
|
|
5199
5242
|
invalidate: evt.shiftKey,
|
|
5200
5243
|
});
|
|
@@ -5262,12 +5305,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5262
5305
|
const setInitialGraph = async (d, inputs) => {
|
|
5263
5306
|
await wb.load(d);
|
|
5264
5307
|
try {
|
|
5265
|
-
runner.build(wb.
|
|
5308
|
+
runner.build(wb.def);
|
|
5266
5309
|
}
|
|
5267
5310
|
catch { }
|
|
5268
5311
|
if (inputs) {
|
|
5269
5312
|
for (const [nodeId, map] of Object.entries(inputs)) {
|
|
5270
|
-
runner.setInputs(nodeId, map);
|
|
5313
|
+
runner.setInputs(nodeId, map, { dry: true });
|
|
5271
5314
|
}
|
|
5272
5315
|
}
|
|
5273
5316
|
};
|
|
@@ -5277,36 +5320,27 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5277
5320
|
React.useEffect(() => {
|
|
5278
5321
|
if (!onChange)
|
|
5279
5322
|
return;
|
|
5280
|
-
const
|
|
5281
|
-
try {
|
|
5282
|
-
const cur = wb.export();
|
|
5283
|
-
const inputs = runner.getInputs(cur);
|
|
5284
|
-
onChange({ def: cur, inputs });
|
|
5285
|
-
}
|
|
5286
|
-
catch { }
|
|
5287
|
-
});
|
|
5288
|
-
const off2 = runner.on("value", () => {
|
|
5323
|
+
const offGraphChanged = wb.on("graphChanged", () => {
|
|
5289
5324
|
try {
|
|
5290
|
-
const cur = wb.
|
|
5325
|
+
const cur = wb.def;
|
|
5291
5326
|
const inputs = runner.getInputs(cur);
|
|
5292
5327
|
onChange({ def: cur, inputs });
|
|
5293
5328
|
}
|
|
5294
5329
|
catch { }
|
|
5295
5330
|
});
|
|
5296
|
-
const
|
|
5331
|
+
const offGraphUiChanged = wb.on("graphUiChanged", (evt) => {
|
|
5297
5332
|
if (!evt.commit)
|
|
5298
5333
|
return;
|
|
5299
5334
|
try {
|
|
5300
|
-
const cur = wb.
|
|
5335
|
+
const cur = wb.def;
|
|
5301
5336
|
const inputs = runner.getInputs(cur);
|
|
5302
5337
|
onChange({ def: cur, inputs });
|
|
5303
5338
|
}
|
|
5304
5339
|
catch { }
|
|
5305
5340
|
});
|
|
5306
5341
|
return () => {
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
off3();
|
|
5342
|
+
offGraphChanged();
|
|
5343
|
+
offGraphUiChanged();
|
|
5310
5344
|
};
|
|
5311
5345
|
}, [wb, runner, onChange]);
|
|
5312
5346
|
const applyExample = React.useCallback(async (key) => {
|
|
@@ -5329,11 +5363,11 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5329
5363
|
}
|
|
5330
5364
|
await wb.load(def);
|
|
5331
5365
|
// Build a local runtime so seeded defaults are visible pre-run
|
|
5332
|
-
runner.build(wb.
|
|
5366
|
+
runner.build(wb.def);
|
|
5333
5367
|
// Set initial inputs if provided
|
|
5334
5368
|
if (inputs) {
|
|
5335
5369
|
for (const [nodeId, map] of Object.entries(inputs)) {
|
|
5336
|
-
runner.setInputs(nodeId, map);
|
|
5370
|
+
runner.setInputs(nodeId, map, { dry: true });
|
|
5337
5371
|
}
|
|
5338
5372
|
}
|
|
5339
5373
|
runAutoLayout();
|
|
@@ -5414,11 +5448,10 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5414
5448
|
// Only auto-launch for local backend; require explicit Start for remote
|
|
5415
5449
|
if (backendKind !== "local")
|
|
5416
5450
|
return;
|
|
5417
|
-
|
|
5418
|
-
if (!d.nodes || d.nodes.length === 0)
|
|
5451
|
+
if (!wb.def.nodes || wb.def.nodes.length === 0)
|
|
5419
5452
|
return;
|
|
5420
5453
|
try {
|
|
5421
|
-
runner.launch(
|
|
5454
|
+
runner.launch(wb.def, { engine: engine });
|
|
5422
5455
|
}
|
|
5423
5456
|
catch {
|
|
5424
5457
|
// ignore
|
|
@@ -5428,12 +5461,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5428
5461
|
if (!selectedNodeId)
|
|
5429
5462
|
return;
|
|
5430
5463
|
// If selected input is wired (has inbound edge), ignore user input to respect runtime value
|
|
5431
|
-
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
|
|
5464
|
+
const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
|
|
5432
5465
|
if (isLinked)
|
|
5433
5466
|
return;
|
|
5434
5467
|
// If raw is undefined, pass it through to delete the input value
|
|
5435
5468
|
if (raw === undefined) {
|
|
5436
|
-
|
|
5469
|
+
wb.setInputs(selectedNodeId, { [handle]: undefined }, { commit: true });
|
|
5437
5470
|
return;
|
|
5438
5471
|
}
|
|
5439
5472
|
const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, handle);
|
|
@@ -5512,8 +5545,8 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5512
5545
|
value = raw;
|
|
5513
5546
|
}
|
|
5514
5547
|
}
|
|
5515
|
-
|
|
5516
|
-
}, [selectedNodeId, def.edges, effectiveHandles,
|
|
5548
|
+
wb.setInputs(selectedNodeId, { [handle]: value }, { commit: true });
|
|
5549
|
+
}, [selectedNodeId, wb.def.edges, effectiveHandles, wb]);
|
|
5517
5550
|
const setInput = React.useMemo(() => {
|
|
5518
5551
|
if (overrides?.setInput) {
|
|
5519
5552
|
return overrides.setInput(baseSetInput, {
|