@bian-womp/spark-workbench 0.1.30 → 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 +484 -124
- 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 +3 -2
- package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
- package/lib/cjs/src/misc/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 +3 -4
- 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 +23 -1
- package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
- package/lib/esm/index.js +481 -123
- 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 +3 -2
- package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
- package/lib/esm/src/misc/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 +3 -4
- 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 +23 -1
- package/lib/esm/src/misc/mapping.d.ts.map +1 -1
- package/package.json +10 -8
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,77 +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
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
})] }));
|
|
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
|
+
} })] }));
|
|
1842
2049
|
});
|
|
1843
2050
|
DefaultNode.displayName = "DefaultNode";
|
|
1844
2051
|
|
|
@@ -2003,6 +2210,58 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2003
2210
|
const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
|
|
2004
2211
|
const nodeValidation = validationByNode;
|
|
2005
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
|
+
};
|
|
2006
2265
|
// Expose imperative API
|
|
2007
2266
|
const rfInstanceRef = useRef(null);
|
|
2008
2267
|
useImperativeHandle(ref, () => ({
|
|
@@ -2013,7 +2272,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2013
2272
|
catch { }
|
|
2014
2273
|
},
|
|
2015
2274
|
}));
|
|
2016
|
-
const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete,
|
|
2275
|
+
const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, } = useWorkbenchBridge(wb);
|
|
2017
2276
|
const { nodeTypes, resolveNodeType } = useMemo(() => {
|
|
2018
2277
|
// Build nodeTypes map using UI extension registry
|
|
2019
2278
|
const ui = wb.getUI();
|
|
@@ -2051,8 +2310,91 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2051
2310
|
selectedNodeIds: new Set(sel.nodes),
|
|
2052
2311
|
selectedEdgeIds: new Set(sel.edges),
|
|
2053
2312
|
});
|
|
2054
|
-
|
|
2055
|
-
|
|
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 };
|
|
2056
2398
|
}, [
|
|
2057
2399
|
showValues,
|
|
2058
2400
|
inputsMap,
|
|
@@ -2064,9 +2406,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2064
2406
|
edgeStatus,
|
|
2065
2407
|
nodeValidation,
|
|
2066
2408
|
edgeValidation,
|
|
2067
|
-
nodeTypes,
|
|
2068
2409
|
resolveNodeType,
|
|
2069
2410
|
]);
|
|
2411
|
+
const throttled = useThrottledValue({ nodes, edges }, 100);
|
|
2070
2412
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
2071
2413
|
const [menuPos, setMenuPos] = useState(null);
|
|
2072
2414
|
const [nodeMenuOpen, setNodeMenuOpen] = useState(false);
|
|
@@ -2093,7 +2435,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2093
2435
|
const addNodeAt = (typeId, pos) => {
|
|
2094
2436
|
wb.addNode({ typeId, position: pos });
|
|
2095
2437
|
};
|
|
2096
|
-
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) })] }) }));
|
|
2097
2439
|
});
|
|
2098
2440
|
|
|
2099
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, }) {
|
|
@@ -2170,7 +2512,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2170
2512
|
runAutoLayout();
|
|
2171
2513
|
};
|
|
2172
2514
|
onInit({ wb, runner, setInitialGraph });
|
|
2173
|
-
}, [onInit, wb, runner, runAutoLayout]);
|
|
2515
|
+
}, [onInit, wb, runner, runAutoLayout, registry, setRegistry]);
|
|
2174
2516
|
// Expose change callback on graph/value changes
|
|
2175
2517
|
useEffect(() => {
|
|
2176
2518
|
if (!onChange)
|
|
@@ -2205,9 +2547,14 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2205
2547
|
if (!ex)
|
|
2206
2548
|
return;
|
|
2207
2549
|
const { registry: r, def } = await ex.load();
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
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
|
+
}
|
|
2211
2558
|
}
|
|
2212
2559
|
await wb.load(def);
|
|
2213
2560
|
// Build a local runtime so seeded defaults are visible pre-run
|
|
@@ -2215,7 +2562,15 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2215
2562
|
runAutoLayout();
|
|
2216
2563
|
setExampleState(key);
|
|
2217
2564
|
onExampleChange?.(key);
|
|
2218
|
-
}, [
|
|
2565
|
+
}, [
|
|
2566
|
+
runner,
|
|
2567
|
+
wb,
|
|
2568
|
+
onExampleChange,
|
|
2569
|
+
runAutoLayout,
|
|
2570
|
+
examples,
|
|
2571
|
+
setRegistry,
|
|
2572
|
+
backendKind,
|
|
2573
|
+
]);
|
|
2219
2574
|
const downloadGraph = useCallback(() => {
|
|
2220
2575
|
try {
|
|
2221
2576
|
const def = wb.export();
|
|
@@ -2325,6 +2680,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2325
2680
|
return;
|
|
2326
2681
|
if (runner.isRunning())
|
|
2327
2682
|
return;
|
|
2683
|
+
// Only auto-launch for local backend; require explicit Start for remote
|
|
2684
|
+
if (backendKind !== "local")
|
|
2685
|
+
return;
|
|
2328
2686
|
const d = wb.export();
|
|
2329
2687
|
if (!d.nodes || d.nodes.length === 0)
|
|
2330
2688
|
return;
|
|
@@ -2337,14 +2695,14 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2337
2695
|
catch {
|
|
2338
2696
|
// ignore
|
|
2339
2697
|
}
|
|
2340
|
-
}, [engine, runner, wb]);
|
|
2698
|
+
}, [engine, runner, wb, backendKind]);
|
|
2341
2699
|
// When switching to remote backend, auto-hydrate registry from backend
|
|
2342
2700
|
useEffect(() => {
|
|
2343
2701
|
if (backendKind === "remote-http" && httpBaseUrl) {
|
|
2344
|
-
|
|
2702
|
+
hydrateFromBackend("remote-http", httpBaseUrl);
|
|
2345
2703
|
}
|
|
2346
2704
|
else if (backendKind === "remote-ws" && wsUrl) {
|
|
2347
|
-
|
|
2705
|
+
hydrateFromBackend("remote-ws", wsUrl);
|
|
2348
2706
|
}
|
|
2349
2707
|
}, [backendKind, httpBaseUrl, wsUrl, hydrateFromBackend]);
|
|
2350
2708
|
useEffect(() => {
|
|
@@ -2518,7 +2876,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2518
2876
|
? "Stop engine before switching example"
|
|
2519
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()
|
|
2520
2878
|
? "Stop engine before switching backend"
|
|
2521
|
-
: 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) => {
|
|
2522
2880
|
const kind = e.target.value || undefined;
|
|
2523
2881
|
onEngineChange?.(kind);
|
|
2524
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: () => {
|
|
@@ -2533,7 +2891,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2533
2891
|
}
|
|
2534
2892
|
}, disabled: !engine, children: "Start" })), jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: "Fit View" }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx("span", { children: "Debug events" })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx("span", { children: "Show values in nodes" })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement, contextPanel: overrides?.contextPanel })] })] }));
|
|
2535
2893
|
}
|
|
2536
|
-
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, onInit, onChange,
|
|
2894
|
+
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, onInit, onChange, }) {
|
|
2537
2895
|
const [registry, setRegistry] = useState(createSimpleGraphRegistry());
|
|
2538
2896
|
const [wb] = useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
|
|
2539
2897
|
const runner = useMemo(() => {
|
|
@@ -2561,5 +2919,5 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
2561
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 }) }));
|
|
2562
2920
|
}
|
|
2563
2921
|
|
|
2564
|
-
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 };
|
|
2565
2923
|
//# sourceMappingURL=index.js.map
|