@bian-womp/spark-workbench 0.2.78 → 0.2.80
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 +456 -287
- 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 +61 -3
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/core/contracts.d.ts +7 -2
- package/lib/cjs/src/core/contracts.d.ts.map +1 -1
- package/lib/cjs/src/misc/DefaultNode.d.ts +3 -2
- package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
- package/lib/cjs/src/misc/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 +2 -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 +457 -288
- 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 +61 -3
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/esm/src/core/contracts.d.ts +7 -2
- package/lib/esm/src/core/contracts.d.ts.map +1 -1
- package/lib/esm/src/misc/DefaultNode.d.ts +3 -2
- package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
- package/lib/esm/src/misc/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 +2 -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,52 +123,53 @@ 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 = {
|
|
130
130
|
nodes: [],
|
|
131
131
|
edges: [],
|
|
132
132
|
};
|
|
133
|
-
this.
|
|
133
|
+
this.nodeNames = {};
|
|
134
134
|
this.runtimeState = null;
|
|
135
|
+
this.viewport = null;
|
|
135
136
|
this.historyState = undefined;
|
|
136
137
|
this.copiedData = null;
|
|
137
138
|
}
|
|
139
|
+
get def() {
|
|
140
|
+
return this._def;
|
|
141
|
+
}
|
|
138
142
|
setRegistry(registry) {
|
|
139
143
|
this.registry = registry;
|
|
140
144
|
}
|
|
141
145
|
async load(def) {
|
|
142
|
-
this.
|
|
146
|
+
this._def = { nodes: [...def.nodes], edges: [...def.edges] };
|
|
143
147
|
if (this.layout) {
|
|
144
|
-
const { positions } = await this.layout.layout(this.
|
|
148
|
+
const { positions } = await this.layout.layout(this._def);
|
|
145
149
|
this.positions = positions;
|
|
146
150
|
}
|
|
147
|
-
const defNodeIds = new Set(this.
|
|
148
|
-
const defEdgeIds = new Set(this.
|
|
151
|
+
const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
|
|
152
|
+
const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
|
|
149
153
|
const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
|
|
150
154
|
const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
|
|
151
155
|
const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
|
|
152
156
|
this.positions = filteredPositions;
|
|
153
157
|
this.selection = { nodes: filteredNodes, edges: filteredEdges };
|
|
154
|
-
this.emit("graphChanged", { def: this.
|
|
158
|
+
this.emit("graphChanged", { def: this._def });
|
|
155
159
|
this.refreshValidation();
|
|
156
160
|
}
|
|
157
|
-
export() {
|
|
158
|
-
return this.def;
|
|
159
|
-
}
|
|
160
161
|
refreshValidation() {
|
|
161
162
|
this.emit("validationChanged", this.validate());
|
|
162
163
|
}
|
|
163
164
|
validate() {
|
|
164
165
|
if (this.registry) {
|
|
165
166
|
const builder = new sparkGraph.GraphBuilder(this.registry);
|
|
166
|
-
const report = builder.validate(this.
|
|
167
|
+
const report = builder.validate(this._def);
|
|
167
168
|
return report;
|
|
168
169
|
}
|
|
169
170
|
const issues = [];
|
|
170
171
|
const nodeIds = new Set();
|
|
171
|
-
for (const n of this.
|
|
172
|
+
for (const n of this._def.nodes) {
|
|
172
173
|
if (nodeIds.has(n.nodeId)) {
|
|
173
174
|
issues.push({
|
|
174
175
|
level: "error",
|
|
@@ -180,7 +181,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
180
181
|
nodeIds.add(n.nodeId);
|
|
181
182
|
}
|
|
182
183
|
const edgeIds = new Set();
|
|
183
|
-
for (const e of this.
|
|
184
|
+
for (const e of this._def.edges) {
|
|
184
185
|
if (edgeIds.has(e.id)) {
|
|
185
186
|
issues.push({
|
|
186
187
|
level: "error",
|
|
@@ -195,15 +196,15 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
195
196
|
}
|
|
196
197
|
setInputs(nodeId, inputs, options) {
|
|
197
198
|
this.emit("graphChanged", {
|
|
198
|
-
def: this.
|
|
199
|
+
def: this._def,
|
|
199
200
|
change: { type: "setInputs", nodeId, inputs },
|
|
200
201
|
...options,
|
|
201
202
|
});
|
|
202
203
|
}
|
|
203
204
|
addNode(node, options) {
|
|
204
205
|
const id = node.nodeId ??
|
|
205
|
-
this.genId("n", new Set(this.
|
|
206
|
-
this.
|
|
206
|
+
this.genId("n", new Set(this._def.nodes.map((n) => n.nodeId)));
|
|
207
|
+
this._def.nodes.push({
|
|
207
208
|
nodeId: id,
|
|
208
209
|
typeId: node.typeId,
|
|
209
210
|
params: node.params,
|
|
@@ -212,7 +213,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
212
213
|
if (node.position)
|
|
213
214
|
this.positions[id] = node.position;
|
|
214
215
|
this.emit("graphChanged", {
|
|
215
|
-
def: this.
|
|
216
|
+
def: this._def,
|
|
216
217
|
change: {
|
|
217
218
|
type: "addNode",
|
|
218
219
|
nodeId: id,
|
|
@@ -225,26 +226,27 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
225
226
|
return id;
|
|
226
227
|
}
|
|
227
228
|
removeNode(nodeId, options) {
|
|
228
|
-
this.
|
|
229
|
-
this.
|
|
229
|
+
this._def.nodes = this._def.nodes.filter((n) => n.nodeId !== nodeId);
|
|
230
|
+
this._def.edges = this._def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
|
|
230
231
|
delete this.positions[nodeId];
|
|
232
|
+
delete this.nodeNames[nodeId];
|
|
231
233
|
this.emit("graphChanged", {
|
|
232
|
-
def: this.
|
|
234
|
+
def: this._def,
|
|
233
235
|
change: { type: "removeNode", nodeId },
|
|
234
236
|
...options,
|
|
235
237
|
});
|
|
236
238
|
this.refreshValidation();
|
|
237
239
|
}
|
|
238
240
|
connect(edge, options) {
|
|
239
|
-
const id = edge.id ?? this.genId("e", new Set(this.
|
|
240
|
-
this.
|
|
241
|
+
const id = edge.id ?? this.genId("e", new Set(this._def.edges.map((e) => e.id)));
|
|
242
|
+
this._def.edges.push({
|
|
241
243
|
id,
|
|
242
244
|
source: { ...edge.source },
|
|
243
245
|
target: { ...edge.target },
|
|
244
246
|
typeId: edge.typeId,
|
|
245
247
|
});
|
|
246
248
|
this.emit("graphChanged", {
|
|
247
|
-
def: this.
|
|
249
|
+
def: this._def,
|
|
248
250
|
change: { type: "connect", edgeId: id },
|
|
249
251
|
...options,
|
|
250
252
|
});
|
|
@@ -252,16 +254,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
252
254
|
return id;
|
|
253
255
|
}
|
|
254
256
|
disconnect(edgeId, options) {
|
|
255
|
-
this.
|
|
257
|
+
this._def.edges = this._def.edges.filter((e) => e.id !== edgeId);
|
|
256
258
|
this.emit("graphChanged", {
|
|
257
|
-
def: this.
|
|
259
|
+
def: this._def,
|
|
258
260
|
change: { type: "disconnect", edgeId },
|
|
259
261
|
...options,
|
|
260
262
|
});
|
|
261
263
|
this.emit("validationChanged", this.validate());
|
|
262
264
|
}
|
|
263
265
|
updateEdgeType(edgeId, typeId) {
|
|
264
|
-
const e = this.
|
|
266
|
+
const e = this._def.edges.find((x) => x.id === edgeId);
|
|
265
267
|
if (!e)
|
|
266
268
|
return;
|
|
267
269
|
if (!typeId)
|
|
@@ -269,18 +271,18 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
269
271
|
else
|
|
270
272
|
e.typeId = typeId;
|
|
271
273
|
this.emit("graphChanged", {
|
|
272
|
-
def: this.
|
|
274
|
+
def: this._def,
|
|
273
275
|
change: { type: "updateEdgeType", edgeId, typeId },
|
|
274
276
|
});
|
|
275
277
|
this.refreshValidation();
|
|
276
278
|
}
|
|
277
279
|
updateParams(nodeId, params) {
|
|
278
|
-
const n = this.
|
|
280
|
+
const n = this._def.nodes.find((n) => n.nodeId === nodeId);
|
|
279
281
|
if (!n)
|
|
280
282
|
return;
|
|
281
283
|
n.params = { ...(n.params ?? {}), ...params };
|
|
282
284
|
this.emit("graphChanged", {
|
|
283
|
-
def: this.
|
|
285
|
+
def: this._def,
|
|
284
286
|
change: { type: "updateParams", nodeId },
|
|
285
287
|
});
|
|
286
288
|
}
|
|
@@ -288,7 +290,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
288
290
|
setPositions(map, options) {
|
|
289
291
|
this.positions = { ...this.positions, ...map };
|
|
290
292
|
this.emit("graphUiChanged", {
|
|
291
|
-
def: this.
|
|
293
|
+
def: this._def,
|
|
292
294
|
change: { type: "moveNodes" },
|
|
293
295
|
...options,
|
|
294
296
|
});
|
|
@@ -296,17 +298,20 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
296
298
|
getPositions() {
|
|
297
299
|
return { ...this.positions };
|
|
298
300
|
}
|
|
299
|
-
|
|
301
|
+
setSelectionInternal(sel, options) {
|
|
300
302
|
if (lod.isEqual(this.selection, sel))
|
|
301
303
|
return;
|
|
302
304
|
this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
|
|
303
305
|
this.emit("selectionChanged", this.selection);
|
|
304
306
|
this.emit("graphUiChanged", {
|
|
305
|
-
def: this.
|
|
307
|
+
def: this._def,
|
|
306
308
|
change: { type: "selection" },
|
|
307
309
|
...options,
|
|
308
310
|
});
|
|
309
311
|
}
|
|
312
|
+
setSelection(sel, options) {
|
|
313
|
+
this.setSelectionInternal(sel, options);
|
|
314
|
+
}
|
|
310
315
|
getSelection() {
|
|
311
316
|
return {
|
|
312
317
|
nodes: [...this.selection.nodes],
|
|
@@ -327,14 +332,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
327
332
|
this.disconnect(edgeId);
|
|
328
333
|
}
|
|
329
334
|
// Clear selection
|
|
330
|
-
this.
|
|
335
|
+
this.setSelectionInternal({ nodes: [], edges: [] }, options);
|
|
331
336
|
}
|
|
332
337
|
setViewport(viewport) {
|
|
333
338
|
if (lod.isEqual(this.viewport, viewport))
|
|
334
339
|
return;
|
|
335
340
|
this.viewport = { ...viewport };
|
|
336
341
|
this.emit("graphUiChanged", {
|
|
337
|
-
def: this.
|
|
342
|
+
def: this._def,
|
|
338
343
|
change: { type: "viewport" },
|
|
339
344
|
});
|
|
340
345
|
}
|
|
@@ -342,11 +347,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
342
347
|
return this.viewport ? { ...this.viewport } : null;
|
|
343
348
|
}
|
|
344
349
|
getUIState() {
|
|
345
|
-
const defNodeIds = new Set(this.
|
|
346
|
-
const defEdgeIds = new Set(this.
|
|
350
|
+
const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
|
|
351
|
+
const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
|
|
347
352
|
const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
|
|
348
353
|
const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
|
|
349
354
|
const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
|
|
355
|
+
const filteredNodeNames = Object.fromEntries(Object.entries(this.nodeNames).filter(([id]) => defNodeIds.has(id)));
|
|
350
356
|
return {
|
|
351
357
|
positions: Object.keys(filteredPositions).length > 0
|
|
352
358
|
? filteredPositions
|
|
@@ -358,6 +364,9 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
358
364
|
}
|
|
359
365
|
: undefined,
|
|
360
366
|
viewport: this.viewport ? { ...this.viewport } : undefined,
|
|
367
|
+
nodeNames: Object.keys(filteredNodeNames).length > 0
|
|
368
|
+
? filteredNodeNames
|
|
369
|
+
: undefined,
|
|
361
370
|
};
|
|
362
371
|
}
|
|
363
372
|
setUIState(ui) {
|
|
@@ -376,12 +385,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
376
385
|
if (ui.viewport) {
|
|
377
386
|
this.viewport = { ...ui.viewport };
|
|
378
387
|
}
|
|
388
|
+
if (ui.nodeNames !== undefined) {
|
|
389
|
+
this.nodeNames = { ...ui.nodeNames };
|
|
390
|
+
}
|
|
379
391
|
}
|
|
380
392
|
getRuntimeState() {
|
|
381
393
|
return this.runtimeState ? { ...this.runtimeState } : null;
|
|
382
394
|
}
|
|
383
395
|
setRuntimeState(runtime) {
|
|
384
396
|
this.runtimeState = runtime ? { ...runtime } : null;
|
|
397
|
+
this.emit("runtimeMetadataChanged", {});
|
|
385
398
|
}
|
|
386
399
|
getHistory() {
|
|
387
400
|
return this.historyState;
|
|
@@ -398,6 +411,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
398
411
|
const nodeMeta = current.nodes[nodeId] ?? {};
|
|
399
412
|
const updated = updater({ ...nodeMeta });
|
|
400
413
|
this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
|
|
414
|
+
this.emit("runtimeMetadataChanged", { nodeId });
|
|
401
415
|
}
|
|
402
416
|
on(event, handler) {
|
|
403
417
|
if (!this.listeners.has(event))
|
|
@@ -422,11 +436,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
422
436
|
const selection = this.getSelection();
|
|
423
437
|
if (selection.nodes.length === 0)
|
|
424
438
|
return null;
|
|
425
|
-
const def = this.export();
|
|
426
439
|
const positions = this.getPositions();
|
|
427
440
|
const selectedNodeSet = new Set(selection.nodes);
|
|
428
441
|
// Collect nodes to copy
|
|
429
|
-
const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
|
|
442
|
+
const nodesToCopy = this.def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
|
|
430
443
|
if (nodesToCopy.length === 0)
|
|
431
444
|
return null;
|
|
432
445
|
// Calculate bounds
|
|
@@ -450,12 +463,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
450
463
|
const centerY = (bounds.minY + bounds.maxY) / 2;
|
|
451
464
|
// Get inputs for each node
|
|
452
465
|
// Include values from inbound edges if those edges are selected
|
|
453
|
-
const allInputs = runner.getInputs(def);
|
|
466
|
+
const allInputs = runner.getInputs(this.def);
|
|
454
467
|
const selectedEdgeSet = new Set(selection.edges);
|
|
455
468
|
const copiedNodes = nodesToCopy.map((node) => {
|
|
456
469
|
const pos = positions[node.nodeId] || { x: 0, y: 0 };
|
|
457
470
|
// Get all inbound edges for this node
|
|
458
|
-
const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
|
|
471
|
+
const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === node.nodeId);
|
|
459
472
|
// Build set of handles that have inbound edges
|
|
460
473
|
// But only exclude handles whose edges are NOT selected
|
|
461
474
|
const inboundHandlesToExclude = new Set(inboundEdges
|
|
@@ -479,7 +492,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
479
492
|
};
|
|
480
493
|
});
|
|
481
494
|
// Collect edges between copied nodes
|
|
482
|
-
const copiedEdges = def.edges
|
|
495
|
+
const copiedEdges = this.def.edges
|
|
483
496
|
.filter((edge) => {
|
|
484
497
|
return (selectedNodeSet.has(edge.source.nodeId) &&
|
|
485
498
|
selectedNodeSet.has(edge.target.nodeId));
|
|
@@ -497,6 +510,193 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
497
510
|
bounds,
|
|
498
511
|
};
|
|
499
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Duplicate all selected nodes.
|
|
515
|
+
* Returns the list of newly created node IDs.
|
|
516
|
+
* Each duplicated node is offset by 24px in both x and y directions.
|
|
517
|
+
* Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
|
|
518
|
+
*/
|
|
519
|
+
duplicateSelection(runner, options) {
|
|
520
|
+
const selection = this.getSelection();
|
|
521
|
+
if (selection.nodes.length === 0)
|
|
522
|
+
return [];
|
|
523
|
+
const positions = this.getPositions();
|
|
524
|
+
const newNodes = [];
|
|
525
|
+
// Get inputs without bindings (literal values only)
|
|
526
|
+
const allInputs = runner.getInputs(this.def) || {};
|
|
527
|
+
// Duplicate each selected node
|
|
528
|
+
for (const nodeId of selection.nodes) {
|
|
529
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
530
|
+
if (!n)
|
|
531
|
+
continue;
|
|
532
|
+
const pos = positions[nodeId] || { x: 0, y: 0 };
|
|
533
|
+
const inboundHandles = new Set(this.def.edges
|
|
534
|
+
.filter((e) => e.target.nodeId === nodeId)
|
|
535
|
+
.map((e) => e.target.handle));
|
|
536
|
+
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
537
|
+
const newNodeId = this.addNode({
|
|
538
|
+
typeId: n.typeId,
|
|
539
|
+
params: n.params,
|
|
540
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
541
|
+
resolvedHandles: n.resolvedHandles,
|
|
542
|
+
}, {
|
|
543
|
+
inputs: inputsWithoutBindings,
|
|
544
|
+
copyOutputsFrom: nodeId,
|
|
545
|
+
dry: true,
|
|
546
|
+
});
|
|
547
|
+
newNodes.push(newNodeId);
|
|
548
|
+
}
|
|
549
|
+
// Select all newly duplicated nodes
|
|
550
|
+
if (newNodes.length > 0) {
|
|
551
|
+
this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
|
|
552
|
+
}
|
|
553
|
+
return newNodes;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Bake an output value from a node into a new node.
|
|
557
|
+
* Creates a new node based on the output type's bakeTarget configuration.
|
|
558
|
+
* Returns the ID of the last created node (or undefined if none created).
|
|
559
|
+
*/
|
|
560
|
+
async bake(registry, runner, outputValue, outputTypeId, nodePosition, options) {
|
|
561
|
+
try {
|
|
562
|
+
if (!outputTypeId || outputValue === undefined)
|
|
563
|
+
return undefined;
|
|
564
|
+
const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
|
|
565
|
+
const coerceIfNeeded = async (fromType, toType, value) => {
|
|
566
|
+
if (!toType || toType === fromType || !runner?.coerce)
|
|
567
|
+
return value;
|
|
568
|
+
try {
|
|
569
|
+
return await runner.coerce(fromType, toType, value);
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return value;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
const pos = nodePosition;
|
|
576
|
+
const isArray = outputTypeId.endsWith("[]");
|
|
577
|
+
const baseTypeId = isArray ? outputTypeId.slice(0, -2) : outputTypeId;
|
|
578
|
+
const tArr = isArray ? registry.types.get(outputTypeId) : undefined;
|
|
579
|
+
const tElem = registry.types.get(baseTypeId);
|
|
580
|
+
const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
|
|
581
|
+
const arrTarget = isArray ? tArr?.bakeTarget : undefined;
|
|
582
|
+
const elemTarget = isArray ? tElem?.bakeTarget : undefined;
|
|
583
|
+
let newNodeId;
|
|
584
|
+
if (singleTarget) {
|
|
585
|
+
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
586
|
+
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
587
|
+
const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
|
|
588
|
+
newNodeId = this.addNode({
|
|
589
|
+
typeId: singleTarget.nodeTypeId,
|
|
590
|
+
position: { x: pos.x + 180, y: pos.y },
|
|
591
|
+
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
592
|
+
}
|
|
593
|
+
else if (isArray && arrTarget) {
|
|
594
|
+
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
595
|
+
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
596
|
+
const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
|
|
597
|
+
newNodeId = this.addNode({
|
|
598
|
+
typeId: arrTarget.nodeTypeId,
|
|
599
|
+
position: { x: pos.x + 180, y: pos.y },
|
|
600
|
+
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
601
|
+
}
|
|
602
|
+
else if (isArray && elemTarget) {
|
|
603
|
+
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
604
|
+
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
605
|
+
const src = unwrap(outputValue);
|
|
606
|
+
const items = Array.isArray(src) ? src : [src];
|
|
607
|
+
const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
|
|
608
|
+
const COLS = 4;
|
|
609
|
+
const DX = 180;
|
|
610
|
+
const DY = 160;
|
|
611
|
+
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
612
|
+
const col = idx % COLS;
|
|
613
|
+
const row = Math.floor(idx / COLS);
|
|
614
|
+
newNodeId = this.addNode({
|
|
615
|
+
typeId: elemTarget.nodeTypeId,
|
|
616
|
+
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
617
|
+
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (newNodeId) {
|
|
621
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "bake" });
|
|
622
|
+
}
|
|
623
|
+
return newNodeId;
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Duplicate a single node.
|
|
631
|
+
* Returns the ID of the newly created node.
|
|
632
|
+
* The duplicated node is offset by 24px in both x and y directions.
|
|
633
|
+
* Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
|
|
634
|
+
*/
|
|
635
|
+
duplicateNode(nodeId, runner, options) {
|
|
636
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
637
|
+
if (!n)
|
|
638
|
+
return undefined;
|
|
639
|
+
const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
640
|
+
// Get inputs without bindings (literal values only)
|
|
641
|
+
const allInputs = runner.getInputs(this.def)[nodeId] || {};
|
|
642
|
+
const inboundHandles = new Set(this.def.edges
|
|
643
|
+
.filter((e) => e.target.nodeId === nodeId)
|
|
644
|
+
.map((e) => e.target.handle));
|
|
645
|
+
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
646
|
+
const newNodeId = this.addNode({
|
|
647
|
+
typeId: n.typeId,
|
|
648
|
+
params: n.params,
|
|
649
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
650
|
+
resolvedHandles: n.resolvedHandles,
|
|
651
|
+
}, {
|
|
652
|
+
inputs: inputsWithoutBindings,
|
|
653
|
+
copyOutputsFrom: nodeId,
|
|
654
|
+
dry: true,
|
|
655
|
+
});
|
|
656
|
+
// Select the newly duplicated node
|
|
657
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node" });
|
|
658
|
+
return newNodeId;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Duplicate a node and all its incoming edges.
|
|
662
|
+
* Returns the ID of the newly created node.
|
|
663
|
+
* The duplicated node is offset by 24px in both x and y directions.
|
|
664
|
+
* All incoming edges are duplicated to point to the new node.
|
|
665
|
+
*/
|
|
666
|
+
duplicateNodeWithEdges(nodeId, runner, options) {
|
|
667
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
668
|
+
if (!n)
|
|
669
|
+
return undefined;
|
|
670
|
+
const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
671
|
+
// Get all inputs (including those with bindings, since edges will be duplicated)
|
|
672
|
+
const inputs = runner.getInputs(this.def)[nodeId] || {};
|
|
673
|
+
// Add the duplicated node
|
|
674
|
+
const newNodeId = this.addNode({
|
|
675
|
+
typeId: n.typeId,
|
|
676
|
+
params: n.params,
|
|
677
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
678
|
+
resolvedHandles: n.resolvedHandles,
|
|
679
|
+
}, {
|
|
680
|
+
inputs,
|
|
681
|
+
copyOutputsFrom: nodeId,
|
|
682
|
+
dry: true,
|
|
683
|
+
});
|
|
684
|
+
// Find all incoming edges (edges where target is the original node)
|
|
685
|
+
const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
686
|
+
// Duplicate each incoming edge to point to the new node
|
|
687
|
+
for (const edge of incomingEdges) {
|
|
688
|
+
this.connect({
|
|
689
|
+
source: edge.source, // Keep the same source
|
|
690
|
+
target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
|
|
691
|
+
typeId: edge.typeId,
|
|
692
|
+
}, { dry: true });
|
|
693
|
+
}
|
|
694
|
+
// Select the newly duplicated node
|
|
695
|
+
if (newNodeId) {
|
|
696
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node-with-edges" });
|
|
697
|
+
}
|
|
698
|
+
return newNodeId;
|
|
699
|
+
}
|
|
500
700
|
/**
|
|
501
701
|
* Paste copied graph data at the specified center position.
|
|
502
702
|
* Returns the mapping from original node IDs to new node IDs.
|
|
@@ -542,7 +742,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
542
742
|
}
|
|
543
743
|
}
|
|
544
744
|
// Select the newly pasted nodes
|
|
545
|
-
this.
|
|
745
|
+
this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
546
746
|
return { nodeIdMap, edgeIds };
|
|
547
747
|
}
|
|
548
748
|
/**
|
|
@@ -557,6 +757,29 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
557
757
|
setCopiedData(data) {
|
|
558
758
|
this.copiedData = data;
|
|
559
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* Get the custom name for a node, if set.
|
|
762
|
+
*/
|
|
763
|
+
getNodeName(nodeId) {
|
|
764
|
+
return this.nodeNames[nodeId];
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Set a custom name for a node. Empty string or undefined removes the custom name.
|
|
768
|
+
* This is included in undo/redo history via extData.ui.
|
|
769
|
+
*/
|
|
770
|
+
setNodeName(nodeId, name, options) {
|
|
771
|
+
if (name === undefined || name.trim() === "") {
|
|
772
|
+
delete this.nodeNames[nodeId];
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
this.nodeNames[nodeId] = name.trim();
|
|
776
|
+
}
|
|
777
|
+
this.emit("graphUiChanged", {
|
|
778
|
+
def: this._def,
|
|
779
|
+
change: { type: "nodeName", nodeId },
|
|
780
|
+
...options,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
560
783
|
}
|
|
561
784
|
|
|
562
785
|
class CLIWorkbench {
|
|
@@ -567,8 +790,8 @@ class CLIWorkbench {
|
|
|
567
790
|
async load(def) {
|
|
568
791
|
await this.wb.load(def);
|
|
569
792
|
}
|
|
570
|
-
print(
|
|
571
|
-
const d =
|
|
793
|
+
print(options) {
|
|
794
|
+
const d = this.wb.def;
|
|
572
795
|
const detail = !!options?.detail;
|
|
573
796
|
const lines = [];
|
|
574
797
|
lines.push(`Nodes (${d.nodes.length})`);
|
|
@@ -1937,8 +2160,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1937
2160
|
if (!params.sourceHandle || !params.targetHandle)
|
|
1938
2161
|
return;
|
|
1939
2162
|
// Prevent duplicate edges between the same endpoints
|
|
1940
|
-
const
|
|
1941
|
-
const exists = def.edges.some((e) => e.source.nodeId === params.source &&
|
|
2163
|
+
const exists = wb.def.edges.some((e) => e.source.nodeId === params.source &&
|
|
1942
2164
|
e.source.handle === params.sourceHandle &&
|
|
1943
2165
|
e.target.nodeId === params.target &&
|
|
1944
2166
|
e.target.handle === params.targetHandle);
|
|
@@ -1988,7 +2210,10 @@ function useWorkbenchBridge(wb) {
|
|
|
1988
2210
|
}
|
|
1989
2211
|
}
|
|
1990
2212
|
if (selectionChanged) {
|
|
1991
|
-
wb.setSelection({
|
|
2213
|
+
wb.setSelection({
|
|
2214
|
+
nodes: Array.from(nextNodeIds),
|
|
2215
|
+
edges: current.edges,
|
|
2216
|
+
});
|
|
1992
2217
|
}
|
|
1993
2218
|
if (Object.keys(positions).length > 0) {
|
|
1994
2219
|
wb.setPositions(positions, { commit });
|
|
@@ -2023,7 +2248,10 @@ function useWorkbenchBridge(wb) {
|
|
|
2023
2248
|
}
|
|
2024
2249
|
}
|
|
2025
2250
|
if (selectionChanged) {
|
|
2026
|
-
wb.setSelection({
|
|
2251
|
+
wb.setSelection({
|
|
2252
|
+
nodes: current.nodes,
|
|
2253
|
+
edges: Array.from(nextEdgeIds),
|
|
2254
|
+
});
|
|
2027
2255
|
}
|
|
2028
2256
|
}, [wb]);
|
|
2029
2257
|
const onNodesDelete = React.useCallback((nodes) => {
|
|
@@ -2469,7 +2697,6 @@ function isSnapshotPayload(parsed) {
|
|
|
2469
2697
|
}
|
|
2470
2698
|
async function download(wb, runner) {
|
|
2471
2699
|
try {
|
|
2472
|
-
const def = wb.export();
|
|
2473
2700
|
const fullUiState = wb.getUIState();
|
|
2474
2701
|
const uiState = excludeViewportFromUIState(fullUiState);
|
|
2475
2702
|
const runtimeState = wb.getRuntimeState();
|
|
@@ -2478,7 +2705,7 @@ async function download(wb, runner) {
|
|
|
2478
2705
|
const fullSnapshot = await runner.snapshotFull();
|
|
2479
2706
|
snapshot = {
|
|
2480
2707
|
...fullSnapshot,
|
|
2481
|
-
def,
|
|
2708
|
+
def: wb.def,
|
|
2482
2709
|
extData: {
|
|
2483
2710
|
...(fullSnapshot.extData || {}),
|
|
2484
2711
|
ui: Object.keys(uiState || {}).length > 0 ? uiState : undefined,
|
|
@@ -2487,9 +2714,9 @@ async function download(wb, runner) {
|
|
|
2487
2714
|
};
|
|
2488
2715
|
}
|
|
2489
2716
|
else {
|
|
2490
|
-
const inputs = runner.getInputs(def);
|
|
2717
|
+
const inputs = runner.getInputs(wb.def);
|
|
2491
2718
|
snapshot = {
|
|
2492
|
-
def,
|
|
2719
|
+
def: wb.def,
|
|
2493
2720
|
inputs,
|
|
2494
2721
|
outputs: {},
|
|
2495
2722
|
environment: {},
|
|
@@ -2528,7 +2755,7 @@ async function upload(parsed, wb, runner) {
|
|
|
2528
2755
|
}
|
|
2529
2756
|
if (runner.isRunning()) {
|
|
2530
2757
|
await runner.applySnapshotFull({
|
|
2531
|
-
def,
|
|
2758
|
+
def: wb.def,
|
|
2532
2759
|
environment,
|
|
2533
2760
|
inputs,
|
|
2534
2761
|
outputs,
|
|
@@ -2536,7 +2763,7 @@ async function upload(parsed, wb, runner) {
|
|
|
2536
2763
|
});
|
|
2537
2764
|
}
|
|
2538
2765
|
else {
|
|
2539
|
-
runner.build(wb.
|
|
2766
|
+
runner.build(wb.def);
|
|
2540
2767
|
if (inputs && typeof inputs === "object") {
|
|
2541
2768
|
for (const [nodeId, map] of Object.entries(inputs)) {
|
|
2542
2769
|
runner.setInputs(nodeId, map, { dry: true });
|
|
@@ -2632,14 +2859,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2632
2859
|
const versionTick = useWorkbenchVersionTick(runner);
|
|
2633
2860
|
const valuesTick = versionTick + graphTick + graphUiTick;
|
|
2634
2861
|
// Def and IO values
|
|
2635
|
-
const
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
2638
|
-
const outputsMap = React.useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
|
|
2862
|
+
const inputsMap = React.useMemo(() => runner.getInputs(wb.def), [runner, wb, wb.def, valuesTick]);
|
|
2863
|
+
const inputDefaultsMap = React.useMemo(() => runner.getInputDefaults(wb.def), [runner, wb, wb.def, valuesTick]);
|
|
2864
|
+
const outputsMap = React.useMemo(() => runner.getOutputs(wb.def), [runner, wb, wb.def, valuesTick]);
|
|
2639
2865
|
const outputTypesMap = React.useMemo(() => {
|
|
2640
2866
|
const out = {};
|
|
2641
2867
|
// Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
|
|
2642
|
-
for (const n of def.nodes) {
|
|
2868
|
+
for (const n of wb.def.nodes) {
|
|
2643
2869
|
const effectiveHandles = computeEffectiveHandles(n, registry);
|
|
2644
2870
|
const outputsDecl = effectiveHandles.outputs;
|
|
2645
2871
|
const handles = Object.keys(outputsDecl);
|
|
@@ -2654,14 +2880,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2654
2880
|
out[n.nodeId] = cur;
|
|
2655
2881
|
}
|
|
2656
2882
|
return out;
|
|
2657
|
-
}, [def, outputsMap, registry]);
|
|
2883
|
+
}, [wb, wb.def, outputsMap, registry]);
|
|
2658
2884
|
// Initialize nodes and derive invalidated status from persisted metadata
|
|
2659
2885
|
React.useEffect(() => {
|
|
2660
2886
|
const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
|
|
2661
2887
|
setNodeStatus((prev) => {
|
|
2662
2888
|
const next = { ...prev };
|
|
2663
2889
|
const metadata = workbenchRuntimeState;
|
|
2664
|
-
for (const n of def.nodes) {
|
|
2890
|
+
for (const n of wb.def.nodes) {
|
|
2665
2891
|
const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
|
|
2666
2892
|
const nodeMeta = metadata.nodes[n.nodeId];
|
|
2667
2893
|
const updates = {};
|
|
@@ -2683,18 +2909,17 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2683
2909
|
}
|
|
2684
2910
|
return next;
|
|
2685
2911
|
});
|
|
2686
|
-
}, [def, wb]);
|
|
2912
|
+
}, [wb.def, wb]);
|
|
2687
2913
|
// Auto layout (simple layered layout)
|
|
2688
2914
|
const runAutoLayout = React.useCallback(() => {
|
|
2689
|
-
const cur = wb.export();
|
|
2690
2915
|
// Build DAG layers by indegree
|
|
2691
2916
|
const indegree = {};
|
|
2692
2917
|
const adj = {};
|
|
2693
|
-
for (const n of
|
|
2918
|
+
for (const n of wb.def.nodes) {
|
|
2694
2919
|
indegree[n.nodeId] = 0;
|
|
2695
2920
|
adj[n.nodeId] = [];
|
|
2696
2921
|
}
|
|
2697
|
-
for (const e of
|
|
2922
|
+
for (const e of wb.def.edges) {
|
|
2698
2923
|
indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
|
|
2699
2924
|
adj[e.source.nodeId].push(e.target.nodeId);
|
|
2700
2925
|
}
|
|
@@ -2725,7 +2950,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2725
2950
|
let maxWidth = 0;
|
|
2726
2951
|
const heights = {};
|
|
2727
2952
|
for (const id of layer) {
|
|
2728
|
-
const node =
|
|
2953
|
+
const node = wb.def.nodes.find((n) => n.nodeId === id);
|
|
2729
2954
|
if (!node)
|
|
2730
2955
|
continue;
|
|
2731
2956
|
// Prefer showValues sizing similar to node rendering
|
|
@@ -2751,26 +2976,39 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2751
2976
|
curX += maxWidth + H_GAP;
|
|
2752
2977
|
}
|
|
2753
2978
|
wb.setPositions(pos, { commit: true, reason: "auto-layout" });
|
|
2754
|
-
}, [wb, registry, overrides?.getDefaultNodeSize]);
|
|
2979
|
+
}, [wb, wb.def, registry, overrides?.getDefaultNodeSize]);
|
|
2755
2980
|
const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
|
|
2756
2981
|
const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
|
|
2982
|
+
const getNodeDisplayName = React.useCallback((nodeId) => {
|
|
2983
|
+
const customName = wb.getNodeName(nodeId);
|
|
2984
|
+
if (customName)
|
|
2985
|
+
return customName;
|
|
2986
|
+
const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
|
|
2987
|
+
if (!node)
|
|
2988
|
+
return nodeId;
|
|
2989
|
+
const desc = registry.nodes.get(node.typeId);
|
|
2990
|
+
return desc?.displayName || node.typeId;
|
|
2991
|
+
}, [wb, registry]);
|
|
2992
|
+
const setNodeName = React.useCallback((nodeId, name) => {
|
|
2993
|
+
wb.setNodeName(nodeId, name, { commit: true, reason: "rename-node" });
|
|
2994
|
+
}, [wb]);
|
|
2757
2995
|
// Helper to save runtime metadata and UI state to extData
|
|
2758
|
-
const saveUiRuntimeMetadata = React.useCallback(async () => {
|
|
2996
|
+
const saveUiRuntimeMetadata = React.useCallback(async (workbench, graphRunner) => {
|
|
2759
2997
|
try {
|
|
2760
|
-
const current =
|
|
2998
|
+
const current = workbench.getRuntimeState() ?? { nodes: {} };
|
|
2761
2999
|
const metadata = { nodes: { ...current.nodes } };
|
|
2762
3000
|
// Clean up metadata for nodes that no longer exist
|
|
2763
|
-
const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
|
|
3001
|
+
const nodeIds = new Set(workbench.def.nodes.map((n) => n.nodeId));
|
|
2764
3002
|
for (const nodeId of Object.keys(metadata.nodes)) {
|
|
2765
3003
|
if (!nodeIds.has(nodeId)) {
|
|
2766
3004
|
delete metadata.nodes[nodeId];
|
|
2767
3005
|
}
|
|
2768
3006
|
}
|
|
2769
3007
|
// Save cleaned metadata to workbench state
|
|
2770
|
-
|
|
2771
|
-
const fullUiState =
|
|
3008
|
+
workbench.setRuntimeState(metadata);
|
|
3009
|
+
const fullUiState = workbench.getUIState();
|
|
2772
3010
|
const uiWithoutViewport = excludeViewportFromUIState(fullUiState);
|
|
2773
|
-
await
|
|
3011
|
+
await graphRunner.setExtData?.({
|
|
2774
3012
|
...(Object.keys(uiWithoutViewport || {}).length > 0
|
|
2775
3013
|
? { ui: uiWithoutViewport }
|
|
2776
3014
|
: {}),
|
|
@@ -2780,7 +3018,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2780
3018
|
catch (err) {
|
|
2781
3019
|
console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
|
|
2782
3020
|
}
|
|
2783
|
-
}, [
|
|
3021
|
+
}, []);
|
|
2784
3022
|
// Subscribe to runner/workbench events
|
|
2785
3023
|
React.useEffect(() => {
|
|
2786
3024
|
const add = (source, type) => (payload) => setEvents((prev) => {
|
|
@@ -2806,9 +3044,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2806
3044
|
});
|
|
2807
3045
|
// Helper to apply resolved handles from event payload to workbench
|
|
2808
3046
|
const applyResolvedHandles = (resolvedHandles) => {
|
|
2809
|
-
const cur = wb.export();
|
|
2810
3047
|
let changed = false;
|
|
2811
|
-
for (const n of
|
|
3048
|
+
for (const n of wb.def.nodes) {
|
|
2812
3049
|
const updated = resolvedHandles[n.nodeId];
|
|
2813
3050
|
if (updated) {
|
|
2814
3051
|
const before = JSON.stringify(n.resolvedHandles || {});
|
|
@@ -3097,6 +3334,9 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3097
3334
|
else if (changeType === "updateEdgeType") {
|
|
3098
3335
|
reason = "update-edge-type";
|
|
3099
3336
|
}
|
|
3337
|
+
else if (changeType === "setInputs") {
|
|
3338
|
+
reason = "set-inputs";
|
|
3339
|
+
}
|
|
3100
3340
|
}
|
|
3101
3341
|
if (event.change?.type === "setInputs") {
|
|
3102
3342
|
const { nodeId, inputs } = event.change;
|
|
@@ -3104,7 +3344,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3104
3344
|
}
|
|
3105
3345
|
if (!runner.isRunning()) {
|
|
3106
3346
|
if (event.commit) {
|
|
3107
|
-
await saveUiRuntimeMetadata();
|
|
3347
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3108
3348
|
const history = await runner.commit(reason).catch((err) => {
|
|
3109
3349
|
console.error("[WorkbenchContext] Error committing:", err);
|
|
3110
3350
|
return undefined;
|
|
@@ -3137,7 +3377,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3137
3377
|
await runner.update(event.def, { dry: event.dry });
|
|
3138
3378
|
}
|
|
3139
3379
|
if (event.commit) {
|
|
3140
|
-
await saveUiRuntimeMetadata();
|
|
3380
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3141
3381
|
const history = await runner
|
|
3142
3382
|
.commit(event.reason ?? reason)
|
|
3143
3383
|
.catch((err) => {
|
|
@@ -3158,7 +3398,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3158
3398
|
setSelectedNodeId(sel.nodes?.[0]);
|
|
3159
3399
|
setSelectedEdgeId(sel.edges?.[0]);
|
|
3160
3400
|
if (sel.commit) {
|
|
3161
|
-
await saveUiRuntimeMetadata();
|
|
3401
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3162
3402
|
const history = await runner
|
|
3163
3403
|
.commit(sel.reason ?? "selection")
|
|
3164
3404
|
.catch((err) => {
|
|
@@ -3197,7 +3437,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3197
3437
|
reason = "selection";
|
|
3198
3438
|
}
|
|
3199
3439
|
}
|
|
3200
|
-
await saveUiRuntimeMetadata();
|
|
3440
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3201
3441
|
const history = await runner
|
|
3202
3442
|
.commit(event.reason ?? reason)
|
|
3203
3443
|
.catch((err) => {
|
|
@@ -3217,7 +3457,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3217
3457
|
wb.setRegistry(newReg);
|
|
3218
3458
|
// Trigger a graph update so the UI revalidates with new types/enums/nodes
|
|
3219
3459
|
try {
|
|
3220
|
-
await runner.update(wb.
|
|
3460
|
+
await runner.update(wb.def);
|
|
3221
3461
|
}
|
|
3222
3462
|
catch {
|
|
3223
3463
|
console.error("Failed to update graph definition after registry changed");
|
|
@@ -3240,7 +3480,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3240
3480
|
setNodeStatus(() => {
|
|
3241
3481
|
const next = {};
|
|
3242
3482
|
const metadata = wb.getRuntimeState() ?? { nodes: {} };
|
|
3243
|
-
for (const n of def.nodes) {
|
|
3483
|
+
for (const n of wb.def.nodes) {
|
|
3244
3484
|
const nodeMeta = metadata.nodes[n.nodeId];
|
|
3245
3485
|
next[n.nodeId] = {
|
|
3246
3486
|
activeRuns: 0,
|
|
@@ -3255,6 +3495,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3255
3495
|
errorRunsRef.current = {};
|
|
3256
3496
|
}
|
|
3257
3497
|
});
|
|
3498
|
+
const offWbRuntimeMetadataChanged = wb.on("runtimeMetadataChanged", (event) => {
|
|
3499
|
+
const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
|
|
3500
|
+
setNodeStatus((prev) => {
|
|
3501
|
+
const next = { ...prev };
|
|
3502
|
+
const metadata = workbenchRuntimeState;
|
|
3503
|
+
let changed = false;
|
|
3504
|
+
// If nodeId is specified, only update that node; otherwise update all nodes
|
|
3505
|
+
const nodesToUpdate = event.nodeId
|
|
3506
|
+
? wb.def.nodes.filter((n) => n.nodeId === event.nodeId)
|
|
3507
|
+
: wb.def.nodes;
|
|
3508
|
+
for (const n of nodesToUpdate) {
|
|
3509
|
+
const cur = next[n.nodeId];
|
|
3510
|
+
if (!cur)
|
|
3511
|
+
continue;
|
|
3512
|
+
const nodeMeta = metadata.nodes[n.nodeId];
|
|
3513
|
+
const newInvalidated = computeInvalidatedFromMetadata(nodeMeta);
|
|
3514
|
+
if (cur.invalidated !== newInvalidated) {
|
|
3515
|
+
next[n.nodeId] = { ...cur, invalidated: newInvalidated };
|
|
3516
|
+
changed = true;
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
return changed ? next : prev;
|
|
3520
|
+
});
|
|
3521
|
+
});
|
|
3258
3522
|
wb.refreshValidation();
|
|
3259
3523
|
return () => {
|
|
3260
3524
|
offRunnerValue();
|
|
@@ -3272,13 +3536,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3272
3536
|
offRunnerRegistry();
|
|
3273
3537
|
offRunnerTransport();
|
|
3274
3538
|
offFlowViewport();
|
|
3539
|
+
offWbRuntimeMetadataChanged();
|
|
3275
3540
|
};
|
|
3276
3541
|
}, [runner, wb, setRegistry]);
|
|
3277
3542
|
const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
|
|
3278
3543
|
const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
|
|
3279
3544
|
const start = React.useCallback((engine) => {
|
|
3280
3545
|
try {
|
|
3281
|
-
runner.launch(wb.
|
|
3546
|
+
runner.launch(wb.def, { engine });
|
|
3282
3547
|
}
|
|
3283
3548
|
catch { }
|
|
3284
3549
|
}, [runner, wb]);
|
|
@@ -3353,7 +3618,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3353
3618
|
runner,
|
|
3354
3619
|
registry,
|
|
3355
3620
|
setRegistry,
|
|
3356
|
-
def,
|
|
3357
3621
|
selectedNodeId,
|
|
3358
3622
|
selectedEdgeId,
|
|
3359
3623
|
setSelection,
|
|
@@ -3389,12 +3653,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3389
3653
|
triggerExternal,
|
|
3390
3654
|
uiVersion,
|
|
3391
3655
|
overrides,
|
|
3656
|
+
getNodeDisplayName,
|
|
3657
|
+
setNodeName,
|
|
3392
3658
|
}), [
|
|
3393
3659
|
wb,
|
|
3394
3660
|
runner,
|
|
3395
3661
|
registry,
|
|
3396
3662
|
setRegistry,
|
|
3397
|
-
def,
|
|
3398
3663
|
selectedNodeId,
|
|
3399
3664
|
selectedEdgeId,
|
|
3400
3665
|
setSelection,
|
|
@@ -3429,142 +3694,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3429
3694
|
runner,
|
|
3430
3695
|
uiVersion,
|
|
3431
3696
|
overrides,
|
|
3697
|
+
getNodeDisplayName,
|
|
3698
|
+
setNodeName,
|
|
3432
3699
|
]);
|
|
3433
3700
|
return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
|
|
3434
3701
|
}
|
|
3435
3702
|
|
|
3436
3703
|
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
3704
|
return {
|
|
3506
3705
|
onDelete: () => {
|
|
3507
3706
|
wb.removeNode(nodeId, { commit: true });
|
|
3508
3707
|
onClose();
|
|
3509
3708
|
},
|
|
3510
3709
|
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,
|
|
3710
|
+
wb.duplicateNode(nodeId, runner, {
|
|
3711
|
+
commit: true,
|
|
3712
|
+
reason: "duplicate-node",
|
|
3530
3713
|
});
|
|
3531
|
-
// Select the newly duplicated node
|
|
3532
|
-
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
|
|
3533
3714
|
onClose();
|
|
3534
3715
|
},
|
|
3535
3716
|
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,
|
|
3717
|
+
wb.duplicateNodeWithEdges(nodeId, runner, {
|
|
3718
|
+
commit: true,
|
|
3719
|
+
reason: "duplicate-node-with-edges",
|
|
3553
3720
|
});
|
|
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
3721
|
onClose();
|
|
3569
3722
|
},
|
|
3570
3723
|
onRunPull: async () => {
|
|
@@ -3575,7 +3728,13 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3575
3728
|
onClose();
|
|
3576
3729
|
},
|
|
3577
3730
|
onBake: async (handleId) => {
|
|
3578
|
-
|
|
3731
|
+
const nodePosition = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3732
|
+
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
3733
|
+
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
3734
|
+
await wb.bake(registry, runner, raw, typeId || "", nodePosition, {
|
|
3735
|
+
commit: true,
|
|
3736
|
+
reason: "bake",
|
|
3737
|
+
});
|
|
3579
3738
|
onClose();
|
|
3580
3739
|
},
|
|
3581
3740
|
onCopy: () => {
|
|
@@ -3632,42 +3791,10 @@ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyRes
|
|
|
3632
3791
|
function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
|
|
3633
3792
|
const onDuplicate = runner
|
|
3634
3793
|
? () => {
|
|
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
|
-
}
|
|
3794
|
+
wb.duplicateSelection(runner, {
|
|
3795
|
+
commit: true,
|
|
3796
|
+
reason: "duplicate-selection",
|
|
3797
|
+
});
|
|
3671
3798
|
onClose();
|
|
3672
3799
|
}
|
|
3673
3800
|
: undefined;
|
|
@@ -3702,9 +3829,8 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
|
|
|
3702
3829
|
const canRedo = history ? history.redoCount > 0 : undefined;
|
|
3703
3830
|
const onSelectAll = wb
|
|
3704
3831
|
? () => {
|
|
3705
|
-
const
|
|
3706
|
-
const
|
|
3707
|
-
const allEdgeIds = def.edges.map((e) => e.id);
|
|
3832
|
+
const allNodeIds = wb.def.nodes.map((n) => n.nodeId);
|
|
3833
|
+
const allEdgeIds = wb.def.edges.map((e) => e.id);
|
|
3708
3834
|
wb.setSelection({ nodes: allNodeIds, edges: allEdgeIds }, { commit: true, reason: "select-all" });
|
|
3709
3835
|
onClose();
|
|
3710
3836
|
}
|
|
@@ -3723,8 +3849,7 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
|
|
|
3723
3849
|
}
|
|
3724
3850
|
function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
|
|
3725
3851
|
try {
|
|
3726
|
-
const
|
|
3727
|
-
const node = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3852
|
+
const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
|
|
3728
3853
|
if (!node)
|
|
3729
3854
|
return [];
|
|
3730
3855
|
const desc = registry.nodes.get(node.typeId);
|
|
@@ -3823,13 +3948,13 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3823
3948
|
return String(value ?? "");
|
|
3824
3949
|
}
|
|
3825
3950
|
};
|
|
3826
|
-
const {
|
|
3951
|
+
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
3952
|
const nodeValidationIssues = validationByNode.issues;
|
|
3828
3953
|
const edgeValidationIssues = validationByEdge.issues;
|
|
3829
3954
|
const nodeValidationHandles = validationByNode;
|
|
3830
3955
|
const globalValidationIssues = validationGlobal;
|
|
3831
|
-
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
3832
|
-
const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
|
|
3956
|
+
const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
3957
|
+
const selectedEdge = wb.def.edges.find((e) => e.id === selectedEdgeId);
|
|
3833
3958
|
// Use computeEffectiveHandles to merge registry defaults with dynamically resolved handles
|
|
3834
3959
|
const effectiveHandles = selectedNode
|
|
3835
3960
|
? computeEffectiveHandles(selectedNode, registry)
|
|
@@ -3961,7 +4086,6 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3961
4086
|
setOriginals(nextOriginals);
|
|
3962
4087
|
}, [selectedNodeId, selectedNode, registry, valuesTick]);
|
|
3963
4088
|
const widthClass = debug ? "w-[480px]" : "w-[320px]";
|
|
3964
|
-
const { wb } = useWorkbenchContext();
|
|
3965
4089
|
const deleteEdgeById = (edgeId) => {
|
|
3966
4090
|
if (!edgeId)
|
|
3967
4091
|
return;
|
|
@@ -3988,9 +4112,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3988
4112
|
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
4113
|
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
4114
|
const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
|
|
3991
|
-
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
4115
|
+
const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
3992
4116
|
e.target.handle === h);
|
|
3993
|
-
const inbound = new Set(def.edges
|
|
4117
|
+
const inbound = new Set(wb.def.edges
|
|
3994
4118
|
.filter((e) => e.target.nodeId === selectedNodeId &&
|
|
3995
4119
|
e.target.handle === h)
|
|
3996
4120
|
.map((e) => e.target.handle));
|
|
@@ -4160,11 +4284,27 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
4160
4284
|
position: "relative",
|
|
4161
4285
|
minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
|
|
4162
4286
|
minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
|
|
4163
|
-
}, children: [jsxRuntime.jsx(DefaultNodeHeader, { id: id,
|
|
4287
|
+
}, children: [jsxRuntime.jsx(DefaultNodeHeader, { id: id, typeId: typeId, validation: validation, showId: data.showValues }), jsxRuntime.jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
|
|
4164
4288
|
});
|
|
4165
4289
|
DefaultNode.displayName = "DefaultNode";
|
|
4166
|
-
function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate, }) {
|
|
4290
|
+
function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInvalidate, }) {
|
|
4167
4291
|
const ctx = useWorkbenchContext();
|
|
4292
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
4293
|
+
const [editValue, setEditValue] = React.useState("");
|
|
4294
|
+
const inputRef = React.useRef(null);
|
|
4295
|
+
// Use getNodeDisplayName if typeId is provided, otherwise use title prop
|
|
4296
|
+
const displayName = typeId ? ctx.getNodeDisplayName(id) : (title ?? id);
|
|
4297
|
+
const effectiveTypeId = typeId ?? title ?? id;
|
|
4298
|
+
// Get the default display name (without custom name) for comparison
|
|
4299
|
+
const getDefaultDisplayName = React.useCallback(() => {
|
|
4300
|
+
if (!typeId)
|
|
4301
|
+
return title ?? id;
|
|
4302
|
+
const node = ctx.wb.def.nodes.find((n) => n.nodeId === id);
|
|
4303
|
+
if (!node)
|
|
4304
|
+
return id;
|
|
4305
|
+
const desc = ctx.registry.nodes.get(node.typeId);
|
|
4306
|
+
return desc?.displayName || node.typeId;
|
|
4307
|
+
}, [ctx, id, typeId, title]);
|
|
4168
4308
|
const handleInvalidate = React.useCallback(() => {
|
|
4169
4309
|
try {
|
|
4170
4310
|
if (onInvalidate)
|
|
@@ -4177,10 +4317,51 @@ function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate,
|
|
|
4177
4317
|
}
|
|
4178
4318
|
catch { }
|
|
4179
4319
|
}, [ctx, id, onInvalidate]);
|
|
4320
|
+
const handleDoubleClick = React.useCallback((e) => {
|
|
4321
|
+
// Only allow editing if typeId is provided (enables renaming)
|
|
4322
|
+
if (!typeId)
|
|
4323
|
+
return;
|
|
4324
|
+
e.stopPropagation();
|
|
4325
|
+
setIsEditing(true);
|
|
4326
|
+
setEditValue(displayName);
|
|
4327
|
+
}, [typeId, displayName]);
|
|
4328
|
+
const handleSave = React.useCallback(() => {
|
|
4329
|
+
if (!typeId)
|
|
4330
|
+
return;
|
|
4331
|
+
const trimmed = editValue.trim();
|
|
4332
|
+
const defaultDisplayName = getDefaultDisplayName();
|
|
4333
|
+
// If the trimmed value matches the default display name or typeId, clear the custom name
|
|
4334
|
+
ctx.setNodeName(id, trimmed === defaultDisplayName || trimmed === effectiveTypeId
|
|
4335
|
+
? undefined
|
|
4336
|
+
: trimmed);
|
|
4337
|
+
setIsEditing(false);
|
|
4338
|
+
}, [ctx, id, editValue, getDefaultDisplayName, effectiveTypeId, typeId]);
|
|
4339
|
+
const handleCancel = React.useCallback(() => {
|
|
4340
|
+
setIsEditing(false);
|
|
4341
|
+
setEditValue(displayName);
|
|
4342
|
+
}, [displayName]);
|
|
4343
|
+
const handleKeyDown = React.useCallback((e) => {
|
|
4344
|
+
if (e.key === "Enter") {
|
|
4345
|
+
e.preventDefault();
|
|
4346
|
+
e.stopPropagation();
|
|
4347
|
+
handleSave();
|
|
4348
|
+
}
|
|
4349
|
+
else if (e.key === "Escape") {
|
|
4350
|
+
e.preventDefault();
|
|
4351
|
+
e.stopPropagation();
|
|
4352
|
+
handleCancel();
|
|
4353
|
+
}
|
|
4354
|
+
}, [handleSave, handleCancel]);
|
|
4355
|
+
React.useEffect(() => {
|
|
4356
|
+
if (isEditing && inputRef.current) {
|
|
4357
|
+
inputRef.current.focus();
|
|
4358
|
+
inputRef.current.select();
|
|
4359
|
+
}
|
|
4360
|
+
}, [isEditing]);
|
|
4180
4361
|
return (jsxRuntime.jsxs("div", { className: "flex items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", style: {
|
|
4181
4362
|
maxHeight: NODE_HEADER_HEIGHT_PX,
|
|
4182
4363
|
minHeight: NODE_HEADER_HEIGHT_PX,
|
|
4183
|
-
}, children: [jsxRuntime.jsx("
|
|
4364
|
+
}, children: [isEditing ? (jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: editValue, onChange: (e) => setEditValue(e.target.value), onBlur: handleSave, onKeyDown: handleKeyDown, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), className: "flex-1 h-full text-sm bg-transparent border border-blue-500 rounded px-1 outline-none wb-nodrag", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` } })) : (jsxRuntime.jsx("strong", { className: `flex-1 h-full text-sm select-none truncate ${typeId ? "cursor-text" : ""}`, style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, onDoubleClick: handleDoubleClick, title: typeId ? "Double-click to rename" : undefined, children: displayName })), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("button", { className: "w-4 h-4 border border-gray-400 rounded text-[10px] leading-3 flex items-center justify-center", title: "Invalidate and re-run", onClick: (e) => {
|
|
4184
4365
|
e.stopPropagation();
|
|
4185
4366
|
handleInvalidate();
|
|
4186
4367
|
}, children: jsxRuntime.jsx(react$1.ArrowClockwiseIcon, { size: 10 }) }), right, validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
|
|
@@ -4566,10 +4747,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4566
4747
|
const { nodeTypes, resolveNodeType } = React.useMemo(() => {
|
|
4567
4748
|
// Build nodeTypes map using UI extension registry
|
|
4568
4749
|
const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
|
|
4569
|
-
const def = wb.export();
|
|
4570
4750
|
const ids = new Set([
|
|
4571
4751
|
...Array.from(registry.nodes.keys()),
|
|
4572
|
-
...def.nodes.map((n) => n.typeId),
|
|
4752
|
+
...wb.def.nodes.map((n) => n.typeId),
|
|
4573
4753
|
]);
|
|
4574
4754
|
for (const typeId of ids) {
|
|
4575
4755
|
const renderer = ui.getNodeRenderer(typeId);
|
|
@@ -4588,14 +4768,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4588
4768
|
// Include uiVersion to recompute when custom renderers are registered
|
|
4589
4769
|
}, [wb, registry, uiVersion, ui]);
|
|
4590
4770
|
const { nodes, edges } = React.useMemo(() => {
|
|
4591
|
-
const def = wb.export();
|
|
4592
4771
|
const sel = wb.getSelection();
|
|
4593
4772
|
// Merge defaults with inputs for node display (defaults shown in lighter gray)
|
|
4594
4773
|
const inputsWithDefaults = {};
|
|
4595
|
-
for (const n of def.nodes) {
|
|
4774
|
+
for (const n of wb.def.nodes) {
|
|
4596
4775
|
const nodeInputs = inputsMap[n.nodeId] ?? {};
|
|
4597
4776
|
const nodeDefaults = inputDefaultsMap[n.nodeId] ?? {};
|
|
4598
|
-
const inbound = new Set(def.edges
|
|
4777
|
+
const inbound = new Set(wb.def.edges
|
|
4599
4778
|
.filter((e) => e.target.nodeId === n.nodeId)
|
|
4600
4779
|
.map((e) => e.target.handle));
|
|
4601
4780
|
const merged = { ...nodeInputs };
|
|
@@ -4608,7 +4787,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4608
4787
|
inputsWithDefaults[n.nodeId] = merged;
|
|
4609
4788
|
}
|
|
4610
4789
|
}
|
|
4611
|
-
const out = toReactFlow(def, wb.getPositions(), registry, {
|
|
4790
|
+
const out = toReactFlow(wb.def, wb.getPositions(), registry, {
|
|
4612
4791
|
showValues,
|
|
4613
4792
|
inputs: inputsWithDefaults,
|
|
4614
4793
|
inputDefaults: inputDefaultsMap,
|
|
@@ -5132,11 +5311,11 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
5132
5311
|
});
|
|
5133
5312
|
|
|
5134
5313
|
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,
|
|
5314
|
+
const { wb, runner, registry, selectedNodeId, runAutoLayout } = useWorkbenchContext();
|
|
5136
5315
|
const [transportStatus, setTransportStatus] = React.useState({
|
|
5137
5316
|
state: "local",
|
|
5138
5317
|
});
|
|
5139
|
-
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
5318
|
+
const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
5140
5319
|
const effectiveHandles = selectedNode
|
|
5141
5320
|
? computeEffectiveHandles(selectedNode, registry)
|
|
5142
5321
|
: { inputs: {}, outputs: {}, inputDefaults: {} };
|
|
@@ -5164,7 +5343,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5164
5343
|
if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
|
|
5165
5344
|
return;
|
|
5166
5345
|
try {
|
|
5167
|
-
runner.launch(wb.
|
|
5346
|
+
runner.launch(wb.def, {
|
|
5168
5347
|
engine: kind,
|
|
5169
5348
|
invalidate: evt.shiftKey,
|
|
5170
5349
|
});
|
|
@@ -5232,7 +5411,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5232
5411
|
const setInitialGraph = async (d, inputs) => {
|
|
5233
5412
|
await wb.load(d);
|
|
5234
5413
|
try {
|
|
5235
|
-
runner.build(wb.
|
|
5414
|
+
runner.build(wb.def);
|
|
5236
5415
|
}
|
|
5237
5416
|
catch { }
|
|
5238
5417
|
if (inputs) {
|
|
@@ -5247,36 +5426,27 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5247
5426
|
React.useEffect(() => {
|
|
5248
5427
|
if (!onChange)
|
|
5249
5428
|
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", () => {
|
|
5429
|
+
const offGraphChanged = wb.on("graphChanged", () => {
|
|
5259
5430
|
try {
|
|
5260
|
-
const cur = wb.
|
|
5431
|
+
const cur = wb.def;
|
|
5261
5432
|
const inputs = runner.getInputs(cur);
|
|
5262
5433
|
onChange({ def: cur, inputs });
|
|
5263
5434
|
}
|
|
5264
5435
|
catch { }
|
|
5265
5436
|
});
|
|
5266
|
-
const
|
|
5437
|
+
const offGraphUiChanged = wb.on("graphUiChanged", (evt) => {
|
|
5267
5438
|
if (!evt.commit)
|
|
5268
5439
|
return;
|
|
5269
5440
|
try {
|
|
5270
|
-
const cur = wb.
|
|
5441
|
+
const cur = wb.def;
|
|
5271
5442
|
const inputs = runner.getInputs(cur);
|
|
5272
5443
|
onChange({ def: cur, inputs });
|
|
5273
5444
|
}
|
|
5274
5445
|
catch { }
|
|
5275
5446
|
});
|
|
5276
5447
|
return () => {
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
off3();
|
|
5448
|
+
offGraphChanged();
|
|
5449
|
+
offGraphUiChanged();
|
|
5280
5450
|
};
|
|
5281
5451
|
}, [wb, runner, onChange]);
|
|
5282
5452
|
const applyExample = React.useCallback(async (key) => {
|
|
@@ -5299,7 +5469,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5299
5469
|
}
|
|
5300
5470
|
await wb.load(def);
|
|
5301
5471
|
// Build a local runtime so seeded defaults are visible pre-run
|
|
5302
|
-
runner.build(wb.
|
|
5472
|
+
runner.build(wb.def);
|
|
5303
5473
|
// Set initial inputs if provided
|
|
5304
5474
|
if (inputs) {
|
|
5305
5475
|
for (const [nodeId, map] of Object.entries(inputs)) {
|
|
@@ -5384,11 +5554,10 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5384
5554
|
// Only auto-launch for local backend; require explicit Start for remote
|
|
5385
5555
|
if (backendKind !== "local")
|
|
5386
5556
|
return;
|
|
5387
|
-
|
|
5388
|
-
if (!d.nodes || d.nodes.length === 0)
|
|
5557
|
+
if (!wb.def.nodes || wb.def.nodes.length === 0)
|
|
5389
5558
|
return;
|
|
5390
5559
|
try {
|
|
5391
|
-
runner.launch(
|
|
5560
|
+
runner.launch(wb.def, { engine: engine });
|
|
5392
5561
|
}
|
|
5393
5562
|
catch {
|
|
5394
5563
|
// ignore
|
|
@@ -5398,7 +5567,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5398
5567
|
if (!selectedNodeId)
|
|
5399
5568
|
return;
|
|
5400
5569
|
// 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);
|
|
5570
|
+
const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
|
|
5402
5571
|
if (isLinked)
|
|
5403
5572
|
return;
|
|
5404
5573
|
// If raw is undefined, pass it through to delete the input value
|
|
@@ -5483,7 +5652,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5483
5652
|
}
|
|
5484
5653
|
}
|
|
5485
5654
|
wb.setInputs(selectedNodeId, { [handle]: value }, { commit: true });
|
|
5486
|
-
}, [selectedNodeId, def.edges, effectiveHandles, wb]);
|
|
5655
|
+
}, [selectedNodeId, wb.def.edges, effectiveHandles, wb]);
|
|
5487
5656
|
const setInput = React.useMemo(() => {
|
|
5488
5657
|
if (overrides?.setInput) {
|
|
5489
5658
|
return overrides.setInput(baseSetInput, {
|