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