@bian-womp/spark-workbench 0.2.78 → 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 +346 -283
- 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 +45 -2
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/core/contracts.d.ts +4 -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 +347 -284
- 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 +45 -2
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/esm/src/core/contracts.d.ts +4 -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",
|
|
@@ -195,15 +195,15 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
195
195
|
}
|
|
196
196
|
setInputs(nodeId, inputs, options) {
|
|
197
197
|
this.emit("graphChanged", {
|
|
198
|
-
def: this.
|
|
198
|
+
def: this._def,
|
|
199
199
|
change: { type: "setInputs", nodeId, inputs },
|
|
200
200
|
...options,
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
203
|
addNode(node, options) {
|
|
204
204
|
const id = node.nodeId ??
|
|
205
|
-
this.genId("n", new Set(this.
|
|
206
|
-
this.
|
|
205
|
+
this.genId("n", new Set(this._def.nodes.map((n) => n.nodeId)));
|
|
206
|
+
this._def.nodes.push({
|
|
207
207
|
nodeId: id,
|
|
208
208
|
typeId: node.typeId,
|
|
209
209
|
params: node.params,
|
|
@@ -212,7 +212,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
212
212
|
if (node.position)
|
|
213
213
|
this.positions[id] = node.position;
|
|
214
214
|
this.emit("graphChanged", {
|
|
215
|
-
def: this.
|
|
215
|
+
def: this._def,
|
|
216
216
|
change: {
|
|
217
217
|
type: "addNode",
|
|
218
218
|
nodeId: id,
|
|
@@ -225,26 +225,26 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
225
225
|
return id;
|
|
226
226
|
}
|
|
227
227
|
removeNode(nodeId, options) {
|
|
228
|
-
this.
|
|
229
|
-
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);
|
|
230
230
|
delete this.positions[nodeId];
|
|
231
231
|
this.emit("graphChanged", {
|
|
232
|
-
def: this.
|
|
232
|
+
def: this._def,
|
|
233
233
|
change: { type: "removeNode", nodeId },
|
|
234
234
|
...options,
|
|
235
235
|
});
|
|
236
236
|
this.refreshValidation();
|
|
237
237
|
}
|
|
238
238
|
connect(edge, options) {
|
|
239
|
-
const id = edge.id ?? this.genId("e", new Set(this.
|
|
240
|
-
this.
|
|
239
|
+
const id = edge.id ?? this.genId("e", new Set(this._def.edges.map((e) => e.id)));
|
|
240
|
+
this._def.edges.push({
|
|
241
241
|
id,
|
|
242
242
|
source: { ...edge.source },
|
|
243
243
|
target: { ...edge.target },
|
|
244
244
|
typeId: edge.typeId,
|
|
245
245
|
});
|
|
246
246
|
this.emit("graphChanged", {
|
|
247
|
-
def: this.
|
|
247
|
+
def: this._def,
|
|
248
248
|
change: { type: "connect", edgeId: id },
|
|
249
249
|
...options,
|
|
250
250
|
});
|
|
@@ -252,16 +252,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
252
252
|
return id;
|
|
253
253
|
}
|
|
254
254
|
disconnect(edgeId, options) {
|
|
255
|
-
this.
|
|
255
|
+
this._def.edges = this._def.edges.filter((e) => e.id !== edgeId);
|
|
256
256
|
this.emit("graphChanged", {
|
|
257
|
-
def: this.
|
|
257
|
+
def: this._def,
|
|
258
258
|
change: { type: "disconnect", edgeId },
|
|
259
259
|
...options,
|
|
260
260
|
});
|
|
261
261
|
this.emit("validationChanged", this.validate());
|
|
262
262
|
}
|
|
263
263
|
updateEdgeType(edgeId, typeId) {
|
|
264
|
-
const e = this.
|
|
264
|
+
const e = this._def.edges.find((x) => x.id === edgeId);
|
|
265
265
|
if (!e)
|
|
266
266
|
return;
|
|
267
267
|
if (!typeId)
|
|
@@ -269,18 +269,18 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
269
269
|
else
|
|
270
270
|
e.typeId = typeId;
|
|
271
271
|
this.emit("graphChanged", {
|
|
272
|
-
def: this.
|
|
272
|
+
def: this._def,
|
|
273
273
|
change: { type: "updateEdgeType", edgeId, typeId },
|
|
274
274
|
});
|
|
275
275
|
this.refreshValidation();
|
|
276
276
|
}
|
|
277
277
|
updateParams(nodeId, params) {
|
|
278
|
-
const n = this.
|
|
278
|
+
const n = this._def.nodes.find((n) => n.nodeId === nodeId);
|
|
279
279
|
if (!n)
|
|
280
280
|
return;
|
|
281
281
|
n.params = { ...(n.params ?? {}), ...params };
|
|
282
282
|
this.emit("graphChanged", {
|
|
283
|
-
def: this.
|
|
283
|
+
def: this._def,
|
|
284
284
|
change: { type: "updateParams", nodeId },
|
|
285
285
|
});
|
|
286
286
|
}
|
|
@@ -288,7 +288,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
288
288
|
setPositions(map, options) {
|
|
289
289
|
this.positions = { ...this.positions, ...map };
|
|
290
290
|
this.emit("graphUiChanged", {
|
|
291
|
-
def: this.
|
|
291
|
+
def: this._def,
|
|
292
292
|
change: { type: "moveNodes" },
|
|
293
293
|
...options,
|
|
294
294
|
});
|
|
@@ -296,17 +296,20 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
296
296
|
getPositions() {
|
|
297
297
|
return { ...this.positions };
|
|
298
298
|
}
|
|
299
|
-
|
|
299
|
+
setSelectionInternal(sel, options) {
|
|
300
300
|
if (lod.isEqual(this.selection, sel))
|
|
301
301
|
return;
|
|
302
302
|
this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
|
|
303
303
|
this.emit("selectionChanged", this.selection);
|
|
304
304
|
this.emit("graphUiChanged", {
|
|
305
|
-
def: this.
|
|
305
|
+
def: this._def,
|
|
306
306
|
change: { type: "selection" },
|
|
307
307
|
...options,
|
|
308
308
|
});
|
|
309
309
|
}
|
|
310
|
+
setSelection(sel, options) {
|
|
311
|
+
this.setSelectionInternal(sel, options);
|
|
312
|
+
}
|
|
310
313
|
getSelection() {
|
|
311
314
|
return {
|
|
312
315
|
nodes: [...this.selection.nodes],
|
|
@@ -327,14 +330,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
327
330
|
this.disconnect(edgeId);
|
|
328
331
|
}
|
|
329
332
|
// Clear selection
|
|
330
|
-
this.
|
|
333
|
+
this.setSelectionInternal({ nodes: [], edges: [] }, options);
|
|
331
334
|
}
|
|
332
335
|
setViewport(viewport) {
|
|
333
336
|
if (lod.isEqual(this.viewport, viewport))
|
|
334
337
|
return;
|
|
335
338
|
this.viewport = { ...viewport };
|
|
336
339
|
this.emit("graphUiChanged", {
|
|
337
|
-
def: this.
|
|
340
|
+
def: this._def,
|
|
338
341
|
change: { type: "viewport" },
|
|
339
342
|
});
|
|
340
343
|
}
|
|
@@ -342,8 +345,8 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
342
345
|
return this.viewport ? { ...this.viewport } : null;
|
|
343
346
|
}
|
|
344
347
|
getUIState() {
|
|
345
|
-
const defNodeIds = new Set(this.
|
|
346
|
-
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));
|
|
347
350
|
const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
|
|
348
351
|
const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
|
|
349
352
|
const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
|
|
@@ -382,6 +385,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
382
385
|
}
|
|
383
386
|
setRuntimeState(runtime) {
|
|
384
387
|
this.runtimeState = runtime ? { ...runtime } : null;
|
|
388
|
+
this.emit("runtimeMetadataChanged", {});
|
|
385
389
|
}
|
|
386
390
|
getHistory() {
|
|
387
391
|
return this.historyState;
|
|
@@ -398,6 +402,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
398
402
|
const nodeMeta = current.nodes[nodeId] ?? {};
|
|
399
403
|
const updated = updater({ ...nodeMeta });
|
|
400
404
|
this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
|
|
405
|
+
this.emit("runtimeMetadataChanged", { nodeId });
|
|
401
406
|
}
|
|
402
407
|
on(event, handler) {
|
|
403
408
|
if (!this.listeners.has(event))
|
|
@@ -422,11 +427,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
422
427
|
const selection = this.getSelection();
|
|
423
428
|
if (selection.nodes.length === 0)
|
|
424
429
|
return null;
|
|
425
|
-
const def = this.export();
|
|
426
430
|
const positions = this.getPositions();
|
|
427
431
|
const selectedNodeSet = new Set(selection.nodes);
|
|
428
432
|
// Collect nodes to copy
|
|
429
|
-
const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
|
|
433
|
+
const nodesToCopy = this.def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
|
|
430
434
|
if (nodesToCopy.length === 0)
|
|
431
435
|
return null;
|
|
432
436
|
// Calculate bounds
|
|
@@ -450,12 +454,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
450
454
|
const centerY = (bounds.minY + bounds.maxY) / 2;
|
|
451
455
|
// Get inputs for each node
|
|
452
456
|
// Include values from inbound edges if those edges are selected
|
|
453
|
-
const allInputs = runner.getInputs(def);
|
|
457
|
+
const allInputs = runner.getInputs(this.def);
|
|
454
458
|
const selectedEdgeSet = new Set(selection.edges);
|
|
455
459
|
const copiedNodes = nodesToCopy.map((node) => {
|
|
456
460
|
const pos = positions[node.nodeId] || { x: 0, y: 0 };
|
|
457
461
|
// Get all inbound edges for this node
|
|
458
|
-
const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
|
|
462
|
+
const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === node.nodeId);
|
|
459
463
|
// Build set of handles that have inbound edges
|
|
460
464
|
// But only exclude handles whose edges are NOT selected
|
|
461
465
|
const inboundHandlesToExclude = new Set(inboundEdges
|
|
@@ -479,7 +483,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
479
483
|
};
|
|
480
484
|
});
|
|
481
485
|
// Collect edges between copied nodes
|
|
482
|
-
const copiedEdges = def.edges
|
|
486
|
+
const copiedEdges = this.def.edges
|
|
483
487
|
.filter((edge) => {
|
|
484
488
|
return (selectedNodeSet.has(edge.source.nodeId) &&
|
|
485
489
|
selectedNodeSet.has(edge.target.nodeId));
|
|
@@ -497,6 +501,193 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
497
501
|
bounds,
|
|
498
502
|
};
|
|
499
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
|
+
}
|
|
500
691
|
/**
|
|
501
692
|
* Paste copied graph data at the specified center position.
|
|
502
693
|
* Returns the mapping from original node IDs to new node IDs.
|
|
@@ -542,7 +733,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
542
733
|
}
|
|
543
734
|
}
|
|
544
735
|
// Select the newly pasted nodes
|
|
545
|
-
this.
|
|
736
|
+
this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
546
737
|
return { nodeIdMap, edgeIds };
|
|
547
738
|
}
|
|
548
739
|
/**
|
|
@@ -567,8 +758,8 @@ class CLIWorkbench {
|
|
|
567
758
|
async load(def) {
|
|
568
759
|
await this.wb.load(def);
|
|
569
760
|
}
|
|
570
|
-
print(
|
|
571
|
-
const d =
|
|
761
|
+
print(options) {
|
|
762
|
+
const d = this.wb.def;
|
|
572
763
|
const detail = !!options?.detail;
|
|
573
764
|
const lines = [];
|
|
574
765
|
lines.push(`Nodes (${d.nodes.length})`);
|
|
@@ -1937,8 +2128,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1937
2128
|
if (!params.sourceHandle || !params.targetHandle)
|
|
1938
2129
|
return;
|
|
1939
2130
|
// Prevent duplicate edges between the same endpoints
|
|
1940
|
-
const
|
|
1941
|
-
const exists = def.edges.some((e) => e.source.nodeId === params.source &&
|
|
2131
|
+
const exists = wb.def.edges.some((e) => e.source.nodeId === params.source &&
|
|
1942
2132
|
e.source.handle === params.sourceHandle &&
|
|
1943
2133
|
e.target.nodeId === params.target &&
|
|
1944
2134
|
e.target.handle === params.targetHandle);
|
|
@@ -1988,7 +2178,10 @@ function useWorkbenchBridge(wb) {
|
|
|
1988
2178
|
}
|
|
1989
2179
|
}
|
|
1990
2180
|
if (selectionChanged) {
|
|
1991
|
-
wb.setSelection({
|
|
2181
|
+
wb.setSelection({
|
|
2182
|
+
nodes: Array.from(nextNodeIds),
|
|
2183
|
+
edges: current.edges,
|
|
2184
|
+
});
|
|
1992
2185
|
}
|
|
1993
2186
|
if (Object.keys(positions).length > 0) {
|
|
1994
2187
|
wb.setPositions(positions, { commit });
|
|
@@ -2023,7 +2216,10 @@ function useWorkbenchBridge(wb) {
|
|
|
2023
2216
|
}
|
|
2024
2217
|
}
|
|
2025
2218
|
if (selectionChanged) {
|
|
2026
|
-
wb.setSelection({
|
|
2219
|
+
wb.setSelection({
|
|
2220
|
+
nodes: current.nodes,
|
|
2221
|
+
edges: Array.from(nextEdgeIds),
|
|
2222
|
+
});
|
|
2027
2223
|
}
|
|
2028
2224
|
}, [wb]);
|
|
2029
2225
|
const onNodesDelete = React.useCallback((nodes) => {
|
|
@@ -2469,7 +2665,6 @@ function isSnapshotPayload(parsed) {
|
|
|
2469
2665
|
}
|
|
2470
2666
|
async function download(wb, runner) {
|
|
2471
2667
|
try {
|
|
2472
|
-
const def = wb.export();
|
|
2473
2668
|
const fullUiState = wb.getUIState();
|
|
2474
2669
|
const uiState = excludeViewportFromUIState(fullUiState);
|
|
2475
2670
|
const runtimeState = wb.getRuntimeState();
|
|
@@ -2478,7 +2673,7 @@ async function download(wb, runner) {
|
|
|
2478
2673
|
const fullSnapshot = await runner.snapshotFull();
|
|
2479
2674
|
snapshot = {
|
|
2480
2675
|
...fullSnapshot,
|
|
2481
|
-
def,
|
|
2676
|
+
def: wb.def,
|
|
2482
2677
|
extData: {
|
|
2483
2678
|
...(fullSnapshot.extData || {}),
|
|
2484
2679
|
ui: Object.keys(uiState || {}).length > 0 ? uiState : undefined,
|
|
@@ -2487,9 +2682,9 @@ async function download(wb, runner) {
|
|
|
2487
2682
|
};
|
|
2488
2683
|
}
|
|
2489
2684
|
else {
|
|
2490
|
-
const inputs = runner.getInputs(def);
|
|
2685
|
+
const inputs = runner.getInputs(wb.def);
|
|
2491
2686
|
snapshot = {
|
|
2492
|
-
def,
|
|
2687
|
+
def: wb.def,
|
|
2493
2688
|
inputs,
|
|
2494
2689
|
outputs: {},
|
|
2495
2690
|
environment: {},
|
|
@@ -2528,7 +2723,7 @@ async function upload(parsed, wb, runner) {
|
|
|
2528
2723
|
}
|
|
2529
2724
|
if (runner.isRunning()) {
|
|
2530
2725
|
await runner.applySnapshotFull({
|
|
2531
|
-
def,
|
|
2726
|
+
def: wb.def,
|
|
2532
2727
|
environment,
|
|
2533
2728
|
inputs,
|
|
2534
2729
|
outputs,
|
|
@@ -2536,7 +2731,7 @@ async function upload(parsed, wb, runner) {
|
|
|
2536
2731
|
});
|
|
2537
2732
|
}
|
|
2538
2733
|
else {
|
|
2539
|
-
runner.build(wb.
|
|
2734
|
+
runner.build(wb.def);
|
|
2540
2735
|
if (inputs && typeof inputs === "object") {
|
|
2541
2736
|
for (const [nodeId, map] of Object.entries(inputs)) {
|
|
2542
2737
|
runner.setInputs(nodeId, map, { dry: true });
|
|
@@ -2632,14 +2827,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2632
2827
|
const versionTick = useWorkbenchVersionTick(runner);
|
|
2633
2828
|
const valuesTick = versionTick + graphTick + graphUiTick;
|
|
2634
2829
|
// Def and IO values
|
|
2635
|
-
const
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
2638
|
-
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]);
|
|
2639
2833
|
const outputTypesMap = React.useMemo(() => {
|
|
2640
2834
|
const out = {};
|
|
2641
2835
|
// Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
|
|
2642
|
-
for (const n of def.nodes) {
|
|
2836
|
+
for (const n of wb.def.nodes) {
|
|
2643
2837
|
const effectiveHandles = computeEffectiveHandles(n, registry);
|
|
2644
2838
|
const outputsDecl = effectiveHandles.outputs;
|
|
2645
2839
|
const handles = Object.keys(outputsDecl);
|
|
@@ -2654,14 +2848,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2654
2848
|
out[n.nodeId] = cur;
|
|
2655
2849
|
}
|
|
2656
2850
|
return out;
|
|
2657
|
-
}, [def, outputsMap, registry]);
|
|
2851
|
+
}, [wb, wb.def, outputsMap, registry]);
|
|
2658
2852
|
// Initialize nodes and derive invalidated status from persisted metadata
|
|
2659
2853
|
React.useEffect(() => {
|
|
2660
2854
|
const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
|
|
2661
2855
|
setNodeStatus((prev) => {
|
|
2662
2856
|
const next = { ...prev };
|
|
2663
2857
|
const metadata = workbenchRuntimeState;
|
|
2664
|
-
for (const n of def.nodes) {
|
|
2858
|
+
for (const n of wb.def.nodes) {
|
|
2665
2859
|
const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
|
|
2666
2860
|
const nodeMeta = metadata.nodes[n.nodeId];
|
|
2667
2861
|
const updates = {};
|
|
@@ -2683,18 +2877,17 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2683
2877
|
}
|
|
2684
2878
|
return next;
|
|
2685
2879
|
});
|
|
2686
|
-
}, [def, wb]);
|
|
2880
|
+
}, [wb.def, wb]);
|
|
2687
2881
|
// Auto layout (simple layered layout)
|
|
2688
2882
|
const runAutoLayout = React.useCallback(() => {
|
|
2689
|
-
const cur = wb.export();
|
|
2690
2883
|
// Build DAG layers by indegree
|
|
2691
2884
|
const indegree = {};
|
|
2692
2885
|
const adj = {};
|
|
2693
|
-
for (const n of
|
|
2886
|
+
for (const n of wb.def.nodes) {
|
|
2694
2887
|
indegree[n.nodeId] = 0;
|
|
2695
2888
|
adj[n.nodeId] = [];
|
|
2696
2889
|
}
|
|
2697
|
-
for (const e of
|
|
2890
|
+
for (const e of wb.def.edges) {
|
|
2698
2891
|
indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
|
|
2699
2892
|
adj[e.source.nodeId].push(e.target.nodeId);
|
|
2700
2893
|
}
|
|
@@ -2725,7 +2918,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2725
2918
|
let maxWidth = 0;
|
|
2726
2919
|
const heights = {};
|
|
2727
2920
|
for (const id of layer) {
|
|
2728
|
-
const node =
|
|
2921
|
+
const node = wb.def.nodes.find((n) => n.nodeId === id);
|
|
2729
2922
|
if (!node)
|
|
2730
2923
|
continue;
|
|
2731
2924
|
// Prefer showValues sizing similar to node rendering
|
|
@@ -2751,26 +2944,26 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2751
2944
|
curX += maxWidth + H_GAP;
|
|
2752
2945
|
}
|
|
2753
2946
|
wb.setPositions(pos, { commit: true, reason: "auto-layout" });
|
|
2754
|
-
}, [wb, registry, overrides?.getDefaultNodeSize]);
|
|
2947
|
+
}, [wb, wb.def, registry, overrides?.getDefaultNodeSize]);
|
|
2755
2948
|
const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
|
|
2756
2949
|
const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
|
|
2757
2950
|
// Helper to save runtime metadata and UI state to extData
|
|
2758
|
-
const saveUiRuntimeMetadata = React.useCallback(async () => {
|
|
2951
|
+
const saveUiRuntimeMetadata = React.useCallback(async (workbench, graphRunner) => {
|
|
2759
2952
|
try {
|
|
2760
|
-
const current =
|
|
2953
|
+
const current = workbench.getRuntimeState() ?? { nodes: {} };
|
|
2761
2954
|
const metadata = { nodes: { ...current.nodes } };
|
|
2762
2955
|
// Clean up metadata for nodes that no longer exist
|
|
2763
|
-
const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
|
|
2956
|
+
const nodeIds = new Set(workbench.def.nodes.map((n) => n.nodeId));
|
|
2764
2957
|
for (const nodeId of Object.keys(metadata.nodes)) {
|
|
2765
2958
|
if (!nodeIds.has(nodeId)) {
|
|
2766
2959
|
delete metadata.nodes[nodeId];
|
|
2767
2960
|
}
|
|
2768
2961
|
}
|
|
2769
2962
|
// Save cleaned metadata to workbench state
|
|
2770
|
-
|
|
2771
|
-
const fullUiState =
|
|
2963
|
+
workbench.setRuntimeState(metadata);
|
|
2964
|
+
const fullUiState = workbench.getUIState();
|
|
2772
2965
|
const uiWithoutViewport = excludeViewportFromUIState(fullUiState);
|
|
2773
|
-
await
|
|
2966
|
+
await graphRunner.setExtData?.({
|
|
2774
2967
|
...(Object.keys(uiWithoutViewport || {}).length > 0
|
|
2775
2968
|
? { ui: uiWithoutViewport }
|
|
2776
2969
|
: {}),
|
|
@@ -2780,7 +2973,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2780
2973
|
catch (err) {
|
|
2781
2974
|
console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
|
|
2782
2975
|
}
|
|
2783
|
-
}, [
|
|
2976
|
+
}, []);
|
|
2784
2977
|
// Subscribe to runner/workbench events
|
|
2785
2978
|
React.useEffect(() => {
|
|
2786
2979
|
const add = (source, type) => (payload) => setEvents((prev) => {
|
|
@@ -2806,9 +2999,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2806
2999
|
});
|
|
2807
3000
|
// Helper to apply resolved handles from event payload to workbench
|
|
2808
3001
|
const applyResolvedHandles = (resolvedHandles) => {
|
|
2809
|
-
const cur = wb.export();
|
|
2810
3002
|
let changed = false;
|
|
2811
|
-
for (const n of
|
|
3003
|
+
for (const n of wb.def.nodes) {
|
|
2812
3004
|
const updated = resolvedHandles[n.nodeId];
|
|
2813
3005
|
if (updated) {
|
|
2814
3006
|
const before = JSON.stringify(n.resolvedHandles || {});
|
|
@@ -3097,6 +3289,9 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3097
3289
|
else if (changeType === "updateEdgeType") {
|
|
3098
3290
|
reason = "update-edge-type";
|
|
3099
3291
|
}
|
|
3292
|
+
else if (changeType === "setInputs") {
|
|
3293
|
+
reason = "set-inputs";
|
|
3294
|
+
}
|
|
3100
3295
|
}
|
|
3101
3296
|
if (event.change?.type === "setInputs") {
|
|
3102
3297
|
const { nodeId, inputs } = event.change;
|
|
@@ -3104,7 +3299,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3104
3299
|
}
|
|
3105
3300
|
if (!runner.isRunning()) {
|
|
3106
3301
|
if (event.commit) {
|
|
3107
|
-
await saveUiRuntimeMetadata();
|
|
3302
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3108
3303
|
const history = await runner.commit(reason).catch((err) => {
|
|
3109
3304
|
console.error("[WorkbenchContext] Error committing:", err);
|
|
3110
3305
|
return undefined;
|
|
@@ -3137,7 +3332,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3137
3332
|
await runner.update(event.def, { dry: event.dry });
|
|
3138
3333
|
}
|
|
3139
3334
|
if (event.commit) {
|
|
3140
|
-
await saveUiRuntimeMetadata();
|
|
3335
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3141
3336
|
const history = await runner
|
|
3142
3337
|
.commit(event.reason ?? reason)
|
|
3143
3338
|
.catch((err) => {
|
|
@@ -3158,7 +3353,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3158
3353
|
setSelectedNodeId(sel.nodes?.[0]);
|
|
3159
3354
|
setSelectedEdgeId(sel.edges?.[0]);
|
|
3160
3355
|
if (sel.commit) {
|
|
3161
|
-
await saveUiRuntimeMetadata();
|
|
3356
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3162
3357
|
const history = await runner
|
|
3163
3358
|
.commit(sel.reason ?? "selection")
|
|
3164
3359
|
.catch((err) => {
|
|
@@ -3197,7 +3392,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3197
3392
|
reason = "selection";
|
|
3198
3393
|
}
|
|
3199
3394
|
}
|
|
3200
|
-
await saveUiRuntimeMetadata();
|
|
3395
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3201
3396
|
const history = await runner
|
|
3202
3397
|
.commit(event.reason ?? reason)
|
|
3203
3398
|
.catch((err) => {
|
|
@@ -3217,7 +3412,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3217
3412
|
wb.setRegistry(newReg);
|
|
3218
3413
|
// Trigger a graph update so the UI revalidates with new types/enums/nodes
|
|
3219
3414
|
try {
|
|
3220
|
-
await runner.update(wb.
|
|
3415
|
+
await runner.update(wb.def);
|
|
3221
3416
|
}
|
|
3222
3417
|
catch {
|
|
3223
3418
|
console.error("Failed to update graph definition after registry changed");
|
|
@@ -3240,7 +3435,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3240
3435
|
setNodeStatus(() => {
|
|
3241
3436
|
const next = {};
|
|
3242
3437
|
const metadata = wb.getRuntimeState() ?? { nodes: {} };
|
|
3243
|
-
for (const n of def.nodes) {
|
|
3438
|
+
for (const n of wb.def.nodes) {
|
|
3244
3439
|
const nodeMeta = metadata.nodes[n.nodeId];
|
|
3245
3440
|
next[n.nodeId] = {
|
|
3246
3441
|
activeRuns: 0,
|
|
@@ -3255,6 +3450,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3255
3450
|
errorRunsRef.current = {};
|
|
3256
3451
|
}
|
|
3257
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
|
+
});
|
|
3258
3477
|
wb.refreshValidation();
|
|
3259
3478
|
return () => {
|
|
3260
3479
|
offRunnerValue();
|
|
@@ -3272,13 +3491,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3272
3491
|
offRunnerRegistry();
|
|
3273
3492
|
offRunnerTransport();
|
|
3274
3493
|
offFlowViewport();
|
|
3494
|
+
offWbRuntimeMetadataChanged();
|
|
3275
3495
|
};
|
|
3276
3496
|
}, [runner, wb, setRegistry]);
|
|
3277
3497
|
const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
|
|
3278
3498
|
const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
|
|
3279
3499
|
const start = React.useCallback((engine) => {
|
|
3280
3500
|
try {
|
|
3281
|
-
runner.launch(wb.
|
|
3501
|
+
runner.launch(wb.def, { engine });
|
|
3282
3502
|
}
|
|
3283
3503
|
catch { }
|
|
3284
3504
|
}, [runner, wb]);
|
|
@@ -3353,7 +3573,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3353
3573
|
runner,
|
|
3354
3574
|
registry,
|
|
3355
3575
|
setRegistry,
|
|
3356
|
-
def,
|
|
3357
3576
|
selectedNodeId,
|
|
3358
3577
|
selectedEdgeId,
|
|
3359
3578
|
setSelection,
|
|
@@ -3394,7 +3613,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3394
3613
|
runner,
|
|
3395
3614
|
registry,
|
|
3396
3615
|
setRegistry,
|
|
3397
|
-
def,
|
|
3398
3616
|
selectedNodeId,
|
|
3399
3617
|
selectedEdgeId,
|
|
3400
3618
|
setSelection,
|
|
@@ -3434,137 +3652,23 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3434
3652
|
}
|
|
3435
3653
|
|
|
3436
3654
|
function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
|
|
3437
|
-
const doBake = async (handleId) => {
|
|
3438
|
-
try {
|
|
3439
|
-
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
3440
|
-
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
3441
|
-
let newNodeId;
|
|
3442
|
-
if (!typeId || raw === undefined)
|
|
3443
|
-
return;
|
|
3444
|
-
const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
|
|
3445
|
-
const coerceIfNeeded = async (fromType, toType, value) => {
|
|
3446
|
-
if (!toType || toType === fromType || !runner?.coerce)
|
|
3447
|
-
return value;
|
|
3448
|
-
try {
|
|
3449
|
-
return await runner.coerce(fromType, toType, value);
|
|
3450
|
-
}
|
|
3451
|
-
catch {
|
|
3452
|
-
return value;
|
|
3453
|
-
}
|
|
3454
|
-
};
|
|
3455
|
-
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3456
|
-
const isArray = typeId.endsWith("[]");
|
|
3457
|
-
const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
|
|
3458
|
-
const tArr = isArray ? registry.types.get(typeId) : undefined;
|
|
3459
|
-
const tElem = registry.types.get(baseTypeId);
|
|
3460
|
-
const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
|
|
3461
|
-
const arrTarget = isArray ? tArr?.bakeTarget : undefined;
|
|
3462
|
-
const elemTarget = isArray ? tElem?.bakeTarget : undefined;
|
|
3463
|
-
if (singleTarget) {
|
|
3464
|
-
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
3465
|
-
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
3466
|
-
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3467
|
-
newNodeId = wb.addNode({
|
|
3468
|
-
typeId: singleTarget.nodeTypeId,
|
|
3469
|
-
position: { x: pos.x + 180, y: pos.y },
|
|
3470
|
-
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
3471
|
-
}
|
|
3472
|
-
else if (isArray && arrTarget) {
|
|
3473
|
-
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
3474
|
-
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
3475
|
-
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3476
|
-
newNodeId = wb.addNode({
|
|
3477
|
-
typeId: arrTarget.nodeTypeId,
|
|
3478
|
-
position: { x: pos.x + 180, y: pos.y },
|
|
3479
|
-
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
3480
|
-
}
|
|
3481
|
-
else if (isArray && elemTarget) {
|
|
3482
|
-
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
3483
|
-
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
3484
|
-
const src = unwrap(raw);
|
|
3485
|
-
const items = Array.isArray(src) ? src : [src];
|
|
3486
|
-
const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
|
|
3487
|
-
const COLS = 4;
|
|
3488
|
-
const DX = 180;
|
|
3489
|
-
const DY = 160;
|
|
3490
|
-
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
3491
|
-
const col = idx % COLS;
|
|
3492
|
-
const row = Math.floor(idx / COLS);
|
|
3493
|
-
newNodeId = wb.addNode({
|
|
3494
|
-
typeId: elemTarget.nodeTypeId,
|
|
3495
|
-
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
3496
|
-
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
if (newNodeId) {
|
|
3500
|
-
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "bake" });
|
|
3501
|
-
}
|
|
3502
|
-
}
|
|
3503
|
-
catch { }
|
|
3504
|
-
};
|
|
3505
3655
|
return {
|
|
3506
3656
|
onDelete: () => {
|
|
3507
3657
|
wb.removeNode(nodeId, { commit: true });
|
|
3508
3658
|
onClose();
|
|
3509
3659
|
},
|
|
3510
3660
|
onDuplicate: async () => {
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
return onClose();
|
|
3515
|
-
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3516
|
-
const inboundHandles = new Set(def.edges
|
|
3517
|
-
.filter((e) => e.target.nodeId === nodeId)
|
|
3518
|
-
.map((e) => e.target.handle));
|
|
3519
|
-
const allInputs = runner.getInputs(def)[nodeId] || {};
|
|
3520
|
-
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
3521
|
-
const newNodeId = wb.addNode({
|
|
3522
|
-
typeId: n.typeId,
|
|
3523
|
-
params: n.params,
|
|
3524
|
-
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3525
|
-
resolvedHandles: n.resolvedHandles,
|
|
3526
|
-
}, {
|
|
3527
|
-
inputs: inputsWithoutBindings,
|
|
3528
|
-
copyOutputsFrom: nodeId,
|
|
3529
|
-
dry: true,
|
|
3661
|
+
wb.duplicateNode(nodeId, runner, {
|
|
3662
|
+
commit: true,
|
|
3663
|
+
reason: "duplicate-node",
|
|
3530
3664
|
});
|
|
3531
|
-
// Select the newly duplicated node
|
|
3532
|
-
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
|
|
3533
3665
|
onClose();
|
|
3534
3666
|
},
|
|
3535
3667
|
onDuplicateWithEdges: async () => {
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
return onClose();
|
|
3540
|
-
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3541
|
-
// Get inputs without bindings (literal values only)
|
|
3542
|
-
const inputs = runner.getInputs(def)[nodeId] || {};
|
|
3543
|
-
// Add the duplicated node
|
|
3544
|
-
const newNodeId = wb.addNode({
|
|
3545
|
-
typeId: n.typeId,
|
|
3546
|
-
params: n.params,
|
|
3547
|
-
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3548
|
-
resolvedHandles: n.resolvedHandles,
|
|
3549
|
-
}, {
|
|
3550
|
-
inputs,
|
|
3551
|
-
copyOutputsFrom: nodeId,
|
|
3552
|
-
dry: true,
|
|
3668
|
+
wb.duplicateNodeWithEdges(nodeId, runner, {
|
|
3669
|
+
commit: true,
|
|
3670
|
+
reason: "duplicate-node-with-edges",
|
|
3553
3671
|
});
|
|
3554
|
-
// Find all incoming edges (edges where target is the original node)
|
|
3555
|
-
const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
3556
|
-
// Duplicate each incoming edge to point to the new node
|
|
3557
|
-
for (const edge of incomingEdges) {
|
|
3558
|
-
wb.connect({
|
|
3559
|
-
source: edge.source, // Keep the same source
|
|
3560
|
-
target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
|
|
3561
|
-
typeId: edge.typeId,
|
|
3562
|
-
}, { dry: true });
|
|
3563
|
-
}
|
|
3564
|
-
// Select the newly duplicated node and edges
|
|
3565
|
-
if (newNodeId) {
|
|
3566
|
-
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate-with-edges" });
|
|
3567
|
-
}
|
|
3568
3672
|
onClose();
|
|
3569
3673
|
},
|
|
3570
3674
|
onRunPull: async () => {
|
|
@@ -3575,7 +3679,13 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3575
3679
|
onClose();
|
|
3576
3680
|
},
|
|
3577
3681
|
onBake: async (handleId) => {
|
|
3578
|
-
|
|
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
|
+
});
|
|
3579
3689
|
onClose();
|
|
3580
3690
|
},
|
|
3581
3691
|
onCopy: () => {
|
|
@@ -3632,42 +3742,10 @@ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyRes
|
|
|
3632
3742
|
function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
|
|
3633
3743
|
const onDuplicate = runner
|
|
3634
3744
|
? () => {
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
}
|
|
3640
|
-
const def = wb.export();
|
|
3641
|
-
const positions = wb.getPositions();
|
|
3642
|
-
const newNodes = [];
|
|
3643
|
-
// Duplicate each selected node
|
|
3644
|
-
for (const nodeId of selection.nodes) {
|
|
3645
|
-
const n = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3646
|
-
if (!n)
|
|
3647
|
-
continue;
|
|
3648
|
-
const pos = positions[nodeId] || { x: 0, y: 0 };
|
|
3649
|
-
// Get inputs without bindings (literal values only)
|
|
3650
|
-
const allInputs = runner.getInputs(def)[nodeId] || {};
|
|
3651
|
-
const inboundHandles = new Set(def.edges
|
|
3652
|
-
.filter((e) => e.target.nodeId === nodeId)
|
|
3653
|
-
.map((e) => e.target.handle));
|
|
3654
|
-
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
3655
|
-
const newNodeId = wb.addNode({
|
|
3656
|
-
typeId: n.typeId,
|
|
3657
|
-
params: n.params,
|
|
3658
|
-
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3659
|
-
resolvedHandles: n.resolvedHandles,
|
|
3660
|
-
}, {
|
|
3661
|
-
inputs: inputsWithoutBindings,
|
|
3662
|
-
copyOutputsFrom: nodeId,
|
|
3663
|
-
dry: true,
|
|
3664
|
-
});
|
|
3665
|
-
newNodes.push(newNodeId);
|
|
3666
|
-
}
|
|
3667
|
-
// Select all newly duplicated nodes
|
|
3668
|
-
if (newNodes.length > 0) {
|
|
3669
|
-
wb.setSelection({ nodes: newNodes, edges: [] }, { commit: true, reason: "duplicate-selection" });
|
|
3670
|
-
}
|
|
3745
|
+
wb.duplicateSelection(runner, {
|
|
3746
|
+
commit: true,
|
|
3747
|
+
reason: "duplicate-selection",
|
|
3748
|
+
});
|
|
3671
3749
|
onClose();
|
|
3672
3750
|
}
|
|
3673
3751
|
: undefined;
|
|
@@ -3702,9 +3780,8 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
|
|
|
3702
3780
|
const canRedo = history ? history.redoCount > 0 : undefined;
|
|
3703
3781
|
const onSelectAll = wb
|
|
3704
3782
|
? () => {
|
|
3705
|
-
const
|
|
3706
|
-
const
|
|
3707
|
-
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);
|
|
3708
3785
|
wb.setSelection({ nodes: allNodeIds, edges: allEdgeIds }, { commit: true, reason: "select-all" });
|
|
3709
3786
|
onClose();
|
|
3710
3787
|
}
|
|
@@ -3723,8 +3800,7 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
|
|
|
3723
3800
|
}
|
|
3724
3801
|
function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
|
|
3725
3802
|
try {
|
|
3726
|
-
const
|
|
3727
|
-
const node = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3803
|
+
const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
|
|
3728
3804
|
if (!node)
|
|
3729
3805
|
return [];
|
|
3730
3806
|
const desc = registry.nodes.get(node.typeId);
|
|
@@ -3823,13 +3899,13 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3823
3899
|
return String(value ?? "");
|
|
3824
3900
|
}
|
|
3825
3901
|
};
|
|
3826
|
-
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();
|
|
3827
3903
|
const nodeValidationIssues = validationByNode.issues;
|
|
3828
3904
|
const edgeValidationIssues = validationByEdge.issues;
|
|
3829
3905
|
const nodeValidationHandles = validationByNode;
|
|
3830
3906
|
const globalValidationIssues = validationGlobal;
|
|
3831
|
-
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
3832
|
-
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);
|
|
3833
3909
|
// Use computeEffectiveHandles to merge registry defaults with dynamically resolved handles
|
|
3834
3910
|
const effectiveHandles = selectedNode
|
|
3835
3911
|
? computeEffectiveHandles(selectedNode, registry)
|
|
@@ -3961,7 +4037,6 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3961
4037
|
setOriginals(nextOriginals);
|
|
3962
4038
|
}, [selectedNodeId, selectedNode, registry, valuesTick]);
|
|
3963
4039
|
const widthClass = debug ? "w-[480px]" : "w-[320px]";
|
|
3964
|
-
const { wb } = useWorkbenchContext();
|
|
3965
4040
|
const deleteEdgeById = (edgeId) => {
|
|
3966
4041
|
if (!edgeId)
|
|
3967
4042
|
return;
|
|
@@ -3988,9 +4063,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3988
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 ??
|
|
3989
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) => {
|
|
3990
4065
|
const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
|
|
3991
|
-
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
4066
|
+
const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
3992
4067
|
e.target.handle === h);
|
|
3993
|
-
const inbound = new Set(def.edges
|
|
4068
|
+
const inbound = new Set(wb.def.edges
|
|
3994
4069
|
.filter((e) => e.target.nodeId === selectedNodeId &&
|
|
3995
4070
|
e.target.handle === h)
|
|
3996
4071
|
.map((e) => e.target.handle));
|
|
@@ -4566,10 +4641,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4566
4641
|
const { nodeTypes, resolveNodeType } = React.useMemo(() => {
|
|
4567
4642
|
// Build nodeTypes map using UI extension registry
|
|
4568
4643
|
const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
|
|
4569
|
-
const def = wb.export();
|
|
4570
4644
|
const ids = new Set([
|
|
4571
4645
|
...Array.from(registry.nodes.keys()),
|
|
4572
|
-
...def.nodes.map((n) => n.typeId),
|
|
4646
|
+
...wb.def.nodes.map((n) => n.typeId),
|
|
4573
4647
|
]);
|
|
4574
4648
|
for (const typeId of ids) {
|
|
4575
4649
|
const renderer = ui.getNodeRenderer(typeId);
|
|
@@ -4588,14 +4662,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4588
4662
|
// Include uiVersion to recompute when custom renderers are registered
|
|
4589
4663
|
}, [wb, registry, uiVersion, ui]);
|
|
4590
4664
|
const { nodes, edges } = React.useMemo(() => {
|
|
4591
|
-
const def = wb.export();
|
|
4592
4665
|
const sel = wb.getSelection();
|
|
4593
4666
|
// Merge defaults with inputs for node display (defaults shown in lighter gray)
|
|
4594
4667
|
const inputsWithDefaults = {};
|
|
4595
|
-
for (const n of def.nodes) {
|
|
4668
|
+
for (const n of wb.def.nodes) {
|
|
4596
4669
|
const nodeInputs = inputsMap[n.nodeId] ?? {};
|
|
4597
4670
|
const nodeDefaults = inputDefaultsMap[n.nodeId] ?? {};
|
|
4598
|
-
const inbound = new Set(def.edges
|
|
4671
|
+
const inbound = new Set(wb.def.edges
|
|
4599
4672
|
.filter((e) => e.target.nodeId === n.nodeId)
|
|
4600
4673
|
.map((e) => e.target.handle));
|
|
4601
4674
|
const merged = { ...nodeInputs };
|
|
@@ -4608,7 +4681,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4608
4681
|
inputsWithDefaults[n.nodeId] = merged;
|
|
4609
4682
|
}
|
|
4610
4683
|
}
|
|
4611
|
-
const out = toReactFlow(def, wb.getPositions(), registry, {
|
|
4684
|
+
const out = toReactFlow(wb.def, wb.getPositions(), registry, {
|
|
4612
4685
|
showValues,
|
|
4613
4686
|
inputs: inputsWithDefaults,
|
|
4614
4687
|
inputDefaults: inputDefaultsMap,
|
|
@@ -5132,11 +5205,11 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
5132
5205
|
});
|
|
5133
5206
|
|
|
5134
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, }) {
|
|
5135
|
-
const { wb, runner, registry,
|
|
5208
|
+
const { wb, runner, registry, selectedNodeId, runAutoLayout } = useWorkbenchContext();
|
|
5136
5209
|
const [transportStatus, setTransportStatus] = React.useState({
|
|
5137
5210
|
state: "local",
|
|
5138
5211
|
});
|
|
5139
|
-
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
5212
|
+
const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
5140
5213
|
const effectiveHandles = selectedNode
|
|
5141
5214
|
? computeEffectiveHandles(selectedNode, registry)
|
|
5142
5215
|
: { inputs: {}, outputs: {}, inputDefaults: {} };
|
|
@@ -5164,7 +5237,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5164
5237
|
if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
|
|
5165
5238
|
return;
|
|
5166
5239
|
try {
|
|
5167
|
-
runner.launch(wb.
|
|
5240
|
+
runner.launch(wb.def, {
|
|
5168
5241
|
engine: kind,
|
|
5169
5242
|
invalidate: evt.shiftKey,
|
|
5170
5243
|
});
|
|
@@ -5232,7 +5305,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5232
5305
|
const setInitialGraph = async (d, inputs) => {
|
|
5233
5306
|
await wb.load(d);
|
|
5234
5307
|
try {
|
|
5235
|
-
runner.build(wb.
|
|
5308
|
+
runner.build(wb.def);
|
|
5236
5309
|
}
|
|
5237
5310
|
catch { }
|
|
5238
5311
|
if (inputs) {
|
|
@@ -5247,36 +5320,27 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5247
5320
|
React.useEffect(() => {
|
|
5248
5321
|
if (!onChange)
|
|
5249
5322
|
return;
|
|
5250
|
-
const
|
|
5251
|
-
try {
|
|
5252
|
-
const cur = wb.export();
|
|
5253
|
-
const inputs = runner.getInputs(cur);
|
|
5254
|
-
onChange({ def: cur, inputs });
|
|
5255
|
-
}
|
|
5256
|
-
catch { }
|
|
5257
|
-
});
|
|
5258
|
-
const off2 = runner.on("value", () => {
|
|
5323
|
+
const offGraphChanged = wb.on("graphChanged", () => {
|
|
5259
5324
|
try {
|
|
5260
|
-
const cur = wb.
|
|
5325
|
+
const cur = wb.def;
|
|
5261
5326
|
const inputs = runner.getInputs(cur);
|
|
5262
5327
|
onChange({ def: cur, inputs });
|
|
5263
5328
|
}
|
|
5264
5329
|
catch { }
|
|
5265
5330
|
});
|
|
5266
|
-
const
|
|
5331
|
+
const offGraphUiChanged = wb.on("graphUiChanged", (evt) => {
|
|
5267
5332
|
if (!evt.commit)
|
|
5268
5333
|
return;
|
|
5269
5334
|
try {
|
|
5270
|
-
const cur = wb.
|
|
5335
|
+
const cur = wb.def;
|
|
5271
5336
|
const inputs = runner.getInputs(cur);
|
|
5272
5337
|
onChange({ def: cur, inputs });
|
|
5273
5338
|
}
|
|
5274
5339
|
catch { }
|
|
5275
5340
|
});
|
|
5276
5341
|
return () => {
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
off3();
|
|
5342
|
+
offGraphChanged();
|
|
5343
|
+
offGraphUiChanged();
|
|
5280
5344
|
};
|
|
5281
5345
|
}, [wb, runner, onChange]);
|
|
5282
5346
|
const applyExample = React.useCallback(async (key) => {
|
|
@@ -5299,7 +5363,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5299
5363
|
}
|
|
5300
5364
|
await wb.load(def);
|
|
5301
5365
|
// Build a local runtime so seeded defaults are visible pre-run
|
|
5302
|
-
runner.build(wb.
|
|
5366
|
+
runner.build(wb.def);
|
|
5303
5367
|
// Set initial inputs if provided
|
|
5304
5368
|
if (inputs) {
|
|
5305
5369
|
for (const [nodeId, map] of Object.entries(inputs)) {
|
|
@@ -5384,11 +5448,10 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5384
5448
|
// Only auto-launch for local backend; require explicit Start for remote
|
|
5385
5449
|
if (backendKind !== "local")
|
|
5386
5450
|
return;
|
|
5387
|
-
|
|
5388
|
-
if (!d.nodes || d.nodes.length === 0)
|
|
5451
|
+
if (!wb.def.nodes || wb.def.nodes.length === 0)
|
|
5389
5452
|
return;
|
|
5390
5453
|
try {
|
|
5391
|
-
runner.launch(
|
|
5454
|
+
runner.launch(wb.def, { engine: engine });
|
|
5392
5455
|
}
|
|
5393
5456
|
catch {
|
|
5394
5457
|
// ignore
|
|
@@ -5398,7 +5461,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5398
5461
|
if (!selectedNodeId)
|
|
5399
5462
|
return;
|
|
5400
5463
|
// If selected input is wired (has inbound edge), ignore user input to respect runtime value
|
|
5401
|
-
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);
|
|
5402
5465
|
if (isLinked)
|
|
5403
5466
|
return;
|
|
5404
5467
|
// If raw is undefined, pass it through to delete the input value
|
|
@@ -5483,7 +5546,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5483
5546
|
}
|
|
5484
5547
|
}
|
|
5485
5548
|
wb.setInputs(selectedNodeId, { [handle]: value }, { commit: true });
|
|
5486
|
-
}, [selectedNodeId, def.edges, effectiveHandles, wb]);
|
|
5549
|
+
}, [selectedNodeId, wb.def.edges, effectiveHandles, wb]);
|
|
5487
5550
|
const setInput = React.useMemo(() => {
|
|
5488
5551
|
if (overrides?.setInput) {
|
|
5489
5552
|
return overrides.setInput(baseSetInput, {
|