@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/esm/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateId, GraphBuilder, createEngine, StepEngine, PullEngine, BatchedEngine,
|
|
1
|
+
import { generateId, GraphBuilder, getTypedOutputValue, isTypedOutput, getInputTypeId, createEngine, StepEngine, PullEngine, BatchedEngine, getTypedOutputTypeId, isInputPrivate, createSimpleGraphRegistry, createSimpleGraphDef, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createValidationGraphDef, createValidationGraphRegistry } from '@bian-womp/spark-graph';
|
|
2
2
|
import lod from 'lodash';
|
|
3
3
|
import { RuntimeApiClient } from '@bian-womp/spark-remote';
|
|
4
4
|
import { Position, Handle, useUpdateNodeInternals, useReactFlow, ReactFlowProvider, ReactFlow, Background, BackgroundVariant, MiniMap, Controls } from '@xyflow/react';
|
|
@@ -121,52 +121,53 @@ class AbstractWorkbench {
|
|
|
121
121
|
class InMemoryWorkbench extends AbstractWorkbench {
|
|
122
122
|
constructor() {
|
|
123
123
|
super(...arguments);
|
|
124
|
-
this.
|
|
124
|
+
this._def = { nodes: [], edges: [] };
|
|
125
125
|
this.listeners = new Map();
|
|
126
126
|
this.positions = {};
|
|
127
127
|
this.selection = {
|
|
128
128
|
nodes: [],
|
|
129
129
|
edges: [],
|
|
130
130
|
};
|
|
131
|
-
this.
|
|
131
|
+
this.nodeNames = {};
|
|
132
132
|
this.runtimeState = null;
|
|
133
|
+
this.viewport = null;
|
|
133
134
|
this.historyState = undefined;
|
|
134
135
|
this.copiedData = null;
|
|
135
136
|
}
|
|
137
|
+
get def() {
|
|
138
|
+
return this._def;
|
|
139
|
+
}
|
|
136
140
|
setRegistry(registry) {
|
|
137
141
|
this.registry = registry;
|
|
138
142
|
}
|
|
139
143
|
async load(def) {
|
|
140
|
-
this.
|
|
144
|
+
this._def = { nodes: [...def.nodes], edges: [...def.edges] };
|
|
141
145
|
if (this.layout) {
|
|
142
|
-
const { positions } = await this.layout.layout(this.
|
|
146
|
+
const { positions } = await this.layout.layout(this._def);
|
|
143
147
|
this.positions = positions;
|
|
144
148
|
}
|
|
145
|
-
const defNodeIds = new Set(this.
|
|
146
|
-
const defEdgeIds = new Set(this.
|
|
149
|
+
const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
|
|
150
|
+
const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
|
|
147
151
|
const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
|
|
148
152
|
const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
|
|
149
153
|
const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
|
|
150
154
|
this.positions = filteredPositions;
|
|
151
155
|
this.selection = { nodes: filteredNodes, edges: filteredEdges };
|
|
152
|
-
this.emit("graphChanged", { def: this.
|
|
156
|
+
this.emit("graphChanged", { def: this._def });
|
|
153
157
|
this.refreshValidation();
|
|
154
158
|
}
|
|
155
|
-
export() {
|
|
156
|
-
return this.def;
|
|
157
|
-
}
|
|
158
159
|
refreshValidation() {
|
|
159
160
|
this.emit("validationChanged", this.validate());
|
|
160
161
|
}
|
|
161
162
|
validate() {
|
|
162
163
|
if (this.registry) {
|
|
163
164
|
const builder = new GraphBuilder(this.registry);
|
|
164
|
-
const report = builder.validate(this.
|
|
165
|
+
const report = builder.validate(this._def);
|
|
165
166
|
return report;
|
|
166
167
|
}
|
|
167
168
|
const issues = [];
|
|
168
169
|
const nodeIds = new Set();
|
|
169
|
-
for (const n of this.
|
|
170
|
+
for (const n of this._def.nodes) {
|
|
170
171
|
if (nodeIds.has(n.nodeId)) {
|
|
171
172
|
issues.push({
|
|
172
173
|
level: "error",
|
|
@@ -178,7 +179,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
178
179
|
nodeIds.add(n.nodeId);
|
|
179
180
|
}
|
|
180
181
|
const edgeIds = new Set();
|
|
181
|
-
for (const e of this.
|
|
182
|
+
for (const e of this._def.edges) {
|
|
182
183
|
if (edgeIds.has(e.id)) {
|
|
183
184
|
issues.push({
|
|
184
185
|
level: "error",
|
|
@@ -193,15 +194,15 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
193
194
|
}
|
|
194
195
|
setInputs(nodeId, inputs, options) {
|
|
195
196
|
this.emit("graphChanged", {
|
|
196
|
-
def: this.
|
|
197
|
+
def: this._def,
|
|
197
198
|
change: { type: "setInputs", nodeId, inputs },
|
|
198
199
|
...options,
|
|
199
200
|
});
|
|
200
201
|
}
|
|
201
202
|
addNode(node, options) {
|
|
202
203
|
const id = node.nodeId ??
|
|
203
|
-
this.genId("n", new Set(this.
|
|
204
|
-
this.
|
|
204
|
+
this.genId("n", new Set(this._def.nodes.map((n) => n.nodeId)));
|
|
205
|
+
this._def.nodes.push({
|
|
205
206
|
nodeId: id,
|
|
206
207
|
typeId: node.typeId,
|
|
207
208
|
params: node.params,
|
|
@@ -210,7 +211,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
210
211
|
if (node.position)
|
|
211
212
|
this.positions[id] = node.position;
|
|
212
213
|
this.emit("graphChanged", {
|
|
213
|
-
def: this.
|
|
214
|
+
def: this._def,
|
|
214
215
|
change: {
|
|
215
216
|
type: "addNode",
|
|
216
217
|
nodeId: id,
|
|
@@ -223,26 +224,27 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
223
224
|
return id;
|
|
224
225
|
}
|
|
225
226
|
removeNode(nodeId, options) {
|
|
226
|
-
this.
|
|
227
|
-
this.
|
|
227
|
+
this._def.nodes = this._def.nodes.filter((n) => n.nodeId !== nodeId);
|
|
228
|
+
this._def.edges = this._def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
|
|
228
229
|
delete this.positions[nodeId];
|
|
230
|
+
delete this.nodeNames[nodeId];
|
|
229
231
|
this.emit("graphChanged", {
|
|
230
|
-
def: this.
|
|
232
|
+
def: this._def,
|
|
231
233
|
change: { type: "removeNode", nodeId },
|
|
232
234
|
...options,
|
|
233
235
|
});
|
|
234
236
|
this.refreshValidation();
|
|
235
237
|
}
|
|
236
238
|
connect(edge, options) {
|
|
237
|
-
const id = edge.id ?? this.genId("e", new Set(this.
|
|
238
|
-
this.
|
|
239
|
+
const id = edge.id ?? this.genId("e", new Set(this._def.edges.map((e) => e.id)));
|
|
240
|
+
this._def.edges.push({
|
|
239
241
|
id,
|
|
240
242
|
source: { ...edge.source },
|
|
241
243
|
target: { ...edge.target },
|
|
242
244
|
typeId: edge.typeId,
|
|
243
245
|
});
|
|
244
246
|
this.emit("graphChanged", {
|
|
245
|
-
def: this.
|
|
247
|
+
def: this._def,
|
|
246
248
|
change: { type: "connect", edgeId: id },
|
|
247
249
|
...options,
|
|
248
250
|
});
|
|
@@ -250,16 +252,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
250
252
|
return id;
|
|
251
253
|
}
|
|
252
254
|
disconnect(edgeId, options) {
|
|
253
|
-
this.
|
|
255
|
+
this._def.edges = this._def.edges.filter((e) => e.id !== edgeId);
|
|
254
256
|
this.emit("graphChanged", {
|
|
255
|
-
def: this.
|
|
257
|
+
def: this._def,
|
|
256
258
|
change: { type: "disconnect", edgeId },
|
|
257
259
|
...options,
|
|
258
260
|
});
|
|
259
261
|
this.emit("validationChanged", this.validate());
|
|
260
262
|
}
|
|
261
263
|
updateEdgeType(edgeId, typeId) {
|
|
262
|
-
const e = this.
|
|
264
|
+
const e = this._def.edges.find((x) => x.id === edgeId);
|
|
263
265
|
if (!e)
|
|
264
266
|
return;
|
|
265
267
|
if (!typeId)
|
|
@@ -267,18 +269,18 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
267
269
|
else
|
|
268
270
|
e.typeId = typeId;
|
|
269
271
|
this.emit("graphChanged", {
|
|
270
|
-
def: this.
|
|
272
|
+
def: this._def,
|
|
271
273
|
change: { type: "updateEdgeType", edgeId, typeId },
|
|
272
274
|
});
|
|
273
275
|
this.refreshValidation();
|
|
274
276
|
}
|
|
275
277
|
updateParams(nodeId, params) {
|
|
276
|
-
const n = this.
|
|
278
|
+
const n = this._def.nodes.find((n) => n.nodeId === nodeId);
|
|
277
279
|
if (!n)
|
|
278
280
|
return;
|
|
279
281
|
n.params = { ...(n.params ?? {}), ...params };
|
|
280
282
|
this.emit("graphChanged", {
|
|
281
|
-
def: this.
|
|
283
|
+
def: this._def,
|
|
282
284
|
change: { type: "updateParams", nodeId },
|
|
283
285
|
});
|
|
284
286
|
}
|
|
@@ -286,7 +288,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
286
288
|
setPositions(map, options) {
|
|
287
289
|
this.positions = { ...this.positions, ...map };
|
|
288
290
|
this.emit("graphUiChanged", {
|
|
289
|
-
def: this.
|
|
291
|
+
def: this._def,
|
|
290
292
|
change: { type: "moveNodes" },
|
|
291
293
|
...options,
|
|
292
294
|
});
|
|
@@ -294,17 +296,20 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
294
296
|
getPositions() {
|
|
295
297
|
return { ...this.positions };
|
|
296
298
|
}
|
|
297
|
-
|
|
299
|
+
setSelectionInternal(sel, options) {
|
|
298
300
|
if (lod.isEqual(this.selection, sel))
|
|
299
301
|
return;
|
|
300
302
|
this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
|
|
301
303
|
this.emit("selectionChanged", this.selection);
|
|
302
304
|
this.emit("graphUiChanged", {
|
|
303
|
-
def: this.
|
|
305
|
+
def: this._def,
|
|
304
306
|
change: { type: "selection" },
|
|
305
307
|
...options,
|
|
306
308
|
});
|
|
307
309
|
}
|
|
310
|
+
setSelection(sel, options) {
|
|
311
|
+
this.setSelectionInternal(sel, options);
|
|
312
|
+
}
|
|
308
313
|
getSelection() {
|
|
309
314
|
return {
|
|
310
315
|
nodes: [...this.selection.nodes],
|
|
@@ -325,14 +330,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
325
330
|
this.disconnect(edgeId);
|
|
326
331
|
}
|
|
327
332
|
// Clear selection
|
|
328
|
-
this.
|
|
333
|
+
this.setSelectionInternal({ nodes: [], edges: [] }, options);
|
|
329
334
|
}
|
|
330
335
|
setViewport(viewport) {
|
|
331
336
|
if (lod.isEqual(this.viewport, viewport))
|
|
332
337
|
return;
|
|
333
338
|
this.viewport = { ...viewport };
|
|
334
339
|
this.emit("graphUiChanged", {
|
|
335
|
-
def: this.
|
|
340
|
+
def: this._def,
|
|
336
341
|
change: { type: "viewport" },
|
|
337
342
|
});
|
|
338
343
|
}
|
|
@@ -340,11 +345,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
340
345
|
return this.viewport ? { ...this.viewport } : null;
|
|
341
346
|
}
|
|
342
347
|
getUIState() {
|
|
343
|
-
const defNodeIds = new Set(this.
|
|
344
|
-
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));
|
|
345
350
|
const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
|
|
346
351
|
const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
|
|
347
352
|
const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
|
|
353
|
+
const filteredNodeNames = Object.fromEntries(Object.entries(this.nodeNames).filter(([id]) => defNodeIds.has(id)));
|
|
348
354
|
return {
|
|
349
355
|
positions: Object.keys(filteredPositions).length > 0
|
|
350
356
|
? filteredPositions
|
|
@@ -356,6 +362,9 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
356
362
|
}
|
|
357
363
|
: undefined,
|
|
358
364
|
viewport: this.viewport ? { ...this.viewport } : undefined,
|
|
365
|
+
nodeNames: Object.keys(filteredNodeNames).length > 0
|
|
366
|
+
? filteredNodeNames
|
|
367
|
+
: undefined,
|
|
359
368
|
};
|
|
360
369
|
}
|
|
361
370
|
setUIState(ui) {
|
|
@@ -374,12 +383,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
374
383
|
if (ui.viewport) {
|
|
375
384
|
this.viewport = { ...ui.viewport };
|
|
376
385
|
}
|
|
386
|
+
if (ui.nodeNames !== undefined) {
|
|
387
|
+
this.nodeNames = { ...ui.nodeNames };
|
|
388
|
+
}
|
|
377
389
|
}
|
|
378
390
|
getRuntimeState() {
|
|
379
391
|
return this.runtimeState ? { ...this.runtimeState } : null;
|
|
380
392
|
}
|
|
381
393
|
setRuntimeState(runtime) {
|
|
382
394
|
this.runtimeState = runtime ? { ...runtime } : null;
|
|
395
|
+
this.emit("runtimeMetadataChanged", {});
|
|
383
396
|
}
|
|
384
397
|
getHistory() {
|
|
385
398
|
return this.historyState;
|
|
@@ -396,6 +409,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
396
409
|
const nodeMeta = current.nodes[nodeId] ?? {};
|
|
397
410
|
const updated = updater({ ...nodeMeta });
|
|
398
411
|
this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
|
|
412
|
+
this.emit("runtimeMetadataChanged", { nodeId });
|
|
399
413
|
}
|
|
400
414
|
on(event, handler) {
|
|
401
415
|
if (!this.listeners.has(event))
|
|
@@ -420,11 +434,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
420
434
|
const selection = this.getSelection();
|
|
421
435
|
if (selection.nodes.length === 0)
|
|
422
436
|
return null;
|
|
423
|
-
const def = this.export();
|
|
424
437
|
const positions = this.getPositions();
|
|
425
438
|
const selectedNodeSet = new Set(selection.nodes);
|
|
426
439
|
// Collect nodes to copy
|
|
427
|
-
const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
|
|
440
|
+
const nodesToCopy = this.def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
|
|
428
441
|
if (nodesToCopy.length === 0)
|
|
429
442
|
return null;
|
|
430
443
|
// Calculate bounds
|
|
@@ -448,12 +461,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
448
461
|
const centerY = (bounds.minY + bounds.maxY) / 2;
|
|
449
462
|
// Get inputs for each node
|
|
450
463
|
// Include values from inbound edges if those edges are selected
|
|
451
|
-
const allInputs = runner.getInputs(def);
|
|
464
|
+
const allInputs = runner.getInputs(this.def);
|
|
452
465
|
const selectedEdgeSet = new Set(selection.edges);
|
|
453
466
|
const copiedNodes = nodesToCopy.map((node) => {
|
|
454
467
|
const pos = positions[node.nodeId] || { x: 0, y: 0 };
|
|
455
468
|
// Get all inbound edges for this node
|
|
456
|
-
const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
|
|
469
|
+
const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === node.nodeId);
|
|
457
470
|
// Build set of handles that have inbound edges
|
|
458
471
|
// But only exclude handles whose edges are NOT selected
|
|
459
472
|
const inboundHandlesToExclude = new Set(inboundEdges
|
|
@@ -477,7 +490,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
477
490
|
};
|
|
478
491
|
});
|
|
479
492
|
// Collect edges between copied nodes
|
|
480
|
-
const copiedEdges = def.edges
|
|
493
|
+
const copiedEdges = this.def.edges
|
|
481
494
|
.filter((edge) => {
|
|
482
495
|
return (selectedNodeSet.has(edge.source.nodeId) &&
|
|
483
496
|
selectedNodeSet.has(edge.target.nodeId));
|
|
@@ -495,6 +508,193 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
495
508
|
bounds,
|
|
496
509
|
};
|
|
497
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Duplicate all selected nodes.
|
|
513
|
+
* Returns the list of newly created node IDs.
|
|
514
|
+
* Each duplicated node is offset by 24px in both x and y directions.
|
|
515
|
+
* Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
|
|
516
|
+
*/
|
|
517
|
+
duplicateSelection(runner, options) {
|
|
518
|
+
const selection = this.getSelection();
|
|
519
|
+
if (selection.nodes.length === 0)
|
|
520
|
+
return [];
|
|
521
|
+
const positions = this.getPositions();
|
|
522
|
+
const newNodes = [];
|
|
523
|
+
// Get inputs without bindings (literal values only)
|
|
524
|
+
const allInputs = runner.getInputs(this.def) || {};
|
|
525
|
+
// Duplicate each selected node
|
|
526
|
+
for (const nodeId of selection.nodes) {
|
|
527
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
528
|
+
if (!n)
|
|
529
|
+
continue;
|
|
530
|
+
const pos = positions[nodeId] || { x: 0, y: 0 };
|
|
531
|
+
const inboundHandles = new Set(this.def.edges
|
|
532
|
+
.filter((e) => e.target.nodeId === nodeId)
|
|
533
|
+
.map((e) => e.target.handle));
|
|
534
|
+
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
535
|
+
const newNodeId = this.addNode({
|
|
536
|
+
typeId: n.typeId,
|
|
537
|
+
params: n.params,
|
|
538
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
539
|
+
resolvedHandles: n.resolvedHandles,
|
|
540
|
+
}, {
|
|
541
|
+
inputs: inputsWithoutBindings,
|
|
542
|
+
copyOutputsFrom: nodeId,
|
|
543
|
+
dry: true,
|
|
544
|
+
});
|
|
545
|
+
newNodes.push(newNodeId);
|
|
546
|
+
}
|
|
547
|
+
// Select all newly duplicated nodes
|
|
548
|
+
if (newNodes.length > 0) {
|
|
549
|
+
this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
|
|
550
|
+
}
|
|
551
|
+
return newNodes;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Bake an output value from a node into a new node.
|
|
555
|
+
* Creates a new node based on the output type's bakeTarget configuration.
|
|
556
|
+
* Returns the ID of the last created node (or undefined if none created).
|
|
557
|
+
*/
|
|
558
|
+
async bake(registry, runner, outputValue, outputTypeId, nodePosition, options) {
|
|
559
|
+
try {
|
|
560
|
+
if (!outputTypeId || outputValue === undefined)
|
|
561
|
+
return undefined;
|
|
562
|
+
const unwrap = (v) => isTypedOutput(v) ? getTypedOutputValue(v) : v;
|
|
563
|
+
const coerceIfNeeded = async (fromType, toType, value) => {
|
|
564
|
+
if (!toType || toType === fromType || !runner?.coerce)
|
|
565
|
+
return value;
|
|
566
|
+
try {
|
|
567
|
+
return await runner.coerce(fromType, toType, value);
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
return value;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
const pos = nodePosition;
|
|
574
|
+
const isArray = outputTypeId.endsWith("[]");
|
|
575
|
+
const baseTypeId = isArray ? outputTypeId.slice(0, -2) : outputTypeId;
|
|
576
|
+
const tArr = isArray ? registry.types.get(outputTypeId) : undefined;
|
|
577
|
+
const tElem = registry.types.get(baseTypeId);
|
|
578
|
+
const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
|
|
579
|
+
const arrTarget = isArray ? tArr?.bakeTarget : undefined;
|
|
580
|
+
const elemTarget = isArray ? tElem?.bakeTarget : undefined;
|
|
581
|
+
let newNodeId;
|
|
582
|
+
if (singleTarget) {
|
|
583
|
+
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
584
|
+
const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
585
|
+
const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
|
|
586
|
+
newNodeId = this.addNode({
|
|
587
|
+
typeId: singleTarget.nodeTypeId,
|
|
588
|
+
position: { x: pos.x + 180, y: pos.y },
|
|
589
|
+
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
590
|
+
}
|
|
591
|
+
else if (isArray && arrTarget) {
|
|
592
|
+
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
593
|
+
const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
594
|
+
const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
|
|
595
|
+
newNodeId = this.addNode({
|
|
596
|
+
typeId: arrTarget.nodeTypeId,
|
|
597
|
+
position: { x: pos.x + 180, y: pos.y },
|
|
598
|
+
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
599
|
+
}
|
|
600
|
+
else if (isArray && elemTarget) {
|
|
601
|
+
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
602
|
+
const inType = getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
603
|
+
const src = unwrap(outputValue);
|
|
604
|
+
const items = Array.isArray(src) ? src : [src];
|
|
605
|
+
const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
|
|
606
|
+
const COLS = 4;
|
|
607
|
+
const DX = 180;
|
|
608
|
+
const DY = 160;
|
|
609
|
+
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
610
|
+
const col = idx % COLS;
|
|
611
|
+
const row = Math.floor(idx / COLS);
|
|
612
|
+
newNodeId = this.addNode({
|
|
613
|
+
typeId: elemTarget.nodeTypeId,
|
|
614
|
+
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
615
|
+
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (newNodeId) {
|
|
619
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "bake" });
|
|
620
|
+
}
|
|
621
|
+
return newNodeId;
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Duplicate a single node.
|
|
629
|
+
* Returns the ID of the newly created node.
|
|
630
|
+
* The duplicated node is offset by 24px in both x and y directions.
|
|
631
|
+
* Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
|
|
632
|
+
*/
|
|
633
|
+
duplicateNode(nodeId, runner, options) {
|
|
634
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
635
|
+
if (!n)
|
|
636
|
+
return undefined;
|
|
637
|
+
const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
638
|
+
// Get inputs without bindings (literal values only)
|
|
639
|
+
const allInputs = runner.getInputs(this.def)[nodeId] || {};
|
|
640
|
+
const inboundHandles = new Set(this.def.edges
|
|
641
|
+
.filter((e) => e.target.nodeId === nodeId)
|
|
642
|
+
.map((e) => e.target.handle));
|
|
643
|
+
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
644
|
+
const newNodeId = this.addNode({
|
|
645
|
+
typeId: n.typeId,
|
|
646
|
+
params: n.params,
|
|
647
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
648
|
+
resolvedHandles: n.resolvedHandles,
|
|
649
|
+
}, {
|
|
650
|
+
inputs: inputsWithoutBindings,
|
|
651
|
+
copyOutputsFrom: nodeId,
|
|
652
|
+
dry: true,
|
|
653
|
+
});
|
|
654
|
+
// Select the newly duplicated node
|
|
655
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node" });
|
|
656
|
+
return newNodeId;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Duplicate a node and all its incoming edges.
|
|
660
|
+
* Returns the ID of the newly created node.
|
|
661
|
+
* The duplicated node is offset by 24px in both x and y directions.
|
|
662
|
+
* All incoming edges are duplicated to point to the new node.
|
|
663
|
+
*/
|
|
664
|
+
duplicateNodeWithEdges(nodeId, runner, options) {
|
|
665
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
666
|
+
if (!n)
|
|
667
|
+
return undefined;
|
|
668
|
+
const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
669
|
+
// Get all inputs (including those with bindings, since edges will be duplicated)
|
|
670
|
+
const inputs = runner.getInputs(this.def)[nodeId] || {};
|
|
671
|
+
// Add the duplicated node
|
|
672
|
+
const newNodeId = this.addNode({
|
|
673
|
+
typeId: n.typeId,
|
|
674
|
+
params: n.params,
|
|
675
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
676
|
+
resolvedHandles: n.resolvedHandles,
|
|
677
|
+
}, {
|
|
678
|
+
inputs,
|
|
679
|
+
copyOutputsFrom: nodeId,
|
|
680
|
+
dry: true,
|
|
681
|
+
});
|
|
682
|
+
// Find all incoming edges (edges where target is the original node)
|
|
683
|
+
const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
684
|
+
// Duplicate each incoming edge to point to the new node
|
|
685
|
+
for (const edge of incomingEdges) {
|
|
686
|
+
this.connect({
|
|
687
|
+
source: edge.source, // Keep the same source
|
|
688
|
+
target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
|
|
689
|
+
typeId: edge.typeId,
|
|
690
|
+
}, { dry: true });
|
|
691
|
+
}
|
|
692
|
+
// Select the newly duplicated node
|
|
693
|
+
if (newNodeId) {
|
|
694
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node-with-edges" });
|
|
695
|
+
}
|
|
696
|
+
return newNodeId;
|
|
697
|
+
}
|
|
498
698
|
/**
|
|
499
699
|
* Paste copied graph data at the specified center position.
|
|
500
700
|
* Returns the mapping from original node IDs to new node IDs.
|
|
@@ -540,7 +740,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
540
740
|
}
|
|
541
741
|
}
|
|
542
742
|
// Select the newly pasted nodes
|
|
543
|
-
this.
|
|
743
|
+
this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
544
744
|
return { nodeIdMap, edgeIds };
|
|
545
745
|
}
|
|
546
746
|
/**
|
|
@@ -555,6 +755,29 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
555
755
|
setCopiedData(data) {
|
|
556
756
|
this.copiedData = data;
|
|
557
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Get the custom name for a node, if set.
|
|
760
|
+
*/
|
|
761
|
+
getNodeName(nodeId) {
|
|
762
|
+
return this.nodeNames[nodeId];
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Set a custom name for a node. Empty string or undefined removes the custom name.
|
|
766
|
+
* This is included in undo/redo history via extData.ui.
|
|
767
|
+
*/
|
|
768
|
+
setNodeName(nodeId, name, options) {
|
|
769
|
+
if (name === undefined || name.trim() === "") {
|
|
770
|
+
delete this.nodeNames[nodeId];
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
this.nodeNames[nodeId] = name.trim();
|
|
774
|
+
}
|
|
775
|
+
this.emit("graphUiChanged", {
|
|
776
|
+
def: this._def,
|
|
777
|
+
change: { type: "nodeName", nodeId },
|
|
778
|
+
...options,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
558
781
|
}
|
|
559
782
|
|
|
560
783
|
class CLIWorkbench {
|
|
@@ -565,8 +788,8 @@ class CLIWorkbench {
|
|
|
565
788
|
async load(def) {
|
|
566
789
|
await this.wb.load(def);
|
|
567
790
|
}
|
|
568
|
-
print(
|
|
569
|
-
const d =
|
|
791
|
+
print(options) {
|
|
792
|
+
const d = this.wb.def;
|
|
570
793
|
const detail = !!options?.detail;
|
|
571
794
|
const lines = [];
|
|
572
795
|
lines.push(`Nodes (${d.nodes.length})`);
|
|
@@ -1935,8 +2158,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1935
2158
|
if (!params.sourceHandle || !params.targetHandle)
|
|
1936
2159
|
return;
|
|
1937
2160
|
// Prevent duplicate edges between the same endpoints
|
|
1938
|
-
const
|
|
1939
|
-
const exists = def.edges.some((e) => e.source.nodeId === params.source &&
|
|
2161
|
+
const exists = wb.def.edges.some((e) => e.source.nodeId === params.source &&
|
|
1940
2162
|
e.source.handle === params.sourceHandle &&
|
|
1941
2163
|
e.target.nodeId === params.target &&
|
|
1942
2164
|
e.target.handle === params.targetHandle);
|
|
@@ -1986,7 +2208,10 @@ function useWorkbenchBridge(wb) {
|
|
|
1986
2208
|
}
|
|
1987
2209
|
}
|
|
1988
2210
|
if (selectionChanged) {
|
|
1989
|
-
wb.setSelection({
|
|
2211
|
+
wb.setSelection({
|
|
2212
|
+
nodes: Array.from(nextNodeIds),
|
|
2213
|
+
edges: current.edges,
|
|
2214
|
+
});
|
|
1990
2215
|
}
|
|
1991
2216
|
if (Object.keys(positions).length > 0) {
|
|
1992
2217
|
wb.setPositions(positions, { commit });
|
|
@@ -2021,7 +2246,10 @@ function useWorkbenchBridge(wb) {
|
|
|
2021
2246
|
}
|
|
2022
2247
|
}
|
|
2023
2248
|
if (selectionChanged) {
|
|
2024
|
-
wb.setSelection({
|
|
2249
|
+
wb.setSelection({
|
|
2250
|
+
nodes: current.nodes,
|
|
2251
|
+
edges: Array.from(nextEdgeIds),
|
|
2252
|
+
});
|
|
2025
2253
|
}
|
|
2026
2254
|
}, [wb]);
|
|
2027
2255
|
const onNodesDelete = useCallback((nodes) => {
|
|
@@ -2467,7 +2695,6 @@ function isSnapshotPayload(parsed) {
|
|
|
2467
2695
|
}
|
|
2468
2696
|
async function download(wb, runner) {
|
|
2469
2697
|
try {
|
|
2470
|
-
const def = wb.export();
|
|
2471
2698
|
const fullUiState = wb.getUIState();
|
|
2472
2699
|
const uiState = excludeViewportFromUIState(fullUiState);
|
|
2473
2700
|
const runtimeState = wb.getRuntimeState();
|
|
@@ -2476,7 +2703,7 @@ async function download(wb, runner) {
|
|
|
2476
2703
|
const fullSnapshot = await runner.snapshotFull();
|
|
2477
2704
|
snapshot = {
|
|
2478
2705
|
...fullSnapshot,
|
|
2479
|
-
def,
|
|
2706
|
+
def: wb.def,
|
|
2480
2707
|
extData: {
|
|
2481
2708
|
...(fullSnapshot.extData || {}),
|
|
2482
2709
|
ui: Object.keys(uiState || {}).length > 0 ? uiState : undefined,
|
|
@@ -2485,9 +2712,9 @@ async function download(wb, runner) {
|
|
|
2485
2712
|
};
|
|
2486
2713
|
}
|
|
2487
2714
|
else {
|
|
2488
|
-
const inputs = runner.getInputs(def);
|
|
2715
|
+
const inputs = runner.getInputs(wb.def);
|
|
2489
2716
|
snapshot = {
|
|
2490
|
-
def,
|
|
2717
|
+
def: wb.def,
|
|
2491
2718
|
inputs,
|
|
2492
2719
|
outputs: {},
|
|
2493
2720
|
environment: {},
|
|
@@ -2526,7 +2753,7 @@ async function upload(parsed, wb, runner) {
|
|
|
2526
2753
|
}
|
|
2527
2754
|
if (runner.isRunning()) {
|
|
2528
2755
|
await runner.applySnapshotFull({
|
|
2529
|
-
def,
|
|
2756
|
+
def: wb.def,
|
|
2530
2757
|
environment,
|
|
2531
2758
|
inputs,
|
|
2532
2759
|
outputs,
|
|
@@ -2534,7 +2761,7 @@ async function upload(parsed, wb, runner) {
|
|
|
2534
2761
|
});
|
|
2535
2762
|
}
|
|
2536
2763
|
else {
|
|
2537
|
-
runner.build(wb.
|
|
2764
|
+
runner.build(wb.def);
|
|
2538
2765
|
if (inputs && typeof inputs === "object") {
|
|
2539
2766
|
for (const [nodeId, map] of Object.entries(inputs)) {
|
|
2540
2767
|
runner.setInputs(nodeId, map, { dry: true });
|
|
@@ -2630,14 +2857,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2630
2857
|
const versionTick = useWorkbenchVersionTick(runner);
|
|
2631
2858
|
const valuesTick = versionTick + graphTick + graphUiTick;
|
|
2632
2859
|
// Def and IO values
|
|
2633
|
-
const
|
|
2634
|
-
const
|
|
2635
|
-
const
|
|
2636
|
-
const outputsMap = useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
|
|
2860
|
+
const inputsMap = useMemo(() => runner.getInputs(wb.def), [runner, wb, wb.def, valuesTick]);
|
|
2861
|
+
const inputDefaultsMap = useMemo(() => runner.getInputDefaults(wb.def), [runner, wb, wb.def, valuesTick]);
|
|
2862
|
+
const outputsMap = useMemo(() => runner.getOutputs(wb.def), [runner, wb, wb.def, valuesTick]);
|
|
2637
2863
|
const outputTypesMap = useMemo(() => {
|
|
2638
2864
|
const out = {};
|
|
2639
2865
|
// Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
|
|
2640
|
-
for (const n of def.nodes) {
|
|
2866
|
+
for (const n of wb.def.nodes) {
|
|
2641
2867
|
const effectiveHandles = computeEffectiveHandles(n, registry);
|
|
2642
2868
|
const outputsDecl = effectiveHandles.outputs;
|
|
2643
2869
|
const handles = Object.keys(outputsDecl);
|
|
@@ -2652,14 +2878,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2652
2878
|
out[n.nodeId] = cur;
|
|
2653
2879
|
}
|
|
2654
2880
|
return out;
|
|
2655
|
-
}, [def, outputsMap, registry]);
|
|
2881
|
+
}, [wb, wb.def, outputsMap, registry]);
|
|
2656
2882
|
// Initialize nodes and derive invalidated status from persisted metadata
|
|
2657
2883
|
useEffect(() => {
|
|
2658
2884
|
const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
|
|
2659
2885
|
setNodeStatus((prev) => {
|
|
2660
2886
|
const next = { ...prev };
|
|
2661
2887
|
const metadata = workbenchRuntimeState;
|
|
2662
|
-
for (const n of def.nodes) {
|
|
2888
|
+
for (const n of wb.def.nodes) {
|
|
2663
2889
|
const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
|
|
2664
2890
|
const nodeMeta = metadata.nodes[n.nodeId];
|
|
2665
2891
|
const updates = {};
|
|
@@ -2681,18 +2907,17 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2681
2907
|
}
|
|
2682
2908
|
return next;
|
|
2683
2909
|
});
|
|
2684
|
-
}, [def, wb]);
|
|
2910
|
+
}, [wb.def, wb]);
|
|
2685
2911
|
// Auto layout (simple layered layout)
|
|
2686
2912
|
const runAutoLayout = useCallback(() => {
|
|
2687
|
-
const cur = wb.export();
|
|
2688
2913
|
// Build DAG layers by indegree
|
|
2689
2914
|
const indegree = {};
|
|
2690
2915
|
const adj = {};
|
|
2691
|
-
for (const n of
|
|
2916
|
+
for (const n of wb.def.nodes) {
|
|
2692
2917
|
indegree[n.nodeId] = 0;
|
|
2693
2918
|
adj[n.nodeId] = [];
|
|
2694
2919
|
}
|
|
2695
|
-
for (const e of
|
|
2920
|
+
for (const e of wb.def.edges) {
|
|
2696
2921
|
indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
|
|
2697
2922
|
adj[e.source.nodeId].push(e.target.nodeId);
|
|
2698
2923
|
}
|
|
@@ -2723,7 +2948,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2723
2948
|
let maxWidth = 0;
|
|
2724
2949
|
const heights = {};
|
|
2725
2950
|
for (const id of layer) {
|
|
2726
|
-
const node =
|
|
2951
|
+
const node = wb.def.nodes.find((n) => n.nodeId === id);
|
|
2727
2952
|
if (!node)
|
|
2728
2953
|
continue;
|
|
2729
2954
|
// Prefer showValues sizing similar to node rendering
|
|
@@ -2749,26 +2974,39 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2749
2974
|
curX += maxWidth + H_GAP;
|
|
2750
2975
|
}
|
|
2751
2976
|
wb.setPositions(pos, { commit: true, reason: "auto-layout" });
|
|
2752
|
-
}, [wb, registry, overrides?.getDefaultNodeSize]);
|
|
2977
|
+
}, [wb, wb.def, registry, overrides?.getDefaultNodeSize]);
|
|
2753
2978
|
const updateEdgeType = useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
|
|
2754
2979
|
const triggerExternal = useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
|
|
2980
|
+
const getNodeDisplayName = useCallback((nodeId) => {
|
|
2981
|
+
const customName = wb.getNodeName(nodeId);
|
|
2982
|
+
if (customName)
|
|
2983
|
+
return customName;
|
|
2984
|
+
const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
|
|
2985
|
+
if (!node)
|
|
2986
|
+
return nodeId;
|
|
2987
|
+
const desc = registry.nodes.get(node.typeId);
|
|
2988
|
+
return desc?.displayName || node.typeId;
|
|
2989
|
+
}, [wb, registry]);
|
|
2990
|
+
const setNodeName = useCallback((nodeId, name) => {
|
|
2991
|
+
wb.setNodeName(nodeId, name, { commit: true, reason: "rename-node" });
|
|
2992
|
+
}, [wb]);
|
|
2755
2993
|
// Helper to save runtime metadata and UI state to extData
|
|
2756
|
-
const saveUiRuntimeMetadata = useCallback(async () => {
|
|
2994
|
+
const saveUiRuntimeMetadata = useCallback(async (workbench, graphRunner) => {
|
|
2757
2995
|
try {
|
|
2758
|
-
const current =
|
|
2996
|
+
const current = workbench.getRuntimeState() ?? { nodes: {} };
|
|
2759
2997
|
const metadata = { nodes: { ...current.nodes } };
|
|
2760
2998
|
// Clean up metadata for nodes that no longer exist
|
|
2761
|
-
const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
|
|
2999
|
+
const nodeIds = new Set(workbench.def.nodes.map((n) => n.nodeId));
|
|
2762
3000
|
for (const nodeId of Object.keys(metadata.nodes)) {
|
|
2763
3001
|
if (!nodeIds.has(nodeId)) {
|
|
2764
3002
|
delete metadata.nodes[nodeId];
|
|
2765
3003
|
}
|
|
2766
3004
|
}
|
|
2767
3005
|
// Save cleaned metadata to workbench state
|
|
2768
|
-
|
|
2769
|
-
const fullUiState =
|
|
3006
|
+
workbench.setRuntimeState(metadata);
|
|
3007
|
+
const fullUiState = workbench.getUIState();
|
|
2770
3008
|
const uiWithoutViewport = excludeViewportFromUIState(fullUiState);
|
|
2771
|
-
await
|
|
3009
|
+
await graphRunner.setExtData?.({
|
|
2772
3010
|
...(Object.keys(uiWithoutViewport || {}).length > 0
|
|
2773
3011
|
? { ui: uiWithoutViewport }
|
|
2774
3012
|
: {}),
|
|
@@ -2778,7 +3016,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2778
3016
|
catch (err) {
|
|
2779
3017
|
console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
|
|
2780
3018
|
}
|
|
2781
|
-
}, [
|
|
3019
|
+
}, []);
|
|
2782
3020
|
// Subscribe to runner/workbench events
|
|
2783
3021
|
useEffect(() => {
|
|
2784
3022
|
const add = (source, type) => (payload) => setEvents((prev) => {
|
|
@@ -2804,9 +3042,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2804
3042
|
});
|
|
2805
3043
|
// Helper to apply resolved handles from event payload to workbench
|
|
2806
3044
|
const applyResolvedHandles = (resolvedHandles) => {
|
|
2807
|
-
const cur = wb.export();
|
|
2808
3045
|
let changed = false;
|
|
2809
|
-
for (const n of
|
|
3046
|
+
for (const n of wb.def.nodes) {
|
|
2810
3047
|
const updated = resolvedHandles[n.nodeId];
|
|
2811
3048
|
if (updated) {
|
|
2812
3049
|
const before = JSON.stringify(n.resolvedHandles || {});
|
|
@@ -3095,6 +3332,9 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3095
3332
|
else if (changeType === "updateEdgeType") {
|
|
3096
3333
|
reason = "update-edge-type";
|
|
3097
3334
|
}
|
|
3335
|
+
else if (changeType === "setInputs") {
|
|
3336
|
+
reason = "set-inputs";
|
|
3337
|
+
}
|
|
3098
3338
|
}
|
|
3099
3339
|
if (event.change?.type === "setInputs") {
|
|
3100
3340
|
const { nodeId, inputs } = event.change;
|
|
@@ -3102,7 +3342,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3102
3342
|
}
|
|
3103
3343
|
if (!runner.isRunning()) {
|
|
3104
3344
|
if (event.commit) {
|
|
3105
|
-
await saveUiRuntimeMetadata();
|
|
3345
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3106
3346
|
const history = await runner.commit(reason).catch((err) => {
|
|
3107
3347
|
console.error("[WorkbenchContext] Error committing:", err);
|
|
3108
3348
|
return undefined;
|
|
@@ -3135,7 +3375,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3135
3375
|
await runner.update(event.def, { dry: event.dry });
|
|
3136
3376
|
}
|
|
3137
3377
|
if (event.commit) {
|
|
3138
|
-
await saveUiRuntimeMetadata();
|
|
3378
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3139
3379
|
const history = await runner
|
|
3140
3380
|
.commit(event.reason ?? reason)
|
|
3141
3381
|
.catch((err) => {
|
|
@@ -3156,7 +3396,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3156
3396
|
setSelectedNodeId(sel.nodes?.[0]);
|
|
3157
3397
|
setSelectedEdgeId(sel.edges?.[0]);
|
|
3158
3398
|
if (sel.commit) {
|
|
3159
|
-
await saveUiRuntimeMetadata();
|
|
3399
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3160
3400
|
const history = await runner
|
|
3161
3401
|
.commit(sel.reason ?? "selection")
|
|
3162
3402
|
.catch((err) => {
|
|
@@ -3195,7 +3435,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3195
3435
|
reason = "selection";
|
|
3196
3436
|
}
|
|
3197
3437
|
}
|
|
3198
|
-
await saveUiRuntimeMetadata();
|
|
3438
|
+
await saveUiRuntimeMetadata(wb, runner);
|
|
3199
3439
|
const history = await runner
|
|
3200
3440
|
.commit(event.reason ?? reason)
|
|
3201
3441
|
.catch((err) => {
|
|
@@ -3215,7 +3455,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3215
3455
|
wb.setRegistry(newReg);
|
|
3216
3456
|
// Trigger a graph update so the UI revalidates with new types/enums/nodes
|
|
3217
3457
|
try {
|
|
3218
|
-
await runner.update(wb.
|
|
3458
|
+
await runner.update(wb.def);
|
|
3219
3459
|
}
|
|
3220
3460
|
catch {
|
|
3221
3461
|
console.error("Failed to update graph definition after registry changed");
|
|
@@ -3238,7 +3478,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3238
3478
|
setNodeStatus(() => {
|
|
3239
3479
|
const next = {};
|
|
3240
3480
|
const metadata = wb.getRuntimeState() ?? { nodes: {} };
|
|
3241
|
-
for (const n of def.nodes) {
|
|
3481
|
+
for (const n of wb.def.nodes) {
|
|
3242
3482
|
const nodeMeta = metadata.nodes[n.nodeId];
|
|
3243
3483
|
next[n.nodeId] = {
|
|
3244
3484
|
activeRuns: 0,
|
|
@@ -3253,6 +3493,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3253
3493
|
errorRunsRef.current = {};
|
|
3254
3494
|
}
|
|
3255
3495
|
});
|
|
3496
|
+
const offWbRuntimeMetadataChanged = wb.on("runtimeMetadataChanged", (event) => {
|
|
3497
|
+
const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
|
|
3498
|
+
setNodeStatus((prev) => {
|
|
3499
|
+
const next = { ...prev };
|
|
3500
|
+
const metadata = workbenchRuntimeState;
|
|
3501
|
+
let changed = false;
|
|
3502
|
+
// If nodeId is specified, only update that node; otherwise update all nodes
|
|
3503
|
+
const nodesToUpdate = event.nodeId
|
|
3504
|
+
? wb.def.nodes.filter((n) => n.nodeId === event.nodeId)
|
|
3505
|
+
: wb.def.nodes;
|
|
3506
|
+
for (const n of nodesToUpdate) {
|
|
3507
|
+
const cur = next[n.nodeId];
|
|
3508
|
+
if (!cur)
|
|
3509
|
+
continue;
|
|
3510
|
+
const nodeMeta = metadata.nodes[n.nodeId];
|
|
3511
|
+
const newInvalidated = computeInvalidatedFromMetadata(nodeMeta);
|
|
3512
|
+
if (cur.invalidated !== newInvalidated) {
|
|
3513
|
+
next[n.nodeId] = { ...cur, invalidated: newInvalidated };
|
|
3514
|
+
changed = true;
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
return changed ? next : prev;
|
|
3518
|
+
});
|
|
3519
|
+
});
|
|
3256
3520
|
wb.refreshValidation();
|
|
3257
3521
|
return () => {
|
|
3258
3522
|
offRunnerValue();
|
|
@@ -3270,13 +3534,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3270
3534
|
offRunnerRegistry();
|
|
3271
3535
|
offRunnerTransport();
|
|
3272
3536
|
offFlowViewport();
|
|
3537
|
+
offWbRuntimeMetadataChanged();
|
|
3273
3538
|
};
|
|
3274
3539
|
}, [runner, wb, setRegistry]);
|
|
3275
3540
|
const isRunning = useCallback(() => runner.isRunning(), [runner]);
|
|
3276
3541
|
const engineKind = useCallback(() => runner.getRunningEngine(), [runner]);
|
|
3277
3542
|
const start = useCallback((engine) => {
|
|
3278
3543
|
try {
|
|
3279
|
-
runner.launch(wb.
|
|
3544
|
+
runner.launch(wb.def, { engine });
|
|
3280
3545
|
}
|
|
3281
3546
|
catch { }
|
|
3282
3547
|
}, [runner, wb]);
|
|
@@ -3351,7 +3616,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3351
3616
|
runner,
|
|
3352
3617
|
registry,
|
|
3353
3618
|
setRegistry,
|
|
3354
|
-
def,
|
|
3355
3619
|
selectedNodeId,
|
|
3356
3620
|
selectedEdgeId,
|
|
3357
3621
|
setSelection,
|
|
@@ -3387,12 +3651,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3387
3651
|
triggerExternal,
|
|
3388
3652
|
uiVersion,
|
|
3389
3653
|
overrides,
|
|
3654
|
+
getNodeDisplayName,
|
|
3655
|
+
setNodeName,
|
|
3390
3656
|
}), [
|
|
3391
3657
|
wb,
|
|
3392
3658
|
runner,
|
|
3393
3659
|
registry,
|
|
3394
3660
|
setRegistry,
|
|
3395
|
-
def,
|
|
3396
3661
|
selectedNodeId,
|
|
3397
3662
|
selectedEdgeId,
|
|
3398
3663
|
setSelection,
|
|
@@ -3427,142 +3692,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3427
3692
|
runner,
|
|
3428
3693
|
uiVersion,
|
|
3429
3694
|
overrides,
|
|
3695
|
+
getNodeDisplayName,
|
|
3696
|
+
setNodeName,
|
|
3430
3697
|
]);
|
|
3431
3698
|
return (jsx(WorkbenchContext.Provider, { value: value, children: children }));
|
|
3432
3699
|
}
|
|
3433
3700
|
|
|
3434
3701
|
function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
|
|
3435
|
-
const doBake = async (handleId) => {
|
|
3436
|
-
try {
|
|
3437
|
-
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
3438
|
-
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
3439
|
-
let newNodeId;
|
|
3440
|
-
if (!typeId || raw === undefined)
|
|
3441
|
-
return;
|
|
3442
|
-
const unwrap = (v) => isTypedOutput(v) ? getTypedOutputValue(v) : v;
|
|
3443
|
-
const coerceIfNeeded = async (fromType, toType, value) => {
|
|
3444
|
-
if (!toType || toType === fromType || !runner?.coerce)
|
|
3445
|
-
return value;
|
|
3446
|
-
try {
|
|
3447
|
-
return await runner.coerce(fromType, toType, value);
|
|
3448
|
-
}
|
|
3449
|
-
catch {
|
|
3450
|
-
return value;
|
|
3451
|
-
}
|
|
3452
|
-
};
|
|
3453
|
-
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3454
|
-
const isArray = typeId.endsWith("[]");
|
|
3455
|
-
const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
|
|
3456
|
-
const tArr = isArray ? registry.types.get(typeId) : undefined;
|
|
3457
|
-
const tElem = registry.types.get(baseTypeId);
|
|
3458
|
-
const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
|
|
3459
|
-
const arrTarget = isArray ? tArr?.bakeTarget : undefined;
|
|
3460
|
-
const elemTarget = isArray ? tElem?.bakeTarget : undefined;
|
|
3461
|
-
if (singleTarget) {
|
|
3462
|
-
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
3463
|
-
const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
3464
|
-
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3465
|
-
newNodeId = wb.addNode({
|
|
3466
|
-
typeId: singleTarget.nodeTypeId,
|
|
3467
|
-
position: { x: pos.x + 180, y: pos.y },
|
|
3468
|
-
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
3469
|
-
}
|
|
3470
|
-
else if (isArray && arrTarget) {
|
|
3471
|
-
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
3472
|
-
const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
3473
|
-
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3474
|
-
newNodeId = wb.addNode({
|
|
3475
|
-
typeId: arrTarget.nodeTypeId,
|
|
3476
|
-
position: { x: pos.x + 180, y: pos.y },
|
|
3477
|
-
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
3478
|
-
}
|
|
3479
|
-
else if (isArray && elemTarget) {
|
|
3480
|
-
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
3481
|
-
const inType = getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
3482
|
-
const src = unwrap(raw);
|
|
3483
|
-
const items = Array.isArray(src) ? src : [src];
|
|
3484
|
-
const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
|
|
3485
|
-
const COLS = 4;
|
|
3486
|
-
const DX = 180;
|
|
3487
|
-
const DY = 160;
|
|
3488
|
-
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
3489
|
-
const col = idx % COLS;
|
|
3490
|
-
const row = Math.floor(idx / COLS);
|
|
3491
|
-
newNodeId = wb.addNode({
|
|
3492
|
-
typeId: elemTarget.nodeTypeId,
|
|
3493
|
-
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
3494
|
-
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
3495
|
-
}
|
|
3496
|
-
}
|
|
3497
|
-
if (newNodeId) {
|
|
3498
|
-
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "bake" });
|
|
3499
|
-
}
|
|
3500
|
-
}
|
|
3501
|
-
catch { }
|
|
3502
|
-
};
|
|
3503
3702
|
return {
|
|
3504
3703
|
onDelete: () => {
|
|
3505
3704
|
wb.removeNode(nodeId, { commit: true });
|
|
3506
3705
|
onClose();
|
|
3507
3706
|
},
|
|
3508
3707
|
onDuplicate: async () => {
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
return onClose();
|
|
3513
|
-
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3514
|
-
const inboundHandles = new Set(def.edges
|
|
3515
|
-
.filter((e) => e.target.nodeId === nodeId)
|
|
3516
|
-
.map((e) => e.target.handle));
|
|
3517
|
-
const allInputs = runner.getInputs(def)[nodeId] || {};
|
|
3518
|
-
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
3519
|
-
const newNodeId = wb.addNode({
|
|
3520
|
-
typeId: n.typeId,
|
|
3521
|
-
params: n.params,
|
|
3522
|
-
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3523
|
-
resolvedHandles: n.resolvedHandles,
|
|
3524
|
-
}, {
|
|
3525
|
-
inputs: inputsWithoutBindings,
|
|
3526
|
-
copyOutputsFrom: nodeId,
|
|
3527
|
-
dry: true,
|
|
3708
|
+
wb.duplicateNode(nodeId, runner, {
|
|
3709
|
+
commit: true,
|
|
3710
|
+
reason: "duplicate-node",
|
|
3528
3711
|
});
|
|
3529
|
-
// Select the newly duplicated node
|
|
3530
|
-
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
|
|
3531
3712
|
onClose();
|
|
3532
3713
|
},
|
|
3533
3714
|
onDuplicateWithEdges: async () => {
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
return onClose();
|
|
3538
|
-
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3539
|
-
// Get inputs without bindings (literal values only)
|
|
3540
|
-
const inputs = runner.getInputs(def)[nodeId] || {};
|
|
3541
|
-
// Add the duplicated node
|
|
3542
|
-
const newNodeId = wb.addNode({
|
|
3543
|
-
typeId: n.typeId,
|
|
3544
|
-
params: n.params,
|
|
3545
|
-
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3546
|
-
resolvedHandles: n.resolvedHandles,
|
|
3547
|
-
}, {
|
|
3548
|
-
inputs,
|
|
3549
|
-
copyOutputsFrom: nodeId,
|
|
3550
|
-
dry: true,
|
|
3715
|
+
wb.duplicateNodeWithEdges(nodeId, runner, {
|
|
3716
|
+
commit: true,
|
|
3717
|
+
reason: "duplicate-node-with-edges",
|
|
3551
3718
|
});
|
|
3552
|
-
// Find all incoming edges (edges where target is the original node)
|
|
3553
|
-
const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
3554
|
-
// Duplicate each incoming edge to point to the new node
|
|
3555
|
-
for (const edge of incomingEdges) {
|
|
3556
|
-
wb.connect({
|
|
3557
|
-
source: edge.source, // Keep the same source
|
|
3558
|
-
target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
|
|
3559
|
-
typeId: edge.typeId,
|
|
3560
|
-
}, { dry: true });
|
|
3561
|
-
}
|
|
3562
|
-
// Select the newly duplicated node and edges
|
|
3563
|
-
if (newNodeId) {
|
|
3564
|
-
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate-with-edges" });
|
|
3565
|
-
}
|
|
3566
3719
|
onClose();
|
|
3567
3720
|
},
|
|
3568
3721
|
onRunPull: async () => {
|
|
@@ -3573,7 +3726,13 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3573
3726
|
onClose();
|
|
3574
3727
|
},
|
|
3575
3728
|
onBake: async (handleId) => {
|
|
3576
|
-
|
|
3729
|
+
const nodePosition = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3730
|
+
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
3731
|
+
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
3732
|
+
await wb.bake(registry, runner, raw, typeId || "", nodePosition, {
|
|
3733
|
+
commit: true,
|
|
3734
|
+
reason: "bake",
|
|
3735
|
+
});
|
|
3577
3736
|
onClose();
|
|
3578
3737
|
},
|
|
3579
3738
|
onCopy: () => {
|
|
@@ -3630,42 +3789,10 @@ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyRes
|
|
|
3630
3789
|
function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
|
|
3631
3790
|
const onDuplicate = runner
|
|
3632
3791
|
? () => {
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
}
|
|
3638
|
-
const def = wb.export();
|
|
3639
|
-
const positions = wb.getPositions();
|
|
3640
|
-
const newNodes = [];
|
|
3641
|
-
// Duplicate each selected node
|
|
3642
|
-
for (const nodeId of selection.nodes) {
|
|
3643
|
-
const n = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3644
|
-
if (!n)
|
|
3645
|
-
continue;
|
|
3646
|
-
const pos = positions[nodeId] || { x: 0, y: 0 };
|
|
3647
|
-
// Get inputs without bindings (literal values only)
|
|
3648
|
-
const allInputs = runner.getInputs(def)[nodeId] || {};
|
|
3649
|
-
const inboundHandles = new Set(def.edges
|
|
3650
|
-
.filter((e) => e.target.nodeId === nodeId)
|
|
3651
|
-
.map((e) => e.target.handle));
|
|
3652
|
-
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
3653
|
-
const newNodeId = wb.addNode({
|
|
3654
|
-
typeId: n.typeId,
|
|
3655
|
-
params: n.params,
|
|
3656
|
-
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3657
|
-
resolvedHandles: n.resolvedHandles,
|
|
3658
|
-
}, {
|
|
3659
|
-
inputs: inputsWithoutBindings,
|
|
3660
|
-
copyOutputsFrom: nodeId,
|
|
3661
|
-
dry: true,
|
|
3662
|
-
});
|
|
3663
|
-
newNodes.push(newNodeId);
|
|
3664
|
-
}
|
|
3665
|
-
// Select all newly duplicated nodes
|
|
3666
|
-
if (newNodes.length > 0) {
|
|
3667
|
-
wb.setSelection({ nodes: newNodes, edges: [] }, { commit: true, reason: "duplicate-selection" });
|
|
3668
|
-
}
|
|
3792
|
+
wb.duplicateSelection(runner, {
|
|
3793
|
+
commit: true,
|
|
3794
|
+
reason: "duplicate-selection",
|
|
3795
|
+
});
|
|
3669
3796
|
onClose();
|
|
3670
3797
|
}
|
|
3671
3798
|
: undefined;
|
|
@@ -3700,9 +3827,8 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
|
|
|
3700
3827
|
const canRedo = history ? history.redoCount > 0 : undefined;
|
|
3701
3828
|
const onSelectAll = wb
|
|
3702
3829
|
? () => {
|
|
3703
|
-
const
|
|
3704
|
-
const
|
|
3705
|
-
const allEdgeIds = def.edges.map((e) => e.id);
|
|
3830
|
+
const allNodeIds = wb.def.nodes.map((n) => n.nodeId);
|
|
3831
|
+
const allEdgeIds = wb.def.edges.map((e) => e.id);
|
|
3706
3832
|
wb.setSelection({ nodes: allNodeIds, edges: allEdgeIds }, { commit: true, reason: "select-all" });
|
|
3707
3833
|
onClose();
|
|
3708
3834
|
}
|
|
@@ -3721,8 +3847,7 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
|
|
|
3721
3847
|
}
|
|
3722
3848
|
function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
|
|
3723
3849
|
try {
|
|
3724
|
-
const
|
|
3725
|
-
const node = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3850
|
+
const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
|
|
3726
3851
|
if (!node)
|
|
3727
3852
|
return [];
|
|
3728
3853
|
const desc = registry.nodes.get(node.typeId);
|
|
@@ -3821,13 +3946,13 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3821
3946
|
return String(value ?? "");
|
|
3822
3947
|
}
|
|
3823
3948
|
};
|
|
3824
|
-
const {
|
|
3949
|
+
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();
|
|
3825
3950
|
const nodeValidationIssues = validationByNode.issues;
|
|
3826
3951
|
const edgeValidationIssues = validationByEdge.issues;
|
|
3827
3952
|
const nodeValidationHandles = validationByNode;
|
|
3828
3953
|
const globalValidationIssues = validationGlobal;
|
|
3829
|
-
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
3830
|
-
const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
|
|
3954
|
+
const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
3955
|
+
const selectedEdge = wb.def.edges.find((e) => e.id === selectedEdgeId);
|
|
3831
3956
|
// Use computeEffectiveHandles to merge registry defaults with dynamically resolved handles
|
|
3832
3957
|
const effectiveHandles = selectedNode
|
|
3833
3958
|
? computeEffectiveHandles(selectedNode, registry)
|
|
@@ -3959,7 +4084,6 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3959
4084
|
setOriginals(nextOriginals);
|
|
3960
4085
|
}, [selectedNodeId, selectedNode, registry, valuesTick]);
|
|
3961
4086
|
const widthClass = debug ? "w-[480px]" : "w-[320px]";
|
|
3962
|
-
const { wb } = useWorkbenchContext();
|
|
3963
4087
|
const deleteEdgeById = (edgeId) => {
|
|
3964
4088
|
if (!edgeId)
|
|
3965
4089
|
return;
|
|
@@ -3986,9 +4110,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3986
4110
|
selectedNodeStatus.activeRunIds.length > 0 ? (jsxs("div", { className: "mt-1", children: [jsx("div", { className: "text-[10px] text-blue-600", children: "RunIds:" }), jsx("div", { className: "flex flex-wrap gap-1 mt-1", children: selectedNodeStatus.activeRunIds.map((runId, idx) => (jsx("span", { className: "text-[10px] px-1.5 py-0.5 bg-blue-100 border border-blue-300 rounded font-mono", children: runId }, idx))) })] })) : (jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "RunIds not available (some runs may have started without runId)" }))] })), !!selectedNodeStatus?.lastError && (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 ??
|
|
3987
4111
|
selectedNodeStatus.lastError) }))] })), jsxs("div", { className: "mb-2", children: [jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
|
|
3988
4112
|
const typeId = getInputTypeId(effectiveHandles.inputs, h);
|
|
3989
|
-
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
4113
|
+
const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
3990
4114
|
e.target.handle === h);
|
|
3991
|
-
const inbound = new Set(def.edges
|
|
4115
|
+
const inbound = new Set(wb.def.edges
|
|
3992
4116
|
.filter((e) => e.target.nodeId === selectedNodeId &&
|
|
3993
4117
|
e.target.handle === h)
|
|
3994
4118
|
.map((e) => e.target.handle));
|
|
@@ -4158,11 +4282,27 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
4158
4282
|
position: "relative",
|
|
4159
4283
|
minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
|
|
4160
4284
|
minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
|
|
4161
|
-
}, children: [jsx(DefaultNodeHeader, { id: id,
|
|
4285
|
+
}, children: [jsx(DefaultNodeHeader, { id: id, typeId: typeId, validation: validation, showId: data.showValues }), jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
|
|
4162
4286
|
});
|
|
4163
4287
|
DefaultNode.displayName = "DefaultNode";
|
|
4164
|
-
function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate, }) {
|
|
4288
|
+
function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInvalidate, }) {
|
|
4165
4289
|
const ctx = useWorkbenchContext();
|
|
4290
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
4291
|
+
const [editValue, setEditValue] = React.useState("");
|
|
4292
|
+
const inputRef = React.useRef(null);
|
|
4293
|
+
// Use getNodeDisplayName if typeId is provided, otherwise use title prop
|
|
4294
|
+
const displayName = typeId ? ctx.getNodeDisplayName(id) : (title ?? id);
|
|
4295
|
+
const effectiveTypeId = typeId ?? title ?? id;
|
|
4296
|
+
// Get the default display name (without custom name) for comparison
|
|
4297
|
+
const getDefaultDisplayName = React.useCallback(() => {
|
|
4298
|
+
if (!typeId)
|
|
4299
|
+
return title ?? id;
|
|
4300
|
+
const node = ctx.wb.def.nodes.find((n) => n.nodeId === id);
|
|
4301
|
+
if (!node)
|
|
4302
|
+
return id;
|
|
4303
|
+
const desc = ctx.registry.nodes.get(node.typeId);
|
|
4304
|
+
return desc?.displayName || node.typeId;
|
|
4305
|
+
}, [ctx, id, typeId, title]);
|
|
4166
4306
|
const handleInvalidate = React.useCallback(() => {
|
|
4167
4307
|
try {
|
|
4168
4308
|
if (onInvalidate)
|
|
@@ -4175,10 +4315,51 @@ function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate,
|
|
|
4175
4315
|
}
|
|
4176
4316
|
catch { }
|
|
4177
4317
|
}, [ctx, id, onInvalidate]);
|
|
4318
|
+
const handleDoubleClick = React.useCallback((e) => {
|
|
4319
|
+
// Only allow editing if typeId is provided (enables renaming)
|
|
4320
|
+
if (!typeId)
|
|
4321
|
+
return;
|
|
4322
|
+
e.stopPropagation();
|
|
4323
|
+
setIsEditing(true);
|
|
4324
|
+
setEditValue(displayName);
|
|
4325
|
+
}, [typeId, displayName]);
|
|
4326
|
+
const handleSave = React.useCallback(() => {
|
|
4327
|
+
if (!typeId)
|
|
4328
|
+
return;
|
|
4329
|
+
const trimmed = editValue.trim();
|
|
4330
|
+
const defaultDisplayName = getDefaultDisplayName();
|
|
4331
|
+
// If the trimmed value matches the default display name or typeId, clear the custom name
|
|
4332
|
+
ctx.setNodeName(id, trimmed === defaultDisplayName || trimmed === effectiveTypeId
|
|
4333
|
+
? undefined
|
|
4334
|
+
: trimmed);
|
|
4335
|
+
setIsEditing(false);
|
|
4336
|
+
}, [ctx, id, editValue, getDefaultDisplayName, effectiveTypeId, typeId]);
|
|
4337
|
+
const handleCancel = React.useCallback(() => {
|
|
4338
|
+
setIsEditing(false);
|
|
4339
|
+
setEditValue(displayName);
|
|
4340
|
+
}, [displayName]);
|
|
4341
|
+
const handleKeyDown = React.useCallback((e) => {
|
|
4342
|
+
if (e.key === "Enter") {
|
|
4343
|
+
e.preventDefault();
|
|
4344
|
+
e.stopPropagation();
|
|
4345
|
+
handleSave();
|
|
4346
|
+
}
|
|
4347
|
+
else if (e.key === "Escape") {
|
|
4348
|
+
e.preventDefault();
|
|
4349
|
+
e.stopPropagation();
|
|
4350
|
+
handleCancel();
|
|
4351
|
+
}
|
|
4352
|
+
}, [handleSave, handleCancel]);
|
|
4353
|
+
React.useEffect(() => {
|
|
4354
|
+
if (isEditing && inputRef.current) {
|
|
4355
|
+
inputRef.current.focus();
|
|
4356
|
+
inputRef.current.select();
|
|
4357
|
+
}
|
|
4358
|
+
}, [isEditing]);
|
|
4178
4359
|
return (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: {
|
|
4179
4360
|
maxHeight: NODE_HEADER_HEIGHT_PX,
|
|
4180
4361
|
minHeight: NODE_HEADER_HEIGHT_PX,
|
|
4181
|
-
}, children: [jsx("
|
|
4362
|
+
}, children: [isEditing ? (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` } })) : (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 })), jsxs("div", { className: "flex items-center gap-1", children: [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) => {
|
|
4182
4363
|
e.stopPropagation();
|
|
4183
4364
|
handleInvalidate();
|
|
4184
4365
|
}, children: jsx(ArrowClockwiseIcon, { size: 10 }) }), right, validation.issues && validation.issues.length > 0 && (jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
|
|
@@ -4564,10 +4745,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4564
4745
|
const { nodeTypes, resolveNodeType } = useMemo(() => {
|
|
4565
4746
|
// Build nodeTypes map using UI extension registry
|
|
4566
4747
|
const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
|
|
4567
|
-
const def = wb.export();
|
|
4568
4748
|
const ids = new Set([
|
|
4569
4749
|
...Array.from(registry.nodes.keys()),
|
|
4570
|
-
...def.nodes.map((n) => n.typeId),
|
|
4750
|
+
...wb.def.nodes.map((n) => n.typeId),
|
|
4571
4751
|
]);
|
|
4572
4752
|
for (const typeId of ids) {
|
|
4573
4753
|
const renderer = ui.getNodeRenderer(typeId);
|
|
@@ -4586,14 +4766,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4586
4766
|
// Include uiVersion to recompute when custom renderers are registered
|
|
4587
4767
|
}, [wb, registry, uiVersion, ui]);
|
|
4588
4768
|
const { nodes, edges } = useMemo(() => {
|
|
4589
|
-
const def = wb.export();
|
|
4590
4769
|
const sel = wb.getSelection();
|
|
4591
4770
|
// Merge defaults with inputs for node display (defaults shown in lighter gray)
|
|
4592
4771
|
const inputsWithDefaults = {};
|
|
4593
|
-
for (const n of def.nodes) {
|
|
4772
|
+
for (const n of wb.def.nodes) {
|
|
4594
4773
|
const nodeInputs = inputsMap[n.nodeId] ?? {};
|
|
4595
4774
|
const nodeDefaults = inputDefaultsMap[n.nodeId] ?? {};
|
|
4596
|
-
const inbound = new Set(def.edges
|
|
4775
|
+
const inbound = new Set(wb.def.edges
|
|
4597
4776
|
.filter((e) => e.target.nodeId === n.nodeId)
|
|
4598
4777
|
.map((e) => e.target.handle));
|
|
4599
4778
|
const merged = { ...nodeInputs };
|
|
@@ -4606,7 +4785,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4606
4785
|
inputsWithDefaults[n.nodeId] = merged;
|
|
4607
4786
|
}
|
|
4608
4787
|
}
|
|
4609
|
-
const out = toReactFlow(def, wb.getPositions(), registry, {
|
|
4788
|
+
const out = toReactFlow(wb.def, wb.getPositions(), registry, {
|
|
4610
4789
|
showValues,
|
|
4611
4790
|
inputs: inputsWithDefaults,
|
|
4612
4791
|
inputDefaults: inputDefaultsMap,
|
|
@@ -5130,11 +5309,11 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
5130
5309
|
});
|
|
5131
5310
|
|
|
5132
5311
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
|
|
5133
|
-
const { wb, runner, registry,
|
|
5312
|
+
const { wb, runner, registry, selectedNodeId, runAutoLayout } = useWorkbenchContext();
|
|
5134
5313
|
const [transportStatus, setTransportStatus] = useState({
|
|
5135
5314
|
state: "local",
|
|
5136
5315
|
});
|
|
5137
|
-
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
5316
|
+
const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
5138
5317
|
const effectiveHandles = selectedNode
|
|
5139
5318
|
? computeEffectiveHandles(selectedNode, registry)
|
|
5140
5319
|
: { inputs: {}, outputs: {}, inputDefaults: {} };
|
|
@@ -5162,7 +5341,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5162
5341
|
if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
|
|
5163
5342
|
return;
|
|
5164
5343
|
try {
|
|
5165
|
-
runner.launch(wb.
|
|
5344
|
+
runner.launch(wb.def, {
|
|
5166
5345
|
engine: kind,
|
|
5167
5346
|
invalidate: evt.shiftKey,
|
|
5168
5347
|
});
|
|
@@ -5230,7 +5409,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5230
5409
|
const setInitialGraph = async (d, inputs) => {
|
|
5231
5410
|
await wb.load(d);
|
|
5232
5411
|
try {
|
|
5233
|
-
runner.build(wb.
|
|
5412
|
+
runner.build(wb.def);
|
|
5234
5413
|
}
|
|
5235
5414
|
catch { }
|
|
5236
5415
|
if (inputs) {
|
|
@@ -5245,36 +5424,27 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5245
5424
|
useEffect(() => {
|
|
5246
5425
|
if (!onChange)
|
|
5247
5426
|
return;
|
|
5248
|
-
const
|
|
5249
|
-
try {
|
|
5250
|
-
const cur = wb.export();
|
|
5251
|
-
const inputs = runner.getInputs(cur);
|
|
5252
|
-
onChange({ def: cur, inputs });
|
|
5253
|
-
}
|
|
5254
|
-
catch { }
|
|
5255
|
-
});
|
|
5256
|
-
const off2 = runner.on("value", () => {
|
|
5427
|
+
const offGraphChanged = wb.on("graphChanged", () => {
|
|
5257
5428
|
try {
|
|
5258
|
-
const cur = wb.
|
|
5429
|
+
const cur = wb.def;
|
|
5259
5430
|
const inputs = runner.getInputs(cur);
|
|
5260
5431
|
onChange({ def: cur, inputs });
|
|
5261
5432
|
}
|
|
5262
5433
|
catch { }
|
|
5263
5434
|
});
|
|
5264
|
-
const
|
|
5435
|
+
const offGraphUiChanged = wb.on("graphUiChanged", (evt) => {
|
|
5265
5436
|
if (!evt.commit)
|
|
5266
5437
|
return;
|
|
5267
5438
|
try {
|
|
5268
|
-
const cur = wb.
|
|
5439
|
+
const cur = wb.def;
|
|
5269
5440
|
const inputs = runner.getInputs(cur);
|
|
5270
5441
|
onChange({ def: cur, inputs });
|
|
5271
5442
|
}
|
|
5272
5443
|
catch { }
|
|
5273
5444
|
});
|
|
5274
5445
|
return () => {
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
off3();
|
|
5446
|
+
offGraphChanged();
|
|
5447
|
+
offGraphUiChanged();
|
|
5278
5448
|
};
|
|
5279
5449
|
}, [wb, runner, onChange]);
|
|
5280
5450
|
const applyExample = useCallback(async (key) => {
|
|
@@ -5297,7 +5467,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5297
5467
|
}
|
|
5298
5468
|
await wb.load(def);
|
|
5299
5469
|
// Build a local runtime so seeded defaults are visible pre-run
|
|
5300
|
-
runner.build(wb.
|
|
5470
|
+
runner.build(wb.def);
|
|
5301
5471
|
// Set initial inputs if provided
|
|
5302
5472
|
if (inputs) {
|
|
5303
5473
|
for (const [nodeId, map] of Object.entries(inputs)) {
|
|
@@ -5382,11 +5552,10 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5382
5552
|
// Only auto-launch for local backend; require explicit Start for remote
|
|
5383
5553
|
if (backendKind !== "local")
|
|
5384
5554
|
return;
|
|
5385
|
-
|
|
5386
|
-
if (!d.nodes || d.nodes.length === 0)
|
|
5555
|
+
if (!wb.def.nodes || wb.def.nodes.length === 0)
|
|
5387
5556
|
return;
|
|
5388
5557
|
try {
|
|
5389
|
-
runner.launch(
|
|
5558
|
+
runner.launch(wb.def, { engine: engine });
|
|
5390
5559
|
}
|
|
5391
5560
|
catch {
|
|
5392
5561
|
// ignore
|
|
@@ -5396,7 +5565,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5396
5565
|
if (!selectedNodeId)
|
|
5397
5566
|
return;
|
|
5398
5567
|
// If selected input is wired (has inbound edge), ignore user input to respect runtime value
|
|
5399
|
-
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
|
|
5568
|
+
const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
|
|
5400
5569
|
if (isLinked)
|
|
5401
5570
|
return;
|
|
5402
5571
|
// If raw is undefined, pass it through to delete the input value
|
|
@@ -5481,7 +5650,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
5481
5650
|
}
|
|
5482
5651
|
}
|
|
5483
5652
|
wb.setInputs(selectedNodeId, { [handle]: value }, { commit: true });
|
|
5484
|
-
}, [selectedNodeId, def.edges, effectiveHandles, wb]);
|
|
5653
|
+
}, [selectedNodeId, wb.def.edges, effectiveHandles, wb]);
|
|
5485
5654
|
const setInput = useMemo(() => {
|
|
5486
5655
|
if (overrides?.setInput) {
|
|
5487
5656
|
return overrides.setInput(baseSetInput, {
|