@bian-womp/spark-workbench 0.2.0 → 0.2.1
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 +476 -115
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts +0 -2
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/core/contracts.d.ts +2 -0
- package/lib/cjs/src/core/contracts.d.ts.map +1 -1
- package/lib/cjs/src/index.d.ts +1 -0
- package/lib/cjs/src/index.d.ts.map +1 -1
- package/lib/cjs/src/misc/DefaultNode.d.ts +1 -1
- 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/NodeHandles.d.ts +20 -0
- package/lib/cjs/src/misc/NodeHandles.d.ts.map +1 -0
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts +2 -2
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/cjs/src/misc/constants.d.ts +3 -0
- package/lib/cjs/src/misc/constants.d.ts.map +1 -0
- package/lib/cjs/src/misc/hooks.d.ts +2 -2
- package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
- package/lib/cjs/src/misc/mapping.d.ts +22 -0
- package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
- package/lib/esm/index.js +473 -114
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/InMemoryWorkbench.d.ts +0 -2
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/esm/src/core/contracts.d.ts +2 -0
- package/lib/esm/src/core/contracts.d.ts.map +1 -1
- package/lib/esm/src/index.d.ts +1 -0
- package/lib/esm/src/index.d.ts.map +1 -1
- package/lib/esm/src/misc/DefaultNode.d.ts +1 -1
- 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/NodeHandles.d.ts +20 -0
- package/lib/esm/src/misc/NodeHandles.d.ts.map +1 -0
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchStudio.d.ts +2 -2
- package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/esm/src/misc/constants.d.ts +3 -0
- package/lib/esm/src/misc/constants.d.ts.map +1 -0
- package/lib/esm/src/misc/hooks.d.ts +2 -2
- package/lib/esm/src/misc/hooks.d.ts.map +1 -1
- package/lib/esm/src/misc/mapping.d.ts +22 -0
- package/lib/esm/src/misc/mapping.d.ts.map +1 -1
- package/package.json +7 -5
package/lib/cjs/index.cjs
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
var sparkGraph = require('@bian-womp/spark-graph');
|
|
4
4
|
var sparkRemote = require('@bian-womp/spark-remote');
|
|
5
5
|
var React = require('react');
|
|
6
|
+
var react = require('@xyflow/react');
|
|
6
7
|
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
-
var react = require('@phosphor-icons/react');
|
|
8
|
-
var
|
|
8
|
+
var react$1 = require('@phosphor-icons/react');
|
|
9
|
+
var isEqual = require('lodash/isEqual');
|
|
9
10
|
var cx = require('classnames');
|
|
10
11
|
|
|
11
12
|
class DefaultUIExtensionRegistry {
|
|
@@ -232,6 +233,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
232
233
|
setSelection(sel) {
|
|
233
234
|
this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
|
|
234
235
|
this.emit("selectionChanged", this.selection);
|
|
236
|
+
this.emit("graphUiChanged", {
|
|
237
|
+
def: this.def,
|
|
238
|
+
change: { type: "selection" },
|
|
239
|
+
});
|
|
235
240
|
}
|
|
236
241
|
getSelection() {
|
|
237
242
|
return {
|
|
@@ -239,18 +244,6 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
239
244
|
edges: [...this.selection.edges],
|
|
240
245
|
};
|
|
241
246
|
}
|
|
242
|
-
toggleNodeSelection(nodeId) {
|
|
243
|
-
this.selection.nodes = this.selection.nodes.includes(nodeId)
|
|
244
|
-
? this.selection.nodes.filter((id) => id !== nodeId)
|
|
245
|
-
: [...this.selection.nodes, nodeId];
|
|
246
|
-
this.emit("selectionChanged", this.selection);
|
|
247
|
-
}
|
|
248
|
-
toggleEdgeSelection(edgeId) {
|
|
249
|
-
this.selection.edges = this.selection.edges.includes(edgeId)
|
|
250
|
-
? this.selection.edges.filter((id) => id !== edgeId)
|
|
251
|
-
: [...this.selection.edges, edgeId];
|
|
252
|
-
this.emit("selectionChanged", this.selection);
|
|
253
|
-
}
|
|
254
247
|
on(event, handler) {
|
|
255
248
|
if (!this.listeners.has(event))
|
|
256
249
|
this.listeners.set(event, new Set());
|
|
@@ -760,49 +753,119 @@ function useWorkbenchBridge(wb) {
|
|
|
760
753
|
});
|
|
761
754
|
}, [wb]);
|
|
762
755
|
const onNodesChange = React.useCallback((changes) => {
|
|
756
|
+
// Apply position updates
|
|
763
757
|
changes.forEach((c) => {
|
|
764
|
-
if (c.type === "position" && c.position)
|
|
758
|
+
if (c.type === "position" && c.position) {
|
|
765
759
|
wb.setPosition(c.id, c.position);
|
|
766
|
-
|
|
767
|
-
wb.removeNode(c.id);
|
|
768
|
-
if (c.type === "select")
|
|
769
|
-
wb.toggleNodeSelection(c.id);
|
|
760
|
+
}
|
|
770
761
|
});
|
|
762
|
+
// Derive next node selection from change set
|
|
763
|
+
const current = wb.getSelection();
|
|
764
|
+
const nextNodeIds = new Set(current.nodes);
|
|
765
|
+
let selectionChanged = false;
|
|
766
|
+
for (const change of changes) {
|
|
767
|
+
const type = change?.type;
|
|
768
|
+
if (type === "select") {
|
|
769
|
+
const id = change.id;
|
|
770
|
+
const selected = change.selected;
|
|
771
|
+
if (typeof selected === "boolean") {
|
|
772
|
+
if (selected) {
|
|
773
|
+
if (!nextNodeIds.has(id)) {
|
|
774
|
+
nextNodeIds.add(id);
|
|
775
|
+
selectionChanged = true;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
else if (nextNodeIds.delete(id)) {
|
|
779
|
+
selectionChanged = true;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
else if (type === "selectNodes") {
|
|
784
|
+
const ids = change.ids;
|
|
785
|
+
const selected = change.selected;
|
|
786
|
+
if (Array.isArray(ids) && typeof selected === "boolean") {
|
|
787
|
+
for (const id of ids) {
|
|
788
|
+
if (selected) {
|
|
789
|
+
if (!nextNodeIds.has(id)) {
|
|
790
|
+
nextNodeIds.add(id);
|
|
791
|
+
selectionChanged = true;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
else if (nextNodeIds.delete(id)) {
|
|
795
|
+
selectionChanged = true;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
else if (type === "remove") {
|
|
801
|
+
const id = change.id;
|
|
802
|
+
if (nextNodeIds.delete(id))
|
|
803
|
+
selectionChanged = true;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (selectionChanged) {
|
|
807
|
+
wb.setSelection({ nodes: Array.from(nextNodeIds), edges: current.edges });
|
|
808
|
+
}
|
|
771
809
|
}, [wb]);
|
|
772
810
|
const onEdgesDelete = React.useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
|
|
773
811
|
const onEdgesChange = React.useCallback((changes) => {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
812
|
+
const current = wb.getSelection();
|
|
813
|
+
const nextEdgeIds = new Set(current.edges);
|
|
814
|
+
let selectionChanged = false;
|
|
815
|
+
for (const change of changes) {
|
|
816
|
+
const type = change?.type;
|
|
817
|
+
if (type === "select") {
|
|
818
|
+
const id = change.id;
|
|
819
|
+
const selected = change.selected;
|
|
820
|
+
if (typeof selected === "boolean") {
|
|
821
|
+
if (selected) {
|
|
822
|
+
if (!nextEdgeIds.has(id)) {
|
|
823
|
+
nextEdgeIds.add(id);
|
|
824
|
+
selectionChanged = true;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
else if (nextEdgeIds.delete(id)) {
|
|
828
|
+
selectionChanged = true;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
else if (type === "selectEdges") {
|
|
833
|
+
const ids = change.ids;
|
|
834
|
+
const selected = change.selected;
|
|
835
|
+
if (Array.isArray(ids) && typeof selected === "boolean") {
|
|
836
|
+
for (const id of ids) {
|
|
837
|
+
if (selected) {
|
|
838
|
+
if (!nextEdgeIds.has(id)) {
|
|
839
|
+
nextEdgeIds.add(id);
|
|
840
|
+
selectionChanged = true;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
else if (nextEdgeIds.delete(id)) {
|
|
844
|
+
selectionChanged = true;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
else if (type === "remove") {
|
|
850
|
+
const id = change.id;
|
|
851
|
+
if (nextEdgeIds.delete(id))
|
|
852
|
+
selectionChanged = true;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (selectionChanged) {
|
|
856
|
+
wb.setSelection({ nodes: current.nodes, edges: Array.from(nextEdgeIds) });
|
|
857
|
+
}
|
|
780
858
|
}, [wb]);
|
|
781
859
|
const onNodesDelete = React.useCallback((nodes) => {
|
|
782
860
|
for (const n of nodes)
|
|
783
861
|
wb.removeNode(n.id);
|
|
784
862
|
}, [wb]);
|
|
785
|
-
const onSelectionChange = React.useCallback((sel) => {
|
|
786
|
-
const next = {
|
|
787
|
-
nodes: sel.nodes.map((n) => n.id),
|
|
788
|
-
edges: sel.edges.map((e) => e.id),
|
|
789
|
-
};
|
|
790
|
-
const cur = wb.getSelection();
|
|
791
|
-
const sameLen = cur.nodes.length === next.nodes.length &&
|
|
792
|
-
cur.edges.length === next.edges.length;
|
|
793
|
-
const same = sameLen &&
|
|
794
|
-
cur.nodes.every((id, i) => id === next.nodes[i]) &&
|
|
795
|
-
cur.edges.every((id, i) => id === next.edges[i]);
|
|
796
|
-
if (!same)
|
|
797
|
-
wb.setSelection(next);
|
|
798
|
-
}, [wb]);
|
|
799
863
|
return {
|
|
800
864
|
onConnect,
|
|
801
865
|
onNodesChange,
|
|
802
866
|
onEdgesChange,
|
|
803
867
|
onEdgesDelete,
|
|
804
868
|
onNodesDelete,
|
|
805
|
-
onSelectionChange,
|
|
806
869
|
};
|
|
807
870
|
}
|
|
808
871
|
function useWorkbenchGraphTick(wb) {
|
|
@@ -842,6 +905,39 @@ function useWorkbenchVersionTick(runner) {
|
|
|
842
905
|
}, [runner]);
|
|
843
906
|
return version;
|
|
844
907
|
}
|
|
908
|
+
function useThrottledValue(value, intervalMs) {
|
|
909
|
+
const [throttled, setThrottled] = React.useState(value);
|
|
910
|
+
const lastSetAtRef = React.useRef(0);
|
|
911
|
+
const timeoutRef = React.useRef(null);
|
|
912
|
+
React.useEffect(() => {
|
|
913
|
+
const now = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
|
|
914
|
+
const elapsed = now - lastSetAtRef.current;
|
|
915
|
+
if (elapsed >= intervalMs) {
|
|
916
|
+
lastSetAtRef.current = now;
|
|
917
|
+
setThrottled(value);
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
if (timeoutRef.current !== null) {
|
|
921
|
+
window.clearTimeout(timeoutRef.current);
|
|
922
|
+
}
|
|
923
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
924
|
+
lastSetAtRef.current = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
|
|
925
|
+
setThrottled(value);
|
|
926
|
+
timeoutRef.current = null;
|
|
927
|
+
}, Math.max(0, intervalMs - elapsed));
|
|
928
|
+
}
|
|
929
|
+
return () => { };
|
|
930
|
+
}, [value, intervalMs]);
|
|
931
|
+
React.useEffect(() => {
|
|
932
|
+
return () => {
|
|
933
|
+
if (timeoutRef.current !== null) {
|
|
934
|
+
window.clearTimeout(timeoutRef.current);
|
|
935
|
+
timeoutRef.current = null;
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}, []);
|
|
939
|
+
return throttled;
|
|
940
|
+
}
|
|
845
941
|
// Query param helpers
|
|
846
942
|
function setSearchParam(key, val) {
|
|
847
943
|
if (typeof window === "undefined")
|
|
@@ -1002,7 +1098,13 @@ function summarizeDeep(value) {
|
|
|
1002
1098
|
return value;
|
|
1003
1099
|
}
|
|
1004
1100
|
|
|
1101
|
+
// Shared UI constants for node layout to keep mapping and rendering in sync
|
|
1102
|
+
const NODE_HEADER_HEIGHT_PX = 24;
|
|
1103
|
+
const NODE_ROW_HEIGHT_PX = 22;
|
|
1104
|
+
|
|
1005
1105
|
function toReactFlow(def, positions, registry, opts) {
|
|
1106
|
+
const EDGE_STYLE_ERROR = { stroke: "#ef4444", strokeWidth: 2 };
|
|
1107
|
+
const EDGE_STYLE_RUNNING = { stroke: "#3b82f6" };
|
|
1006
1108
|
const nodeHandleMap = {};
|
|
1007
1109
|
const nodes = def.nodes.map((n) => {
|
|
1008
1110
|
const desc = registry.nodes.get(n.typeId);
|
|
@@ -1014,6 +1116,35 @@ function toReactFlow(def, positions, registry, opts) {
|
|
|
1014
1116
|
inputs: new Set(inputHandles.map((h) => h.id)),
|
|
1015
1117
|
outputs: new Set(outputHandles.map((h) => h.id)),
|
|
1016
1118
|
};
|
|
1119
|
+
// Match DefaultNode sizing heuristics to avoid hidden nodes during re-measure
|
|
1120
|
+
const HEADER_SIZE = NODE_HEADER_HEIGHT_PX;
|
|
1121
|
+
const ROW_SIZE = NODE_ROW_HEIGHT_PX;
|
|
1122
|
+
const maxRows = Math.max(inputHandles.length, outputHandles.length);
|
|
1123
|
+
const initialWidth = opts.showValues ? 320 : 240;
|
|
1124
|
+
const initialHeight = HEADER_SIZE + maxRows * ROW_SIZE;
|
|
1125
|
+
// Precompute handle bounds so edges can render immediately without waiting for measurement
|
|
1126
|
+
const handles = [
|
|
1127
|
+
// Inputs on the left as targets
|
|
1128
|
+
...inputHandles.map((h, i) => ({
|
|
1129
|
+
id: h.id,
|
|
1130
|
+
type: "target",
|
|
1131
|
+
position: react.Position.Left,
|
|
1132
|
+
x: 0,
|
|
1133
|
+
y: HEADER_SIZE + i * ROW_SIZE,
|
|
1134
|
+
width: 1,
|
|
1135
|
+
height: ROW_SIZE + 2,
|
|
1136
|
+
})),
|
|
1137
|
+
// Outputs on the right as sources
|
|
1138
|
+
...outputHandles.map((h, i) => ({
|
|
1139
|
+
id: h.id,
|
|
1140
|
+
type: "source",
|
|
1141
|
+
position: react.Position.Right,
|
|
1142
|
+
x: initialWidth - 1,
|
|
1143
|
+
y: HEADER_SIZE + i * ROW_SIZE,
|
|
1144
|
+
width: 1,
|
|
1145
|
+
height: ROW_SIZE + 2,
|
|
1146
|
+
})),
|
|
1147
|
+
];
|
|
1017
1148
|
return {
|
|
1018
1149
|
id: n.nodeId,
|
|
1019
1150
|
data: {
|
|
@@ -1021,7 +1152,23 @@ function toReactFlow(def, positions, registry, opts) {
|
|
|
1021
1152
|
params: n.params,
|
|
1022
1153
|
inputHandles,
|
|
1023
1154
|
outputHandles,
|
|
1155
|
+
handleLayout: [
|
|
1156
|
+
...inputHandles.map((h, i) => ({
|
|
1157
|
+
id: h.id,
|
|
1158
|
+
type: "target",
|
|
1159
|
+
position: react.Position.Left,
|
|
1160
|
+
y: HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2,
|
|
1161
|
+
})),
|
|
1162
|
+
...outputHandles.map((h, i) => ({
|
|
1163
|
+
id: h.id,
|
|
1164
|
+
type: "source",
|
|
1165
|
+
position: react.Position.Right,
|
|
1166
|
+
y: HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2,
|
|
1167
|
+
})),
|
|
1168
|
+
],
|
|
1024
1169
|
showValues: opts.showValues,
|
|
1170
|
+
renderWidth: initialWidth,
|
|
1171
|
+
renderHeight: initialHeight,
|
|
1025
1172
|
inputValues: opts.inputs?.[n.nodeId],
|
|
1026
1173
|
outputValues: opts.outputs?.[n.nodeId],
|
|
1027
1174
|
status: opts.nodeStatus?.[n.nodeId],
|
|
@@ -1038,6 +1185,11 @@ function toReactFlow(def, positions, registry, opts) {
|
|
|
1038
1185
|
selected: opts.selectedNodeIds
|
|
1039
1186
|
? opts.selectedNodeIds.has(n.nodeId)
|
|
1040
1187
|
: undefined,
|
|
1188
|
+
initialWidth,
|
|
1189
|
+
initialHeight,
|
|
1190
|
+
handles,
|
|
1191
|
+
width: initialWidth,
|
|
1192
|
+
height: initialHeight,
|
|
1041
1193
|
};
|
|
1042
1194
|
});
|
|
1043
1195
|
const edges = def.edges
|
|
@@ -1054,9 +1206,9 @@ function toReactFlow(def, positions, registry, opts) {
|
|
|
1054
1206
|
const hasError = !!st?.lastError;
|
|
1055
1207
|
const isInvalidEdge = !!opts.edgeValidation?.[e.id];
|
|
1056
1208
|
const style = hasError || isInvalidEdge
|
|
1057
|
-
?
|
|
1209
|
+
? EDGE_STYLE_ERROR
|
|
1058
1210
|
: isRunning
|
|
1059
|
-
?
|
|
1211
|
+
? EDGE_STYLE_RUNNING
|
|
1060
1212
|
: undefined;
|
|
1061
1213
|
return {
|
|
1062
1214
|
id: e.id,
|
|
@@ -1084,17 +1236,35 @@ function getNodeBorderClassNames(args) {
|
|
|
1084
1236
|
const hasValidationWarning = !hasValidationError && issues.length > 0;
|
|
1085
1237
|
const isRunning = !!status.activeRuns;
|
|
1086
1238
|
const isInvalid = !!status.invalidated && !isRunning && !hasError;
|
|
1087
|
-
|
|
1239
|
+
// Keep border width constant to avoid layout reflow on selection toggles
|
|
1240
|
+
const borderWidth = "border";
|
|
1088
1241
|
const borderStyle = isInvalid ? "border-dashed" : "border-solid";
|
|
1089
|
-
const
|
|
1090
|
-
? "
|
|
1242
|
+
const severity = hasError || hasValidationError
|
|
1243
|
+
? "red"
|
|
1091
1244
|
: hasValidationWarning
|
|
1092
|
-
? "
|
|
1245
|
+
? "amber"
|
|
1093
1246
|
: isRunning
|
|
1094
|
-
? "
|
|
1095
|
-
: "
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1247
|
+
? "blue"
|
|
1248
|
+
: "gray";
|
|
1249
|
+
const borderBySeverity = {
|
|
1250
|
+
red: "border-red-500",
|
|
1251
|
+
amber: "border-amber-500",
|
|
1252
|
+
blue: "border-blue-500",
|
|
1253
|
+
gray: "border-gray-500 dark:border-gray-400",
|
|
1254
|
+
};
|
|
1255
|
+
const ringBySeverity = {
|
|
1256
|
+
red: "ring-2 ring-red-300 dark:ring-red-900",
|
|
1257
|
+
amber: "ring-2 ring-amber-300 dark:ring-amber-900",
|
|
1258
|
+
blue: "ring-2 ring-blue-200 dark:ring-blue-900",
|
|
1259
|
+
gray: "ring-2 ring-gray-300 dark:ring-gray-500",
|
|
1260
|
+
};
|
|
1261
|
+
const borderColor = borderBySeverity[severity];
|
|
1262
|
+
const ring = isRunning
|
|
1263
|
+
? ringBySeverity.blue
|
|
1264
|
+
: selected
|
|
1265
|
+
? ringBySeverity[severity === "blue" ? "gray" : severity]
|
|
1266
|
+
: "";
|
|
1267
|
+
return [borderWidth, borderStyle, borderColor, ring].join(" ").trim();
|
|
1098
1268
|
}
|
|
1099
1269
|
|
|
1100
1270
|
const WorkbenchContext = React.createContext(null);
|
|
@@ -1588,7 +1758,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1588
1758
|
|
|
1589
1759
|
function IssueBadge({ level, title, size = 12, className, }) {
|
|
1590
1760
|
const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
|
|
1591
|
-
return (jsxRuntime.jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsxRuntime.jsx(react.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react.WarningCircleIcon, { size: size, weight: "fill" })) }));
|
|
1761
|
+
return (jsxRuntime.jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsxRuntime.jsx(react$1.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react$1.WarningCircleIcon, { size: size, weight: "fill" })) }));
|
|
1592
1762
|
}
|
|
1593
1763
|
|
|
1594
1764
|
function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWorkbenchChange, }) {
|
|
@@ -1709,7 +1879,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1709
1879
|
setOriginals(nextOriginals);
|
|
1710
1880
|
}, [selectedNodeId, selectedDesc, valuesTick]);
|
|
1711
1881
|
const widthClass = debug ? "w-[480px]" : "w-[320px]";
|
|
1712
|
-
return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [contextPanel && (jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!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 ??
|
|
1882
|
+
return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [contextPanel && (jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!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 ??
|
|
1713
1883
|
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) => {
|
|
1714
1884
|
const typeId = sparkGraph.getInputTypeId(selectedDesc?.inputs, h);
|
|
1715
1885
|
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
@@ -1770,79 +1940,120 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1770
1940
|
})()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) }), debug && (jsxRuntime.jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsxRuntime.jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
|
|
1771
1941
|
}
|
|
1772
1942
|
|
|
1943
|
+
function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray-600", outputClassName = "!w-2 !h-2 !bg-gray-600", getClassName, renderLabel, labelClassName = "absolute text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", }) {
|
|
1944
|
+
const layout = data.handleLayout ?? [];
|
|
1945
|
+
const byId = React.useMemo(() => {
|
|
1946
|
+
const m = new Map();
|
|
1947
|
+
for (const h of layout) {
|
|
1948
|
+
m.set(h.id, { position: h.position, y: h.y, type: h.type });
|
|
1949
|
+
}
|
|
1950
|
+
return m;
|
|
1951
|
+
}, [layout]);
|
|
1952
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(data.inputHandles ?? []).map((h) => {
|
|
1953
|
+
const placed = byId.get(h.id);
|
|
1954
|
+
const position = placed?.position ?? react.Position.Left;
|
|
1955
|
+
const y = placed?.y;
|
|
1956
|
+
const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ??
|
|
1957
|
+
inputClassName;
|
|
1958
|
+
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: h.id, type: "target", position: position, isConnectable: isConnectable, className: cls, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + " left-2", style: {
|
|
1959
|
+
top: (y ?? 0) - 8,
|
|
1960
|
+
right: "50%",
|
|
1961
|
+
whiteSpace: "nowrap",
|
|
1962
|
+
overflow: "hidden",
|
|
1963
|
+
textOverflow: "ellipsis",
|
|
1964
|
+
}, children: renderLabel({ kind: "input", id: h.id }) }))] }, h.id));
|
|
1965
|
+
}), (data.outputHandles ?? []).map((h) => {
|
|
1966
|
+
const placed = byId.get(h.id);
|
|
1967
|
+
const position = placed?.position ?? react.Position.Right;
|
|
1968
|
+
const y = placed?.y;
|
|
1969
|
+
const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ??
|
|
1970
|
+
outputClassName;
|
|
1971
|
+
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: h.id, type: "source", position: position, isConnectable: isConnectable, className: cls, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + " right-2", style: {
|
|
1972
|
+
top: (y ?? 0) - 8,
|
|
1973
|
+
left: "50%",
|
|
1974
|
+
textAlign: "right",
|
|
1975
|
+
whiteSpace: "nowrap",
|
|
1976
|
+
overflow: "hidden",
|
|
1977
|
+
textOverflow: "ellipsis",
|
|
1978
|
+
}, children: renderLabel({ kind: "output", id: h.id }) }))] }, h.id));
|
|
1979
|
+
})] }));
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1773
1982
|
const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
|
|
1983
|
+
const updateNodeInternals = react.useUpdateNodeInternals();
|
|
1774
1984
|
const { typeId, showValues, inputValues, outputValues, toString } = data;
|
|
1775
1985
|
const inputEntries = data.inputHandles ?? [];
|
|
1776
1986
|
const outputEntries = data.outputHandles ?? [];
|
|
1987
|
+
React.useEffect(() => {
|
|
1988
|
+
updateNodeInternals(id);
|
|
1989
|
+
}, [
|
|
1990
|
+
id,
|
|
1991
|
+
inputEntries.length,
|
|
1992
|
+
outputEntries.length,
|
|
1993
|
+
showValues,
|
|
1994
|
+
updateNodeInternals,
|
|
1995
|
+
]);
|
|
1777
1996
|
const status = data.status ?? { activeRuns: 0 };
|
|
1778
1997
|
const validation = data.validation ?? {
|
|
1779
1998
|
inputs: [],
|
|
1780
1999
|
outputs: [],
|
|
1781
2000
|
issues: [],
|
|
1782
2001
|
};
|
|
1783
|
-
const HEADER_SIZE = 24;
|
|
1784
|
-
const ROW_SIZE = 22;
|
|
1785
|
-
const maxRows = Math.max(inputEntries.length, outputEntries.length);
|
|
1786
|
-
const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
|
|
1787
|
-
const minWidth = data.showValues ? 320 : 240;
|
|
1788
|
-
const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
|
|
1789
2002
|
const hasError = !!status.lastError;
|
|
1790
|
-
const hasValidationError = validation.issues.some((i) => i.level === "error");
|
|
1791
|
-
const hasValidationWarning = !hasValidationError && validation.issues.length > 0;
|
|
1792
2003
|
const isRunning = !!status.activeRuns;
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
? "border-red-500"
|
|
1799
|
-
: hasValidationWarning
|
|
1800
|
-
? "border-amber-500"
|
|
1801
|
-
: isRunning
|
|
1802
|
-
? "border-blue-500"
|
|
1803
|
-
: "border-gray-500 dark:border-gray-400";
|
|
1804
|
-
const ringClasses = isRunning
|
|
1805
|
-
? "ring-2 ring-blue-200 dark:ring-blue-900"
|
|
1806
|
-
: undefined;
|
|
1807
|
-
const borderClasses = cx(borderWidth, borderStyle, borderColor, ringClasses);
|
|
2004
|
+
const containerBorder = getNodeBorderClassNames({
|
|
2005
|
+
selected,
|
|
2006
|
+
status,
|
|
2007
|
+
validation,
|
|
2008
|
+
});
|
|
1808
2009
|
const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
|
|
1809
|
-
return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900",
|
|
2010
|
+
return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900", containerBorder), style: {
|
|
2011
|
+
position: "relative",
|
|
2012
|
+
minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
|
|
2013
|
+
minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
|
|
2014
|
+
}, children: [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: {
|
|
2015
|
+
maxHeight: NODE_HEADER_HEIGHT_PX,
|
|
2016
|
+
minHeight: NODE_HEADER_HEIGHT_PX,
|
|
2017
|
+
}, children: [jsxRuntime.jsx("strong", { className: "flex-1 h-full text-sm", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, children: typeId }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsxRuntime.jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
|
|
1810
2018
|
? "error"
|
|
1811
2019
|
: "warning", size: 12, className: "w-3 h-3", title: validation.issues
|
|
1812
2020
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1813
|
-
.join("; ") })), jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }),
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
2021
|
+
.join("; ") })), jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }), jsxRuntime.jsx("div", { className: cx("h-px", (isRunning || pct > 0) && "bg-blue-200 dark:bg-blue-900"), children: jsxRuntime.jsx("div", { className: cx("h-px transition-all", (isRunning || pct > 0) && "bg-blue-500"), style: { width: isRunning || pct > 0 ? `${pct}%` : 0 } }) }), jsxRuntime.jsx(NodeHandles, { data: data, isConnectable: isConnectable, getClassName: ({ kind, id }) => {
|
|
2022
|
+
const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === id);
|
|
2023
|
+
const hasAny = vIssues.length > 0;
|
|
2024
|
+
const hasErr = vIssues.some((v) => v.level === "error");
|
|
2025
|
+
return cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", kind === "output" && "!rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500"));
|
|
2026
|
+
}, renderLabel: ({ kind, id }) => {
|
|
2027
|
+
const entries = kind === "input" ? inputEntries : outputEntries;
|
|
2028
|
+
const entry = entries.find((e) => e.id === id);
|
|
2029
|
+
if (!entry)
|
|
2030
|
+
return id;
|
|
2031
|
+
const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === id);
|
|
2032
|
+
const hasAny = vIssues.length > 0;
|
|
2033
|
+
const hasErr = vIssues.some((v) => v.level === "error");
|
|
2034
|
+
const title = vIssues
|
|
2035
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
2036
|
+
.join("; ");
|
|
2037
|
+
// Compose label with truncated value to prevent layout growth
|
|
2038
|
+
const valueText = (() => {
|
|
2039
|
+
if (!showValues)
|
|
2040
|
+
return undefined;
|
|
2041
|
+
if (kind === "input") {
|
|
2042
|
+
const txt = toString(entry.typeId, inputValues?.[entry.id]);
|
|
2043
|
+
return typeof txt === "string" ? txt : String(txt);
|
|
2044
|
+
}
|
|
2045
|
+
const resolved = resolveOutputDisplay(outputValues?.[entry.id], entry.typeId);
|
|
2046
|
+
const txt = toString(resolved.typeId, resolved.value);
|
|
2047
|
+
return typeof txt === "string" ? txt : String(txt);
|
|
2048
|
+
})();
|
|
2049
|
+
return (jsxRuntime.jsxs("span", { className: "flex items-center gap-1 w-full", children: [kind === "output" ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [valueText !== undefined && (jsxRuntime.jsx("span", { className: "opacity-60 truncate pl-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText })), jsxRuntime.jsx("span", { className: "truncate shrink-0", style: { maxWidth: "40%" }, children: id })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "truncate shrink-0", style: { maxWidth: "40%" }, children: id }), valueText !== undefined && (jsxRuntime.jsx("span", { className: "opacity-60 truncate pr-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText }))] })), hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "shrink-0", title: title }))] }));
|
|
2050
|
+
} })] }));
|
|
1840
2051
|
});
|
|
1841
2052
|
DefaultNode.displayName = "DefaultNode";
|
|
1842
2053
|
|
|
1843
2054
|
function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
|
|
1844
2055
|
const { registry } = useWorkbenchContext();
|
|
1845
|
-
const rf = react
|
|
2056
|
+
const rf = react.useReactFlow();
|
|
1846
2057
|
const ids = Array.from(registry.nodes.keys());
|
|
1847
2058
|
const [query, setQuery] = React.useState("");
|
|
1848
2059
|
const q = query.trim().toLowerCase();
|
|
@@ -2001,6 +2212,58 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2001
2212
|
const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
|
|
2002
2213
|
const nodeValidation = validationByNode;
|
|
2003
2214
|
const edgeValidation = validationByEdge.errors;
|
|
2215
|
+
// Keep stable references for nodes/edges to avoid unnecessary updates
|
|
2216
|
+
const prevNodesRef = React.useRef([]);
|
|
2217
|
+
const prevEdgesRef = React.useRef([]);
|
|
2218
|
+
function retainStabilityById(prev, next, isSame) {
|
|
2219
|
+
if (prev.length === 0)
|
|
2220
|
+
return next;
|
|
2221
|
+
const map = new Map();
|
|
2222
|
+
for (const p of prev)
|
|
2223
|
+
map.set(p.id, p);
|
|
2224
|
+
const out = new Array(next.length);
|
|
2225
|
+
for (let i = 0; i < next.length; i++) {
|
|
2226
|
+
const n = next[i];
|
|
2227
|
+
const p = map.get(n.id);
|
|
2228
|
+
out[i] = p && isSame(p, n) ? p : n;
|
|
2229
|
+
}
|
|
2230
|
+
return out;
|
|
2231
|
+
}
|
|
2232
|
+
const isSameNode = (a, b) => {
|
|
2233
|
+
// Compare the parts that affect rendering
|
|
2234
|
+
const pick = (n) => ({
|
|
2235
|
+
position: n.position,
|
|
2236
|
+
type: n.type,
|
|
2237
|
+
selected: n.selected,
|
|
2238
|
+
initialWidth: n.initialWidth,
|
|
2239
|
+
initialHeight: n.initialHeight,
|
|
2240
|
+
data: n.data && {
|
|
2241
|
+
typeId: n.data.typeId,
|
|
2242
|
+
inputHandles: n.data.inputHandles,
|
|
2243
|
+
outputHandles: n.data.outputHandles,
|
|
2244
|
+
showValues: n.data.showValues,
|
|
2245
|
+
inputValues: n.data.inputValues,
|
|
2246
|
+
outputValues: n.data.outputValues,
|
|
2247
|
+
status: n.data.status,
|
|
2248
|
+
validation: n.data.validation,
|
|
2249
|
+
},
|
|
2250
|
+
});
|
|
2251
|
+
return isEqual(pick(a), pick(b));
|
|
2252
|
+
};
|
|
2253
|
+
const isSameEdge = (a, b) => {
|
|
2254
|
+
const pick = (e) => ({
|
|
2255
|
+
source: e.source,
|
|
2256
|
+
target: e.target,
|
|
2257
|
+
sourceHandle: e.sourceHandle,
|
|
2258
|
+
targetHandle: e.targetHandle,
|
|
2259
|
+
selected: e.selected,
|
|
2260
|
+
animated: e.animated,
|
|
2261
|
+
style: e.style,
|
|
2262
|
+
label: e.label,
|
|
2263
|
+
type: e.type,
|
|
2264
|
+
});
|
|
2265
|
+
return isEqual(pick(a), pick(b));
|
|
2266
|
+
};
|
|
2004
2267
|
// Expose imperative API
|
|
2005
2268
|
const rfInstanceRef = React.useRef(null);
|
|
2006
2269
|
React.useImperativeHandle(ref, () => ({
|
|
@@ -2011,7 +2274,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2011
2274
|
catch { }
|
|
2012
2275
|
},
|
|
2013
2276
|
}));
|
|
2014
|
-
const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete,
|
|
2277
|
+
const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, } = useWorkbenchBridge(wb);
|
|
2015
2278
|
const { nodeTypes, resolveNodeType } = React.useMemo(() => {
|
|
2016
2279
|
// Build nodeTypes map using UI extension registry
|
|
2017
2280
|
const ui = wb.getUI();
|
|
@@ -2049,8 +2312,91 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2049
2312
|
selectedNodeIds: new Set(sel.nodes),
|
|
2050
2313
|
selectedEdgeIds: new Set(sel.edges),
|
|
2051
2314
|
});
|
|
2052
|
-
//
|
|
2053
|
-
|
|
2315
|
+
// Retain references for unchanged items
|
|
2316
|
+
const stableNodes = retainStabilityById(prevNodesRef.current, out.nodes, isSameNode);
|
|
2317
|
+
const stableEdges = retainStabilityById(prevEdgesRef.current, out.edges, isSameEdge);
|
|
2318
|
+
// Debug: log updates/additions/removals (use value equality, not reference)
|
|
2319
|
+
try {
|
|
2320
|
+
const prevNodeIds = new Set(prevNodesRef.current.map((n) => n.id));
|
|
2321
|
+
const nextNodeIds = new Set(out.nodes.map((n) => n.id));
|
|
2322
|
+
const addedNodeIds = out.nodes
|
|
2323
|
+
.filter((n) => !prevNodeIds.has(n.id))
|
|
2324
|
+
.map((n) => n.id);
|
|
2325
|
+
const removedNodeIds = prevNodesRef.current
|
|
2326
|
+
.filter((n) => !nextNodeIds.has(n.id))
|
|
2327
|
+
.map((n) => n.id);
|
|
2328
|
+
const prevNodeMap = new Map(prevNodesRef.current.map((n) => [n.id, n]));
|
|
2329
|
+
const changedNodeIds = out.nodes
|
|
2330
|
+
.filter((n) => {
|
|
2331
|
+
const p = prevNodeMap.get(n.id);
|
|
2332
|
+
return p ? !isSameNode(p, n) : false;
|
|
2333
|
+
})
|
|
2334
|
+
.map((n) => n.id);
|
|
2335
|
+
// Detect handle updates (ids/length changes) for targeted debug
|
|
2336
|
+
const toIds = (arr) => Array.isArray(arr) ? arr.map((h) => h?.id) : [];
|
|
2337
|
+
const handlesEqual = (a, b) => {
|
|
2338
|
+
const aIds = toIds(a);
|
|
2339
|
+
const bIds = toIds(b);
|
|
2340
|
+
if (aIds.length !== bIds.length)
|
|
2341
|
+
return false;
|
|
2342
|
+
for (let i = 0; i < aIds.length; i++) {
|
|
2343
|
+
if (aIds[i] !== bIds[i])
|
|
2344
|
+
return false;
|
|
2345
|
+
}
|
|
2346
|
+
return true;
|
|
2347
|
+
};
|
|
2348
|
+
const handleChanged = out.nodes
|
|
2349
|
+
.filter((n) => {
|
|
2350
|
+
const p = prevNodeMap.get(n.id);
|
|
2351
|
+
if (!p)
|
|
2352
|
+
return false;
|
|
2353
|
+
const inChanged = !handlesEqual(p.data?.inputHandles, n.data?.inputHandles);
|
|
2354
|
+
const outChanged = !handlesEqual(p.data?.outputHandles, n.data?.outputHandles);
|
|
2355
|
+
return inChanged || outChanged;
|
|
2356
|
+
})
|
|
2357
|
+
.map((n) => n.id);
|
|
2358
|
+
const prevEdgeIds = new Set(prevEdgesRef.current.map((e) => e.id));
|
|
2359
|
+
const nextEdgeIds = new Set(out.edges.map((e) => e.id));
|
|
2360
|
+
const addedEdgeIds = out.edges
|
|
2361
|
+
.filter((e) => !prevEdgeIds.has(e.id))
|
|
2362
|
+
.map((e) => e.id);
|
|
2363
|
+
const removedEdgeIds = prevEdgesRef.current
|
|
2364
|
+
.filter((e) => !nextEdgeIds.has(e.id))
|
|
2365
|
+
.map((e) => e.id);
|
|
2366
|
+
const prevEdgeMap = new Map(prevEdgesRef.current.map((e) => [e.id, e]));
|
|
2367
|
+
const changedEdgeIds = out.edges
|
|
2368
|
+
.filter((e) => {
|
|
2369
|
+
const p = prevEdgeMap.get(e.id);
|
|
2370
|
+
return p ? !isSameEdge(p, e) : false;
|
|
2371
|
+
})
|
|
2372
|
+
.map((e) => e.id);
|
|
2373
|
+
if (addedNodeIds.length ||
|
|
2374
|
+
removedNodeIds.length ||
|
|
2375
|
+
changedNodeIds.length ||
|
|
2376
|
+
handleChanged.length) {
|
|
2377
|
+
// eslint-disable-next-line no-console
|
|
2378
|
+
console.debug("[WorkbenchCanvas] node updates", {
|
|
2379
|
+
added: addedNodeIds,
|
|
2380
|
+
removed: removedNodeIds,
|
|
2381
|
+
changed: changedNodeIds,
|
|
2382
|
+
handleChanged,
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
if (addedEdgeIds.length ||
|
|
2386
|
+
removedEdgeIds.length ||
|
|
2387
|
+
changedEdgeIds.length) {
|
|
2388
|
+
// eslint-disable-next-line no-console
|
|
2389
|
+
console.debug("[WorkbenchCanvas] edge updates", {
|
|
2390
|
+
added: addedEdgeIds,
|
|
2391
|
+
removed: removedEdgeIds,
|
|
2392
|
+
changed: changedEdgeIds,
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
catch { }
|
|
2397
|
+
prevNodesRef.current = stableNodes;
|
|
2398
|
+
prevEdgesRef.current = stableEdges;
|
|
2399
|
+
return { nodes: stableNodes, edges: stableEdges };
|
|
2054
2400
|
}, [
|
|
2055
2401
|
showValues,
|
|
2056
2402
|
inputsMap,
|
|
@@ -2062,9 +2408,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2062
2408
|
edgeStatus,
|
|
2063
2409
|
nodeValidation,
|
|
2064
2410
|
edgeValidation,
|
|
2065
|
-
nodeTypes,
|
|
2066
2411
|
resolveNodeType,
|
|
2067
2412
|
]);
|
|
2413
|
+
const throttled = useThrottledValue({ nodes, edges }, 100);
|
|
2068
2414
|
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
2069
2415
|
const [menuPos, setMenuPos] = React.useState(null);
|
|
2070
2416
|
const [nodeMenuOpen, setNodeMenuOpen] = React.useState(false);
|
|
@@ -2091,7 +2437,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2091
2437
|
const addNodeAt = (typeId, pos) => {
|
|
2092
2438
|
wb.addNode({ typeId, position: pos });
|
|
2093
2439
|
};
|
|
2094
|
-
return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.jsxs(react
|
|
2440
|
+
return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.jsxs(react.ReactFlow, { nodes: throttled.nodes, edges: throttled.edges, nodeTypes: nodeTypes, onlyRenderVisibleElements: true, selectionOnDrag: true, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, onInit: (inst) => (rfInstanceRef.current = inst), children: [jsxRuntime.jsx(react.Background, {}), jsxRuntime.jsx(react.MiniMap, {}), jsxRuntime.jsx(react.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) }), jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: () => setNodeMenuOpen(false) })] }) }));
|
|
2095
2441
|
});
|
|
2096
2442
|
|
|
2097
2443
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
|
|
@@ -2203,9 +2549,14 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2203
2549
|
if (!ex)
|
|
2204
2550
|
return;
|
|
2205
2551
|
const { registry: r, def } = await ex.load();
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2552
|
+
// Keep registry consistent with backend:
|
|
2553
|
+
// - For local backend, allow example to provide its own registry
|
|
2554
|
+
// - For remote backend, NEVER overwrite the hydrated remote registry
|
|
2555
|
+
if (backendKind === "local") {
|
|
2556
|
+
if (r) {
|
|
2557
|
+
setRegistry(r);
|
|
2558
|
+
wb.setRegistry(r);
|
|
2559
|
+
}
|
|
2209
2560
|
}
|
|
2210
2561
|
await wb.load(def);
|
|
2211
2562
|
// Build a local runtime so seeded defaults are visible pre-run
|
|
@@ -2213,7 +2564,15 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2213
2564
|
runAutoLayout();
|
|
2214
2565
|
setExampleState(key);
|
|
2215
2566
|
onExampleChange?.(key);
|
|
2216
|
-
}, [
|
|
2567
|
+
}, [
|
|
2568
|
+
runner,
|
|
2569
|
+
wb,
|
|
2570
|
+
onExampleChange,
|
|
2571
|
+
runAutoLayout,
|
|
2572
|
+
examples,
|
|
2573
|
+
setRegistry,
|
|
2574
|
+
backendKind,
|
|
2575
|
+
]);
|
|
2217
2576
|
const downloadGraph = React.useCallback(() => {
|
|
2218
2577
|
try {
|
|
2219
2578
|
const def = wb.export();
|
|
@@ -2515,11 +2874,11 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2515
2874
|
return overrides.toElement(baseToElement, { registry });
|
|
2516
2875
|
return baseToElement;
|
|
2517
2876
|
}, [overrides, baseToElement, registry]);
|
|
2518
|
-
return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsxs("span", { className: "ml-2 flex items-center gap-1 text-xs", title: transportStatus.kind || undefined, children: [transportStatus.state === "local" && (jsxRuntime.jsx(react.PlugsConnectedIcon, { size: 14, className: "text-gray-500" })), transportStatus.state === "connecting" && (jsxRuntime.jsx(react.ClockClockwiseIcon, { size: 14, className: "text-amber-600 animate-pulse" })), transportStatus.state === "connected" && (jsxRuntime.jsx(react.WifiHighIcon, { size: 14, className: "text-green-600" })), transportStatus.state === "disconnected" && (jsxRuntime.jsx(react.WifiSlashIcon, { size: 14, className: "text-red-600" })), transportStatus.state === "retrying" && (jsxRuntime.jsx(react.ClockClockwiseIcon, { size: 14, className: "text-amber-700 animate-pulse" }))] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
|
|
2877
|
+
return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsxs("span", { className: "ml-2 flex items-center gap-1 text-xs", title: transportStatus.kind || undefined, children: [transportStatus.state === "local" && (jsxRuntime.jsx(react$1.PlugsConnectedIcon, { size: 14, className: "text-gray-500" })), transportStatus.state === "connecting" && (jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 14, className: "text-amber-600 animate-pulse" })), transportStatus.state === "connected" && (jsxRuntime.jsx(react$1.WifiHighIcon, { size: 14, className: "text-green-600" })), transportStatus.state === "disconnected" && (jsxRuntime.jsx(react$1.WifiSlashIcon, { size: 14, className: "text-red-600" })), transportStatus.state === "retrying" && (jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 14, className: "text-amber-700 animate-pulse" }))] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
|
|
2519
2878
|
? "Stop engine before switching example"
|
|
2520
2879
|
: undefined, children: [jsxRuntime.jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsxRuntime.jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
|
|
2521
2880
|
? "Stop engine before switching backend"
|
|
2522
|
-
: undefined, children: [jsxRuntime.jsx("option", { value: "local", children: "Local" }), jsxRuntime.jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsxRuntime.jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: (e) => {
|
|
2881
|
+
: undefined, children: [jsxRuntime.jsx("option", { value: "local", children: "Local" }), jsxRuntime.jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsxRuntime.jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && !!onHttpBaseUrlChange && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && !!onWsUrlChange && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: (e) => {
|
|
2523
2882
|
const kind = e.target.value || undefined;
|
|
2524
2883
|
onEngineChange?.(kind);
|
|
2525
2884
|
}, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.step(), disabled: !runner.isRunning(), children: "Step" })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.flush(), disabled: !runner.isRunning(), children: "Flush" })), runner.isRunning() ? (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.dispose(), disabled: !runner.isRunning(), children: "Stop" })) : (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: () => {
|
|
@@ -2568,6 +2927,7 @@ exports.DefaultUIExtensionRegistry = DefaultUIExtensionRegistry;
|
|
|
2568
2927
|
exports.InMemoryWorkbench = InMemoryWorkbench;
|
|
2569
2928
|
exports.Inspector = Inspector;
|
|
2570
2929
|
exports.LocalGraphRunner = LocalGraphRunner;
|
|
2930
|
+
exports.NodeHandles = NodeHandles;
|
|
2571
2931
|
exports.RemoteGraphRunner = RemoteGraphRunner;
|
|
2572
2932
|
exports.WorkbenchCanvas = WorkbenchCanvas;
|
|
2573
2933
|
exports.WorkbenchContext = WorkbenchContext;
|
|
@@ -2582,6 +2942,7 @@ exports.summarizeDeep = summarizeDeep;
|
|
|
2582
2942
|
exports.toReactFlow = toReactFlow;
|
|
2583
2943
|
exports.useQueryParamBoolean = useQueryParamBoolean;
|
|
2584
2944
|
exports.useQueryParamString = useQueryParamString;
|
|
2945
|
+
exports.useThrottledValue = useThrottledValue;
|
|
2585
2946
|
exports.useWorkbenchBridge = useWorkbenchBridge;
|
|
2586
2947
|
exports.useWorkbenchContext = useWorkbenchContext;
|
|
2587
2948
|
exports.useWorkbenchGraphTick = useWorkbenchGraphTick;
|