@bian-womp/spark-workbench 0.3.45 → 0.3.47
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 +110 -86
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts +6 -16
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
- package/lib/esm/index.js +110 -86
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/InMemoryWorkbench.d.ts +6 -16
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
- package/package.json +4 -4
package/lib/cjs/index.cjs
CHANGED
|
@@ -609,51 +609,6 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
609
609
|
bounds,
|
|
610
610
|
};
|
|
611
611
|
}
|
|
612
|
-
/**
|
|
613
|
-
* Duplicate all selected nodes.
|
|
614
|
-
* Returns the list of newly created node IDs.
|
|
615
|
-
* Each duplicated node is offset by 24px in both x and y directions.
|
|
616
|
-
* Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
|
|
617
|
-
*/
|
|
618
|
-
duplicateSelection(runner, options) {
|
|
619
|
-
const selection = this.getSelection();
|
|
620
|
-
if (selection.nodes.length === 0)
|
|
621
|
-
return [];
|
|
622
|
-
const positions = this.getPositions();
|
|
623
|
-
const sizes = this.getSizes();
|
|
624
|
-
const newNodes = [];
|
|
625
|
-
// Get inputs without bindings (literal values only)
|
|
626
|
-
const allInputs = runner.getInputs(this.def) || {};
|
|
627
|
-
// Duplicate each selected node
|
|
628
|
-
for (const nodeId of selection.nodes) {
|
|
629
|
-
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
630
|
-
if (!n)
|
|
631
|
-
continue;
|
|
632
|
-
const pos = positions[nodeId];
|
|
633
|
-
const size = sizes[nodeId];
|
|
634
|
-
const inboundHandles = new Set(this.def.edges
|
|
635
|
-
.filter((e) => e.target.nodeId === nodeId)
|
|
636
|
-
.map((e) => e.target.handle));
|
|
637
|
-
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
638
|
-
const newNodeId = this.addNode({
|
|
639
|
-
typeId: n.typeId,
|
|
640
|
-
params: n.params,
|
|
641
|
-
resolvedHandles: n.resolvedHandles,
|
|
642
|
-
}, {
|
|
643
|
-
inputs: inputsWithoutBindings,
|
|
644
|
-
position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
|
|
645
|
-
size,
|
|
646
|
-
copyOutputsFrom: nodeId,
|
|
647
|
-
dry: true,
|
|
648
|
-
});
|
|
649
|
-
newNodes.push(newNodeId);
|
|
650
|
-
}
|
|
651
|
-
// Select all newly duplicated nodes
|
|
652
|
-
if (newNodes.length > 0) {
|
|
653
|
-
this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
|
|
654
|
-
}
|
|
655
|
-
return newNodes;
|
|
656
|
-
}
|
|
657
612
|
/**
|
|
658
613
|
* Bake an output value from a node into a new node.
|
|
659
614
|
* Creates a new node based on the output type's bakeTarget configuration.
|
|
@@ -747,37 +702,97 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
747
702
|
}
|
|
748
703
|
}
|
|
749
704
|
/**
|
|
750
|
-
* Duplicate
|
|
751
|
-
* Returns the
|
|
752
|
-
*
|
|
705
|
+
* Duplicate all selected nodes.
|
|
706
|
+
* Returns the list of newly created node IDs.
|
|
707
|
+
* Each duplicated node is offset by 24px in both x and y directions.
|
|
753
708
|
* Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
|
|
754
709
|
*/
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
if (
|
|
758
|
-
return
|
|
759
|
-
const
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
710
|
+
duplicateSelection(runner, options) {
|
|
711
|
+
const selection = this.getSelection();
|
|
712
|
+
if (selection.nodes.length === 0)
|
|
713
|
+
return [];
|
|
714
|
+
const positions = this.getPositions();
|
|
715
|
+
const sizes = this.getSizes();
|
|
716
|
+
const newNodes = [];
|
|
717
|
+
const nodeIdMap = new Map(); // Map from original nodeId to new nodeId
|
|
718
|
+
const processedNodes = new Set(); // Track which nodes have been duplicated
|
|
719
|
+
const selectedNodeSet = new Set(selection.nodes);
|
|
720
|
+
const allInputs = runner.getInputs(this.def) || {};
|
|
721
|
+
// Build a map of incoming edges for each selected node
|
|
722
|
+
const incomingEdgesByNode = new Map();
|
|
723
|
+
for (const nodeId of selection.nodes) {
|
|
724
|
+
const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
725
|
+
incomingEdgesByNode.set(nodeId, incomingEdges);
|
|
726
|
+
}
|
|
727
|
+
// Helper function to check if a node is ready to be processed
|
|
728
|
+
// (all its dependencies from selected nodes have been processed)
|
|
729
|
+
const isNodeReady = (nodeId) => {
|
|
730
|
+
const incomingEdges = incomingEdgesByNode.get(nodeId) || [];
|
|
731
|
+
for (const edge of incomingEdges) {
|
|
732
|
+
// If the source is a selected node, it must have been processed
|
|
733
|
+
if (selectedNodeSet.has(edge.source.nodeId)) {
|
|
734
|
+
if (!processedNodes.has(edge.source.nodeId)) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return true;
|
|
740
|
+
};
|
|
741
|
+
// Process nodes in topological order
|
|
742
|
+
let remainingNodes = new Set(selection.nodes);
|
|
743
|
+
while (remainingNodes.size > 0) {
|
|
744
|
+
// Find nodes that are ready to be processed (no unprocessed dependencies)
|
|
745
|
+
const readyNodes = Array.from(remainingNodes).filter((nodeId) => isNodeReady(nodeId));
|
|
746
|
+
if (readyNodes.length === 0) {
|
|
747
|
+
// If no nodes are ready, there might be a cycle. Process remaining nodes anyway.
|
|
748
|
+
// This shouldn't happen in a DAG, but handle it gracefully.
|
|
749
|
+
readyNodes.push(...remainingNodes);
|
|
750
|
+
}
|
|
751
|
+
// Process each ready node
|
|
752
|
+
for (const nodeId of readyNodes) {
|
|
753
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
754
|
+
if (!n) {
|
|
755
|
+
remainingNodes.delete(nodeId);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const pos = positions[nodeId];
|
|
759
|
+
const size = sizes[nodeId];
|
|
760
|
+
// Get all inputs (including those with bindings, since edges will be duplicated)
|
|
761
|
+
const inputs = allInputs[nodeId] || {};
|
|
762
|
+
// Create the duplicated node
|
|
763
|
+
const newNodeId = this.addNode({
|
|
764
|
+
typeId: n.typeId,
|
|
765
|
+
params: n.params,
|
|
766
|
+
resolvedHandles: n.resolvedHandles,
|
|
767
|
+
}, {
|
|
768
|
+
inputs,
|
|
769
|
+
position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
|
|
770
|
+
size,
|
|
771
|
+
copyOutputsFrom: nodeId,
|
|
772
|
+
dry: true,
|
|
773
|
+
});
|
|
774
|
+
newNodes.push(newNodeId);
|
|
775
|
+
nodeIdMap.set(nodeId, newNodeId);
|
|
776
|
+
processedNodes.add(nodeId);
|
|
777
|
+
// Connect incoming edges for this node
|
|
778
|
+
const incomingEdges = incomingEdgesByNode.get(nodeId) || [];
|
|
779
|
+
for (const edge of incomingEdges) {
|
|
780
|
+
// Determine the source node: use duplicated node if it was duplicated, otherwise use original
|
|
781
|
+
const sourceNodeId = nodeIdMap.get(edge.source.nodeId) || edge.source.nodeId;
|
|
782
|
+
this.connect({
|
|
783
|
+
source: { nodeId: sourceNodeId, handle: edge.source.handle },
|
|
784
|
+
target: { nodeId: newNodeId, handle: edge.target.handle },
|
|
785
|
+
typeId: edge.typeId,
|
|
786
|
+
}, { dry: true });
|
|
787
|
+
}
|
|
788
|
+
remainingNodes.delete(nodeId);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
// Select all newly duplicated nodes
|
|
792
|
+
if (newNodes.length > 0) {
|
|
793
|
+
this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
|
|
794
|
+
}
|
|
795
|
+
return newNodes;
|
|
781
796
|
}
|
|
782
797
|
/**
|
|
783
798
|
* Duplicate a node and all its incoming edges.
|
|
@@ -785,7 +800,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
785
800
|
* The duplicated node is offset by 24px in both x and y directions.
|
|
786
801
|
* All incoming edges are duplicated to point to the new node.
|
|
787
802
|
*/
|
|
788
|
-
|
|
803
|
+
duplicateNode(nodeId, runner, options) {
|
|
789
804
|
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
790
805
|
if (!n)
|
|
791
806
|
return undefined;
|
|
@@ -817,7 +832,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
817
832
|
}
|
|
818
833
|
// Select the newly duplicated node
|
|
819
834
|
if (newNodeId) {
|
|
820
|
-
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node
|
|
835
|
+
this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node" });
|
|
821
836
|
}
|
|
822
837
|
return newNodeId;
|
|
823
838
|
}
|
|
@@ -5091,9 +5106,9 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
5091
5106
|
onClose();
|
|
5092
5107
|
},
|
|
5093
5108
|
onDuplicate: async () => {
|
|
5094
|
-
wb.
|
|
5109
|
+
wb.duplicateNode(nodeId, runner, {
|
|
5095
5110
|
commit: true,
|
|
5096
|
-
reason: "duplicate-node
|
|
5111
|
+
reason: "duplicate-node",
|
|
5097
5112
|
});
|
|
5098
5113
|
onClose();
|
|
5099
5114
|
},
|
|
@@ -5331,6 +5346,13 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
5331
5346
|
return String(value ?? "");
|
|
5332
5347
|
}
|
|
5333
5348
|
};
|
|
5349
|
+
const unwrapForDisplay = (declaredTypeId, value) => {
|
|
5350
|
+
if (sparkGraph.isTyped(value)) {
|
|
5351
|
+
const t = sparkGraph.unwrapTypeId(value) ?? declaredTypeId;
|
|
5352
|
+
return { typeId: t, value: sparkGraph.unwrapValue(value) };
|
|
5353
|
+
}
|
|
5354
|
+
return { typeId: declaredTypeId, value };
|
|
5355
|
+
};
|
|
5334
5356
|
const { wb, registryVersion, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, inputValidationErrors, clearSystemErrors, clearRegistryErrors, clearInputValidationErrors, removeSystemError, removeRegistryError, removeInputValidationError, handlesMap, } = useWorkbenchContext();
|
|
5335
5357
|
const nodeValidationIssues = validationByNode.issues;
|
|
5336
5358
|
const edgeValidationIssues = validationByEdge.issues;
|
|
@@ -5460,7 +5482,8 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
5460
5482
|
for (const h of handles) {
|
|
5461
5483
|
const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
|
|
5462
5484
|
const current = nodeInputs[h];
|
|
5463
|
-
const
|
|
5485
|
+
const { typeId: displayTypeId, value: displayValue } = unwrapForDisplay(typeId, current);
|
|
5486
|
+
const display = safeToString(displayTypeId, displayValue);
|
|
5464
5487
|
const wasOriginal = originals[h];
|
|
5465
5488
|
const isDirty = drafts[h] !== undefined &&
|
|
5466
5489
|
wasOriginal !== undefined &&
|
|
@@ -5512,18 +5535,19 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
5512
5535
|
.filter((e) => e.target.nodeId === selectedNodeId &&
|
|
5513
5536
|
e.target.handle === h)
|
|
5514
5537
|
.map((e) => e.target.handle));
|
|
5538
|
+
const { typeId: defaultTypeId, value: defaultValue } = unwrapForDisplay(typeId, nodeInputsDefaults[h]);
|
|
5515
5539
|
const hasDefault = !inbound.has(h) && nodeInputsDefaults[h] !== undefined;
|
|
5516
5540
|
const defaultStr = hasDefault
|
|
5517
|
-
? safeToString(
|
|
5541
|
+
? safeToString(defaultTypeId, defaultValue)
|
|
5518
5542
|
: undefined;
|
|
5519
5543
|
const commonProps = {
|
|
5520
5544
|
style: { flex: 1 },
|
|
5521
5545
|
disabled: isLinked,
|
|
5522
5546
|
};
|
|
5523
5547
|
const current = nodeInputs[h];
|
|
5524
|
-
const
|
|
5525
|
-
const
|
|
5526
|
-
const
|
|
5548
|
+
const { typeId: displayTypeId, value: displayValue } = unwrapForDisplay(typeId, current);
|
|
5549
|
+
const hasValue = displayValue !== undefined && displayValue !== null;
|
|
5550
|
+
const value = drafts[h] ?? safeToString(displayTypeId, displayValue);
|
|
5527
5551
|
const placeholder = hasDefault ? defaultStr : undefined;
|
|
5528
5552
|
const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
|
|
5529
5553
|
const commit = () => {
|
|
@@ -5549,7 +5573,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
5549
5573
|
setDrafts((d) => ({ ...d, [h]: "" }));
|
|
5550
5574
|
setOriginals((o) => ({ ...o, [h]: "" }));
|
|
5551
5575
|
};
|
|
5552
|
-
const isEnum =
|
|
5576
|
+
const isEnum = displayTypeId?.startsWith("enum:");
|
|
5553
5577
|
const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
|
|
5554
5578
|
const hasValidation = inIssues.length > 0;
|
|
5555
5579
|
const hasErr = inIssues.some((m) => m.level === "error");
|
|
@@ -5570,9 +5594,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
5570
5594
|
? `Default: ${placeholder}`
|
|
5571
5595
|
: "(select)" }), wb.registry.enums
|
|
5572
5596
|
.get(typeId)
|
|
5573
|
-
?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(
|
|
5597
|
+
?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(displayTypeId, displayValue) }) })) : (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1 select-text", placeholder: placeholder
|
|
5574
5598
|
? `Default: ${placeholder}`
|
|
5575
|
-
: undefined, value:
|
|
5599
|
+
: undefined, value: value, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
|
|
5576
5600
|
if (e.key === "Enter")
|
|
5577
5601
|
commit();
|
|
5578
5602
|
if (e.key === "Escape")
|