@bian-womp/spark-workbench 0.2.0 → 0.2.2
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 -118
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts +1 -2
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/core/contracts.d.ts +7 -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/context/WorkbenchContext.d.ts +1 -0
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/cjs/src/misc/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 +481 -117
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/InMemoryWorkbench.d.ts +1 -2
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/esm/src/core/contracts.d.ts +7 -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/context/WorkbenchContext.d.ts +1 -0
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/esm/src/misc/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 {
|
|
@@ -199,6 +200,20 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
199
200
|
});
|
|
200
201
|
this.emit("validationChanged", this.validate());
|
|
201
202
|
}
|
|
203
|
+
updateEdgeType(edgeId, typeId) {
|
|
204
|
+
const e = this.def.edges.find((x) => x.id === edgeId);
|
|
205
|
+
if (!e)
|
|
206
|
+
return;
|
|
207
|
+
if (!typeId)
|
|
208
|
+
delete e.typeId;
|
|
209
|
+
else
|
|
210
|
+
e.typeId = typeId;
|
|
211
|
+
this.emit("graphChanged", {
|
|
212
|
+
def: this.def,
|
|
213
|
+
change: { type: "updateEdgeType", edgeId, typeId },
|
|
214
|
+
});
|
|
215
|
+
this.refreshValidation();
|
|
216
|
+
}
|
|
202
217
|
updateParams(nodeId, params) {
|
|
203
218
|
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
204
219
|
if (!n)
|
|
@@ -230,6 +245,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
230
245
|
setSelection(sel) {
|
|
231
246
|
this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
|
|
232
247
|
this.emit("selectionChanged", this.selection);
|
|
248
|
+
this.emit("graphUiChanged", {
|
|
249
|
+
def: this.def,
|
|
250
|
+
change: { type: "selection" },
|
|
251
|
+
});
|
|
233
252
|
}
|
|
234
253
|
getSelection() {
|
|
235
254
|
return {
|
|
@@ -237,18 +256,6 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
237
256
|
edges: [...this.selection.edges],
|
|
238
257
|
};
|
|
239
258
|
}
|
|
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
259
|
on(event, handler) {
|
|
253
260
|
if (!this.listeners.has(event))
|
|
254
261
|
this.listeners.set(event, new Set());
|
|
@@ -758,49 +765,91 @@ function useWorkbenchBridge(wb) {
|
|
|
758
765
|
});
|
|
759
766
|
}, [wb]);
|
|
760
767
|
const onNodesChange = useCallback((changes) => {
|
|
768
|
+
// Apply position updates
|
|
761
769
|
changes.forEach((c) => {
|
|
762
|
-
if (c.type === "position" && c.position)
|
|
770
|
+
if (c.type === "position" && c.position) {
|
|
763
771
|
wb.setPosition(c.id, c.position);
|
|
764
|
-
|
|
765
|
-
wb.removeNode(c.id);
|
|
766
|
-
if (c.type === "select")
|
|
767
|
-
wb.toggleNodeSelection(c.id);
|
|
772
|
+
}
|
|
768
773
|
});
|
|
774
|
+
// Derive next node selection from change set
|
|
775
|
+
const current = wb.getSelection();
|
|
776
|
+
const nextNodeIds = new Set(current.nodes);
|
|
777
|
+
let selectionChanged = false;
|
|
778
|
+
for (const change of changes) {
|
|
779
|
+
const type = change?.type;
|
|
780
|
+
if (type === "select") {
|
|
781
|
+
const id = change.id;
|
|
782
|
+
const selected = change.selected;
|
|
783
|
+
if (typeof selected === "boolean") {
|
|
784
|
+
if (selected) {
|
|
785
|
+
if (!nextNodeIds.has(id)) {
|
|
786
|
+
nextNodeIds.add(id);
|
|
787
|
+
selectionChanged = true;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
else if (nextNodeIds.delete(id)) {
|
|
791
|
+
selectionChanged = true;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
else if (type === "remove") {
|
|
796
|
+
const id = change.id;
|
|
797
|
+
if (nextNodeIds.delete(id))
|
|
798
|
+
selectionChanged = true;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (selectionChanged) {
|
|
802
|
+
wb.setSelection({
|
|
803
|
+
nodes: Array.from(nextNodeIds),
|
|
804
|
+
edges: current.edges,
|
|
805
|
+
});
|
|
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 === "remove") {
|
|
831
|
+
const id = change.id;
|
|
832
|
+
if (nextEdgeIds.delete(id))
|
|
833
|
+
selectionChanged = true;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (selectionChanged) {
|
|
837
|
+
wb.setSelection({
|
|
838
|
+
nodes: current.nodes,
|
|
839
|
+
edges: Array.from(nextEdgeIds),
|
|
840
|
+
});
|
|
841
|
+
}
|
|
778
842
|
}, [wb]);
|
|
779
843
|
const onNodesDelete = useCallback((nodes) => {
|
|
780
844
|
for (const n of nodes)
|
|
781
845
|
wb.removeNode(n.id);
|
|
782
846
|
}, [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
847
|
return {
|
|
798
848
|
onConnect,
|
|
799
849
|
onNodesChange,
|
|
800
850
|
onEdgesChange,
|
|
801
851
|
onEdgesDelete,
|
|
802
852
|
onNodesDelete,
|
|
803
|
-
onSelectionChange,
|
|
804
853
|
};
|
|
805
854
|
}
|
|
806
855
|
function useWorkbenchGraphTick(wb) {
|
|
@@ -840,6 +889,44 @@ function useWorkbenchVersionTick(runner) {
|
|
|
840
889
|
}, [runner]);
|
|
841
890
|
return version;
|
|
842
891
|
}
|
|
892
|
+
function useThrottledValue(value, intervalMs) {
|
|
893
|
+
const [throttled, setThrottled] = useState(value);
|
|
894
|
+
const lastSetAtRef = useRef(0);
|
|
895
|
+
const timeoutRef = useRef(null);
|
|
896
|
+
useEffect(() => {
|
|
897
|
+
const now = typeof performance !== "undefined" && performance.now
|
|
898
|
+
? performance.now()
|
|
899
|
+
: Date.now();
|
|
900
|
+
const elapsed = now - lastSetAtRef.current;
|
|
901
|
+
if (elapsed >= intervalMs) {
|
|
902
|
+
lastSetAtRef.current = now;
|
|
903
|
+
setThrottled(value);
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
if (timeoutRef.current !== null) {
|
|
907
|
+
window.clearTimeout(timeoutRef.current);
|
|
908
|
+
}
|
|
909
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
910
|
+
lastSetAtRef.current =
|
|
911
|
+
typeof performance !== "undefined" && performance.now
|
|
912
|
+
? performance.now()
|
|
913
|
+
: Date.now();
|
|
914
|
+
setThrottled(value);
|
|
915
|
+
timeoutRef.current = null;
|
|
916
|
+
}, Math.max(0, intervalMs - elapsed));
|
|
917
|
+
}
|
|
918
|
+
return () => { };
|
|
919
|
+
}, [value, intervalMs]);
|
|
920
|
+
useEffect(() => {
|
|
921
|
+
return () => {
|
|
922
|
+
if (timeoutRef.current !== null) {
|
|
923
|
+
window.clearTimeout(timeoutRef.current);
|
|
924
|
+
timeoutRef.current = null;
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
}, []);
|
|
928
|
+
return throttled;
|
|
929
|
+
}
|
|
843
930
|
// Query param helpers
|
|
844
931
|
function setSearchParam(key, val) {
|
|
845
932
|
if (typeof window === "undefined")
|
|
@@ -1000,7 +1087,13 @@ function summarizeDeep(value) {
|
|
|
1000
1087
|
return value;
|
|
1001
1088
|
}
|
|
1002
1089
|
|
|
1090
|
+
// Shared UI constants for node layout to keep mapping and rendering in sync
|
|
1091
|
+
const NODE_HEADER_HEIGHT_PX = 24;
|
|
1092
|
+
const NODE_ROW_HEIGHT_PX = 22;
|
|
1093
|
+
|
|
1003
1094
|
function toReactFlow(def, positions, registry, opts) {
|
|
1095
|
+
const EDGE_STYLE_ERROR = { stroke: "#ef4444", strokeWidth: 2 };
|
|
1096
|
+
const EDGE_STYLE_RUNNING = { stroke: "#3b82f6" };
|
|
1004
1097
|
const nodeHandleMap = {};
|
|
1005
1098
|
const nodes = def.nodes.map((n) => {
|
|
1006
1099
|
const desc = registry.nodes.get(n.typeId);
|
|
@@ -1012,6 +1105,35 @@ function toReactFlow(def, positions, registry, opts) {
|
|
|
1012
1105
|
inputs: new Set(inputHandles.map((h) => h.id)),
|
|
1013
1106
|
outputs: new Set(outputHandles.map((h) => h.id)),
|
|
1014
1107
|
};
|
|
1108
|
+
// Match DefaultNode sizing heuristics to avoid hidden nodes during re-measure
|
|
1109
|
+
const HEADER_SIZE = NODE_HEADER_HEIGHT_PX;
|
|
1110
|
+
const ROW_SIZE = NODE_ROW_HEIGHT_PX;
|
|
1111
|
+
const maxRows = Math.max(inputHandles.length, outputHandles.length);
|
|
1112
|
+
const initialWidth = opts.showValues ? 320 : 240;
|
|
1113
|
+
const initialHeight = HEADER_SIZE + maxRows * ROW_SIZE;
|
|
1114
|
+
// Precompute handle bounds so edges can render immediately without waiting for measurement
|
|
1115
|
+
const handles = [
|
|
1116
|
+
// Inputs on the left as targets
|
|
1117
|
+
...inputHandles.map((h, i) => ({
|
|
1118
|
+
id: h.id,
|
|
1119
|
+
type: "target",
|
|
1120
|
+
position: Position.Left,
|
|
1121
|
+
x: 0,
|
|
1122
|
+
y: HEADER_SIZE + i * ROW_SIZE,
|
|
1123
|
+
width: 1,
|
|
1124
|
+
height: ROW_SIZE + 2,
|
|
1125
|
+
})),
|
|
1126
|
+
// Outputs on the right as sources
|
|
1127
|
+
...outputHandles.map((h, i) => ({
|
|
1128
|
+
id: h.id,
|
|
1129
|
+
type: "source",
|
|
1130
|
+
position: Position.Right,
|
|
1131
|
+
x: initialWidth - 1,
|
|
1132
|
+
y: HEADER_SIZE + i * ROW_SIZE,
|
|
1133
|
+
width: 1,
|
|
1134
|
+
height: ROW_SIZE + 2,
|
|
1135
|
+
})),
|
|
1136
|
+
];
|
|
1015
1137
|
return {
|
|
1016
1138
|
id: n.nodeId,
|
|
1017
1139
|
data: {
|
|
@@ -1019,7 +1141,23 @@ function toReactFlow(def, positions, registry, opts) {
|
|
|
1019
1141
|
params: n.params,
|
|
1020
1142
|
inputHandles,
|
|
1021
1143
|
outputHandles,
|
|
1144
|
+
handleLayout: [
|
|
1145
|
+
...inputHandles.map((h, i) => ({
|
|
1146
|
+
id: h.id,
|
|
1147
|
+
type: "target",
|
|
1148
|
+
position: Position.Left,
|
|
1149
|
+
y: HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2,
|
|
1150
|
+
})),
|
|
1151
|
+
...outputHandles.map((h, i) => ({
|
|
1152
|
+
id: h.id,
|
|
1153
|
+
type: "source",
|
|
1154
|
+
position: Position.Right,
|
|
1155
|
+
y: HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2,
|
|
1156
|
+
})),
|
|
1157
|
+
],
|
|
1022
1158
|
showValues: opts.showValues,
|
|
1159
|
+
renderWidth: initialWidth,
|
|
1160
|
+
renderHeight: initialHeight,
|
|
1023
1161
|
inputValues: opts.inputs?.[n.nodeId],
|
|
1024
1162
|
outputValues: opts.outputs?.[n.nodeId],
|
|
1025
1163
|
status: opts.nodeStatus?.[n.nodeId],
|
|
@@ -1036,6 +1174,11 @@ function toReactFlow(def, positions, registry, opts) {
|
|
|
1036
1174
|
selected: opts.selectedNodeIds
|
|
1037
1175
|
? opts.selectedNodeIds.has(n.nodeId)
|
|
1038
1176
|
: undefined,
|
|
1177
|
+
initialWidth,
|
|
1178
|
+
initialHeight,
|
|
1179
|
+
handles,
|
|
1180
|
+
width: initialWidth,
|
|
1181
|
+
height: initialHeight,
|
|
1039
1182
|
};
|
|
1040
1183
|
});
|
|
1041
1184
|
const edges = def.edges
|
|
@@ -1052,9 +1195,9 @@ function toReactFlow(def, positions, registry, opts) {
|
|
|
1052
1195
|
const hasError = !!st?.lastError;
|
|
1053
1196
|
const isInvalidEdge = !!opts.edgeValidation?.[e.id];
|
|
1054
1197
|
const style = hasError || isInvalidEdge
|
|
1055
|
-
?
|
|
1198
|
+
? EDGE_STYLE_ERROR
|
|
1056
1199
|
: isRunning
|
|
1057
|
-
?
|
|
1200
|
+
? EDGE_STYLE_RUNNING
|
|
1058
1201
|
: undefined;
|
|
1059
1202
|
return {
|
|
1060
1203
|
id: e.id,
|
|
@@ -1082,17 +1225,35 @@ function getNodeBorderClassNames(args) {
|
|
|
1082
1225
|
const hasValidationWarning = !hasValidationError && issues.length > 0;
|
|
1083
1226
|
const isRunning = !!status.activeRuns;
|
|
1084
1227
|
const isInvalid = !!status.invalidated && !isRunning && !hasError;
|
|
1085
|
-
|
|
1228
|
+
// Keep border width constant to avoid layout reflow on selection toggles
|
|
1229
|
+
const borderWidth = "border";
|
|
1086
1230
|
const borderStyle = isInvalid ? "border-dashed" : "border-solid";
|
|
1087
|
-
const
|
|
1088
|
-
? "
|
|
1231
|
+
const severity = hasError || hasValidationError
|
|
1232
|
+
? "red"
|
|
1089
1233
|
: hasValidationWarning
|
|
1090
|
-
? "
|
|
1234
|
+
? "amber"
|
|
1091
1235
|
: isRunning
|
|
1092
|
-
? "
|
|
1093
|
-
: "
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1236
|
+
? "blue"
|
|
1237
|
+
: "gray";
|
|
1238
|
+
const borderBySeverity = {
|
|
1239
|
+
red: "border-red-500",
|
|
1240
|
+
amber: "border-amber-500",
|
|
1241
|
+
blue: "border-blue-500",
|
|
1242
|
+
gray: "border-gray-500 dark:border-gray-400",
|
|
1243
|
+
};
|
|
1244
|
+
const ringBySeverity = {
|
|
1245
|
+
red: "ring-2 ring-red-300 dark:ring-red-900",
|
|
1246
|
+
amber: "ring-2 ring-amber-300 dark:ring-amber-900",
|
|
1247
|
+
blue: "ring-2 ring-blue-200 dark:ring-blue-900",
|
|
1248
|
+
gray: "ring-2 ring-gray-300 dark:ring-gray-500",
|
|
1249
|
+
};
|
|
1250
|
+
const borderColor = borderBySeverity[severity];
|
|
1251
|
+
const ring = isRunning
|
|
1252
|
+
? ringBySeverity.blue
|
|
1253
|
+
: selected
|
|
1254
|
+
? ringBySeverity[severity === "blue" ? "gray" : severity]
|
|
1255
|
+
: "";
|
|
1256
|
+
return [borderWidth, borderStyle, borderColor, ring].join(" ").trim();
|
|
1096
1257
|
}
|
|
1097
1258
|
|
|
1098
1259
|
const WorkbenchContext = createContext(null);
|
|
@@ -1228,6 +1389,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1228
1389
|
});
|
|
1229
1390
|
wb.setPositions(pos);
|
|
1230
1391
|
}, [wb]);
|
|
1392
|
+
const updateEdgeType = useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
|
|
1231
1393
|
// Subscribe to runner/workbench events
|
|
1232
1394
|
useEffect(() => {
|
|
1233
1395
|
const add = (source, type) => (payload) => setEvents((prev) => {
|
|
@@ -1238,8 +1400,15 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1238
1400
|
if (changeType === "moveNode" || changeType === "moveNodes")
|
|
1239
1401
|
return prev;
|
|
1240
1402
|
}
|
|
1403
|
+
const nextNo = prev.length > 0 ? (prev[0]?.no ?? 0) + 1 : 1;
|
|
1241
1404
|
const next = [
|
|
1242
|
-
{
|
|
1405
|
+
{
|
|
1406
|
+
no: nextNo,
|
|
1407
|
+
at: Date.now(),
|
|
1408
|
+
source,
|
|
1409
|
+
type,
|
|
1410
|
+
payload: structuredClone(payload),
|
|
1411
|
+
},
|
|
1243
1412
|
...prev,
|
|
1244
1413
|
];
|
|
1245
1414
|
return next.length > 200 ? next.slice(0, 200) : next;
|
|
@@ -1554,6 +1723,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1554
1723
|
step,
|
|
1555
1724
|
flush,
|
|
1556
1725
|
runAutoLayout,
|
|
1726
|
+
updateEdgeType,
|
|
1557
1727
|
}), [
|
|
1558
1728
|
wb,
|
|
1559
1729
|
runner,
|
|
@@ -1580,6 +1750,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1580
1750
|
step,
|
|
1581
1751
|
flush,
|
|
1582
1752
|
runAutoLayout,
|
|
1753
|
+
wb,
|
|
1583
1754
|
]);
|
|
1584
1755
|
return (jsx(WorkbenchContext.Provider, { value: value, children: children }));
|
|
1585
1756
|
}
|
|
@@ -1615,7 +1786,7 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
|
|
|
1615
1786
|
return String(v);
|
|
1616
1787
|
}
|
|
1617
1788
|
};
|
|
1618
|
-
return (jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsx("div", { className: "font-semibold", children: "Events" }), jsxs("div", { className: "flex items-center gap-2", children: [jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsx("span", { children: "Hide workbench" })] }), jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsx("span", { children: "Auto scroll" })] }), jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev
|
|
1789
|
+
return (jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsx("div", { className: "font-semibold", children: "Events" }), jsxs("div", { className: "flex items-center gap-2", children: [jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsx("span", { children: "Hide workbench" })] }), jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsx("span", { children: "Auto scroll" })] }), jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev) => (jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxs("div", { className: "flex items-baseline gap-2", children: [jsx("span", { className: "w-12 shrink-0 text-right text-gray-500 select-none", children: ev.no }), jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsx("pre", { className: "m-0 whitespace-pre-wrap ml-12", children: renderPayload(ev.payload) })] }, `${ev.at}:${ev.no}`))) })] }));
|
|
1619
1790
|
}
|
|
1620
1791
|
|
|
1621
1792
|
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, contextPanel, setInput, }) {
|
|
@@ -1630,7 +1801,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1630
1801
|
return String(value ?? "");
|
|
1631
1802
|
}
|
|
1632
1803
|
};
|
|
1633
|
-
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, outputTypesMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
|
|
1804
|
+
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, outputTypesMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, } = useWorkbenchContext();
|
|
1634
1805
|
const nodeValidationIssues = validationByNode.issues;
|
|
1635
1806
|
const edgeValidationIssues = validationByEdge.issues;
|
|
1636
1807
|
const nodeValidationHandles = validationByNode;
|
|
@@ -1707,7 +1878,11 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1707
1878
|
setOriginals(nextOriginals);
|
|
1708
1879
|
}, [selectedNodeId, selectedDesc, valuesTick]);
|
|
1709
1880
|
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 &&
|
|
1881
|
+
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", { className: "flex items-center gap-2 mt-1", children: [jsxs("label", { className: "w-20 flex flex-col", children: [jsx("span", { children: "Type" }), jsx("span", { className: "text-gray-500 text-[11px]", children: "DataTypeId" })] }), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value: selectedEdge.typeId ?? "", onChange: (e) => {
|
|
1882
|
+
const v = e.target.value;
|
|
1883
|
+
const next = v === "" ? undefined : v;
|
|
1884
|
+
updateEdgeType(selectedEdge.id, next);
|
|
1885
|
+
}, children: [jsx("option", { value: "", children: "(infer from source)" }), Array.from(registry.types.keys()).map((tid) => (jsx("option", { value: tid, children: tid }, tid)))] })] })] }), 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
1886
|
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
1887
|
const typeId = getInputTypeId(selectedDesc?.inputs, h);
|
|
1713
1888
|
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
@@ -1768,73 +1943,114 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1768
1943
|
})()] }, 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
1944
|
}
|
|
1770
1945
|
|
|
1946
|
+
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", }) {
|
|
1947
|
+
const layout = data.handleLayout ?? [];
|
|
1948
|
+
const byId = React.useMemo(() => {
|
|
1949
|
+
const m = new Map();
|
|
1950
|
+
for (const h of layout) {
|
|
1951
|
+
m.set(h.id, { position: h.position, y: h.y, type: h.type });
|
|
1952
|
+
}
|
|
1953
|
+
return m;
|
|
1954
|
+
}, [layout]);
|
|
1955
|
+
return (jsxs(Fragment, { children: [(data.inputHandles ?? []).map((h) => {
|
|
1956
|
+
const placed = byId.get(h.id);
|
|
1957
|
+
const position = placed?.position ?? Position.Left;
|
|
1958
|
+
const y = placed?.y;
|
|
1959
|
+
const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ??
|
|
1960
|
+
inputClassName;
|
|
1961
|
+
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: {
|
|
1962
|
+
top: (y ?? 0) - 8,
|
|
1963
|
+
right: "50%",
|
|
1964
|
+
whiteSpace: "nowrap",
|
|
1965
|
+
overflow: "hidden",
|
|
1966
|
+
textOverflow: "ellipsis",
|
|
1967
|
+
}, children: renderLabel({ kind: "input", id: h.id }) }))] }, h.id));
|
|
1968
|
+
}), (data.outputHandles ?? []).map((h) => {
|
|
1969
|
+
const placed = byId.get(h.id);
|
|
1970
|
+
const position = placed?.position ?? Position.Right;
|
|
1971
|
+
const y = placed?.y;
|
|
1972
|
+
const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ??
|
|
1973
|
+
outputClassName;
|
|
1974
|
+
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: {
|
|
1975
|
+
top: (y ?? 0) - 8,
|
|
1976
|
+
left: "50%",
|
|
1977
|
+
textAlign: "right",
|
|
1978
|
+
whiteSpace: "nowrap",
|
|
1979
|
+
overflow: "hidden",
|
|
1980
|
+
textOverflow: "ellipsis",
|
|
1981
|
+
}, children: renderLabel({ kind: "output", id: h.id }) }))] }, h.id));
|
|
1982
|
+
})] }));
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1771
1985
|
const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
|
|
1986
|
+
const updateNodeInternals = useUpdateNodeInternals();
|
|
1772
1987
|
const { typeId, showValues, inputValues, outputValues, toString } = data;
|
|
1773
1988
|
const inputEntries = data.inputHandles ?? [];
|
|
1774
1989
|
const outputEntries = data.outputHandles ?? [];
|
|
1990
|
+
React.useEffect(() => {
|
|
1991
|
+
updateNodeInternals(id);
|
|
1992
|
+
}, [
|
|
1993
|
+
id,
|
|
1994
|
+
inputEntries.length,
|
|
1995
|
+
outputEntries.length,
|
|
1996
|
+
showValues,
|
|
1997
|
+
updateNodeInternals,
|
|
1998
|
+
]);
|
|
1775
1999
|
const status = data.status ?? { activeRuns: 0 };
|
|
1776
2000
|
const validation = data.validation ?? {
|
|
1777
2001
|
inputs: [],
|
|
1778
2002
|
outputs: [],
|
|
1779
2003
|
issues: [],
|
|
1780
2004
|
};
|
|
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
2005
|
const hasError = !!status.lastError;
|
|
1788
|
-
const hasValidationError = validation.issues.some((i) => i.level === "error");
|
|
1789
|
-
const hasValidationWarning = !hasValidationError && validation.issues.length > 0;
|
|
1790
2006
|
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);
|
|
2007
|
+
const containerBorder = getNodeBorderClassNames({
|
|
2008
|
+
selected,
|
|
2009
|
+
status,
|
|
2010
|
+
validation,
|
|
2011
|
+
});
|
|
1806
2012
|
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",
|
|
2013
|
+
return (jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900", containerBorder), style: {
|
|
2014
|
+
position: "relative",
|
|
2015
|
+
minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
|
|
2016
|
+
minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
|
|
2017
|
+
}, 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: {
|
|
2018
|
+
maxHeight: NODE_HEADER_HEIGHT_PX,
|
|
2019
|
+
minHeight: NODE_HEADER_HEIGHT_PX,
|
|
2020
|
+
}, 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
2021
|
? "error"
|
|
1809
2022
|
: "warning", size: 12, className: "w-3 h-3", title: validation.issues
|
|
1810
2023
|
.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
|
-
|
|
2024
|
+
.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 }) => {
|
|
2025
|
+
const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === id);
|
|
2026
|
+
const hasAny = vIssues.length > 0;
|
|
2027
|
+
const hasErr = vIssues.some((v) => v.level === "error");
|
|
2028
|
+
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"));
|
|
2029
|
+
}, renderLabel: ({ kind, id }) => {
|
|
2030
|
+
const entries = kind === "input" ? inputEntries : outputEntries;
|
|
2031
|
+
const entry = entries.find((e) => e.id === id);
|
|
2032
|
+
if (!entry)
|
|
2033
|
+
return id;
|
|
2034
|
+
const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === id);
|
|
2035
|
+
const hasAny = vIssues.length > 0;
|
|
2036
|
+
const hasErr = vIssues.some((v) => v.level === "error");
|
|
2037
|
+
const title = vIssues
|
|
2038
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
2039
|
+
.join("; ");
|
|
2040
|
+
// Compose label with truncated value to prevent layout growth
|
|
2041
|
+
const valueText = (() => {
|
|
2042
|
+
if (!showValues)
|
|
2043
|
+
return undefined;
|
|
2044
|
+
if (kind === "input") {
|
|
2045
|
+
const txt = toString(entry.typeId, inputValues?.[entry.id]);
|
|
2046
|
+
return typeof txt === "string" ? txt : String(txt);
|
|
2047
|
+
}
|
|
2048
|
+
const resolved = resolveOutputDisplay(outputValues?.[entry.id], entry.typeId);
|
|
2049
|
+
const txt = toString(resolved.typeId, resolved.value);
|
|
2050
|
+
return typeof txt === "string" ? txt : String(txt);
|
|
2051
|
+
})();
|
|
2052
|
+
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 }))] }));
|
|
2053
|
+
} })] }));
|
|
1838
2054
|
});
|
|
1839
2055
|
DefaultNode.displayName = "DefaultNode";
|
|
1840
2056
|
|
|
@@ -1999,6 +2215,58 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
1999
2215
|
const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
|
|
2000
2216
|
const nodeValidation = validationByNode;
|
|
2001
2217
|
const edgeValidation = validationByEdge.errors;
|
|
2218
|
+
// Keep stable references for nodes/edges to avoid unnecessary updates
|
|
2219
|
+
const prevNodesRef = useRef([]);
|
|
2220
|
+
const prevEdgesRef = useRef([]);
|
|
2221
|
+
function retainStabilityById(prev, next, isSame) {
|
|
2222
|
+
if (prev.length === 0)
|
|
2223
|
+
return next;
|
|
2224
|
+
const map = new Map();
|
|
2225
|
+
for (const p of prev)
|
|
2226
|
+
map.set(p.id, p);
|
|
2227
|
+
const out = new Array(next.length);
|
|
2228
|
+
for (let i = 0; i < next.length; i++) {
|
|
2229
|
+
const n = next[i];
|
|
2230
|
+
const p = map.get(n.id);
|
|
2231
|
+
out[i] = p && isSame(p, n) ? p : n;
|
|
2232
|
+
}
|
|
2233
|
+
return out;
|
|
2234
|
+
}
|
|
2235
|
+
const isSameNode = (a, b) => {
|
|
2236
|
+
// Compare the parts that affect rendering
|
|
2237
|
+
const pick = (n) => ({
|
|
2238
|
+
position: n.position,
|
|
2239
|
+
type: n.type,
|
|
2240
|
+
selected: n.selected,
|
|
2241
|
+
initialWidth: n.initialWidth,
|
|
2242
|
+
initialHeight: n.initialHeight,
|
|
2243
|
+
data: n.data && {
|
|
2244
|
+
typeId: n.data.typeId,
|
|
2245
|
+
inputHandles: n.data.inputHandles,
|
|
2246
|
+
outputHandles: n.data.outputHandles,
|
|
2247
|
+
showValues: n.data.showValues,
|
|
2248
|
+
inputValues: n.data.inputValues,
|
|
2249
|
+
outputValues: n.data.outputValues,
|
|
2250
|
+
status: n.data.status,
|
|
2251
|
+
validation: n.data.validation,
|
|
2252
|
+
},
|
|
2253
|
+
});
|
|
2254
|
+
return isEqual(pick(a), pick(b));
|
|
2255
|
+
};
|
|
2256
|
+
const isSameEdge = (a, b) => {
|
|
2257
|
+
const pick = (e) => ({
|
|
2258
|
+
source: e.source,
|
|
2259
|
+
target: e.target,
|
|
2260
|
+
sourceHandle: e.sourceHandle,
|
|
2261
|
+
targetHandle: e.targetHandle,
|
|
2262
|
+
selected: e.selected,
|
|
2263
|
+
animated: e.animated,
|
|
2264
|
+
style: e.style,
|
|
2265
|
+
label: e.label,
|
|
2266
|
+
type: e.type,
|
|
2267
|
+
});
|
|
2268
|
+
return isEqual(pick(a), pick(b));
|
|
2269
|
+
};
|
|
2002
2270
|
// Expose imperative API
|
|
2003
2271
|
const rfInstanceRef = useRef(null);
|
|
2004
2272
|
useImperativeHandle(ref, () => ({
|
|
@@ -2009,7 +2277,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2009
2277
|
catch { }
|
|
2010
2278
|
},
|
|
2011
2279
|
}));
|
|
2012
|
-
const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete,
|
|
2280
|
+
const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, } = useWorkbenchBridge(wb);
|
|
2013
2281
|
const { nodeTypes, resolveNodeType } = useMemo(() => {
|
|
2014
2282
|
// Build nodeTypes map using UI extension registry
|
|
2015
2283
|
const ui = wb.getUI();
|
|
@@ -2047,8 +2315,91 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2047
2315
|
selectedNodeIds: new Set(sel.nodes),
|
|
2048
2316
|
selectedEdgeIds: new Set(sel.edges),
|
|
2049
2317
|
});
|
|
2050
|
-
//
|
|
2051
|
-
|
|
2318
|
+
// Retain references for unchanged items
|
|
2319
|
+
const stableNodes = retainStabilityById(prevNodesRef.current, out.nodes, isSameNode);
|
|
2320
|
+
const stableEdges = retainStabilityById(prevEdgesRef.current, out.edges, isSameEdge);
|
|
2321
|
+
// Debug: log updates/additions/removals (use value equality, not reference)
|
|
2322
|
+
try {
|
|
2323
|
+
const prevNodeIds = new Set(prevNodesRef.current.map((n) => n.id));
|
|
2324
|
+
const nextNodeIds = new Set(out.nodes.map((n) => n.id));
|
|
2325
|
+
const addedNodeIds = out.nodes
|
|
2326
|
+
.filter((n) => !prevNodeIds.has(n.id))
|
|
2327
|
+
.map((n) => n.id);
|
|
2328
|
+
const removedNodeIds = prevNodesRef.current
|
|
2329
|
+
.filter((n) => !nextNodeIds.has(n.id))
|
|
2330
|
+
.map((n) => n.id);
|
|
2331
|
+
const prevNodeMap = new Map(prevNodesRef.current.map((n) => [n.id, n]));
|
|
2332
|
+
const changedNodeIds = out.nodes
|
|
2333
|
+
.filter((n) => {
|
|
2334
|
+
const p = prevNodeMap.get(n.id);
|
|
2335
|
+
return p ? !isSameNode(p, n) : false;
|
|
2336
|
+
})
|
|
2337
|
+
.map((n) => n.id);
|
|
2338
|
+
// Detect handle updates (ids/length changes) for targeted debug
|
|
2339
|
+
const toIds = (arr) => Array.isArray(arr) ? arr.map((h) => h?.id) : [];
|
|
2340
|
+
const handlesEqual = (a, b) => {
|
|
2341
|
+
const aIds = toIds(a);
|
|
2342
|
+
const bIds = toIds(b);
|
|
2343
|
+
if (aIds.length !== bIds.length)
|
|
2344
|
+
return false;
|
|
2345
|
+
for (let i = 0; i < aIds.length; i++) {
|
|
2346
|
+
if (aIds[i] !== bIds[i])
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
return true;
|
|
2350
|
+
};
|
|
2351
|
+
const handleChanged = out.nodes
|
|
2352
|
+
.filter((n) => {
|
|
2353
|
+
const p = prevNodeMap.get(n.id);
|
|
2354
|
+
if (!p)
|
|
2355
|
+
return false;
|
|
2356
|
+
const inChanged = !handlesEqual(p.data?.inputHandles, n.data?.inputHandles);
|
|
2357
|
+
const outChanged = !handlesEqual(p.data?.outputHandles, n.data?.outputHandles);
|
|
2358
|
+
return inChanged || outChanged;
|
|
2359
|
+
})
|
|
2360
|
+
.map((n) => n.id);
|
|
2361
|
+
const prevEdgeIds = new Set(prevEdgesRef.current.map((e) => e.id));
|
|
2362
|
+
const nextEdgeIds = new Set(out.edges.map((e) => e.id));
|
|
2363
|
+
const addedEdgeIds = out.edges
|
|
2364
|
+
.filter((e) => !prevEdgeIds.has(e.id))
|
|
2365
|
+
.map((e) => e.id);
|
|
2366
|
+
const removedEdgeIds = prevEdgesRef.current
|
|
2367
|
+
.filter((e) => !nextEdgeIds.has(e.id))
|
|
2368
|
+
.map((e) => e.id);
|
|
2369
|
+
const prevEdgeMap = new Map(prevEdgesRef.current.map((e) => [e.id, e]));
|
|
2370
|
+
const changedEdgeIds = out.edges
|
|
2371
|
+
.filter((e) => {
|
|
2372
|
+
const p = prevEdgeMap.get(e.id);
|
|
2373
|
+
return p ? !isSameEdge(p, e) : false;
|
|
2374
|
+
})
|
|
2375
|
+
.map((e) => e.id);
|
|
2376
|
+
if (addedNodeIds.length ||
|
|
2377
|
+
removedNodeIds.length ||
|
|
2378
|
+
changedNodeIds.length ||
|
|
2379
|
+
handleChanged.length) {
|
|
2380
|
+
// eslint-disable-next-line no-console
|
|
2381
|
+
console.debug("[WorkbenchCanvas] node updates", {
|
|
2382
|
+
added: addedNodeIds,
|
|
2383
|
+
removed: removedNodeIds,
|
|
2384
|
+
changed: changedNodeIds,
|
|
2385
|
+
handleChanged,
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
if (addedEdgeIds.length ||
|
|
2389
|
+
removedEdgeIds.length ||
|
|
2390
|
+
changedEdgeIds.length) {
|
|
2391
|
+
// eslint-disable-next-line no-console
|
|
2392
|
+
console.debug("[WorkbenchCanvas] edge updates", {
|
|
2393
|
+
added: addedEdgeIds,
|
|
2394
|
+
removed: removedEdgeIds,
|
|
2395
|
+
changed: changedEdgeIds,
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
catch { }
|
|
2400
|
+
prevNodesRef.current = stableNodes;
|
|
2401
|
+
prevEdgesRef.current = stableEdges;
|
|
2402
|
+
return { nodes: stableNodes, edges: stableEdges };
|
|
2052
2403
|
}, [
|
|
2053
2404
|
showValues,
|
|
2054
2405
|
inputsMap,
|
|
@@ -2060,9 +2411,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2060
2411
|
edgeStatus,
|
|
2061
2412
|
nodeValidation,
|
|
2062
2413
|
edgeValidation,
|
|
2063
|
-
nodeTypes,
|
|
2064
2414
|
resolveNodeType,
|
|
2065
2415
|
]);
|
|
2416
|
+
const throttled = useThrottledValue({ nodes, edges }, 100);
|
|
2066
2417
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
2067
2418
|
const [menuPos, setMenuPos] = useState(null);
|
|
2068
2419
|
const [nodeMenuOpen, setNodeMenuOpen] = useState(false);
|
|
@@ -2089,7 +2440,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
|
|
|
2089
2440
|
const addNodeAt = (typeId, pos) => {
|
|
2090
2441
|
wb.addNode({ typeId, position: pos });
|
|
2091
2442
|
};
|
|
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,
|
|
2443
|
+
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
2444
|
});
|
|
2094
2445
|
|
|
2095
2446
|
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 +2552,14 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2201
2552
|
if (!ex)
|
|
2202
2553
|
return;
|
|
2203
2554
|
const { registry: r, def } = await ex.load();
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2555
|
+
// Keep registry consistent with backend:
|
|
2556
|
+
// - For local backend, allow example to provide its own registry
|
|
2557
|
+
// - For remote backend, NEVER overwrite the hydrated remote registry
|
|
2558
|
+
if (backendKind === "local") {
|
|
2559
|
+
if (r) {
|
|
2560
|
+
setRegistry(r);
|
|
2561
|
+
wb.setRegistry(r);
|
|
2562
|
+
}
|
|
2207
2563
|
}
|
|
2208
2564
|
await wb.load(def);
|
|
2209
2565
|
// Build a local runtime so seeded defaults are visible pre-run
|
|
@@ -2211,7 +2567,15 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2211
2567
|
runAutoLayout();
|
|
2212
2568
|
setExampleState(key);
|
|
2213
2569
|
onExampleChange?.(key);
|
|
2214
|
-
}, [
|
|
2570
|
+
}, [
|
|
2571
|
+
runner,
|
|
2572
|
+
wb,
|
|
2573
|
+
onExampleChange,
|
|
2574
|
+
runAutoLayout,
|
|
2575
|
+
examples,
|
|
2576
|
+
setRegistry,
|
|
2577
|
+
backendKind,
|
|
2578
|
+
]);
|
|
2215
2579
|
const downloadGraph = useCallback(() => {
|
|
2216
2580
|
try {
|
|
2217
2581
|
const def = wb.export();
|
|
@@ -2517,7 +2881,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2517
2881
|
? "Stop engine before switching example"
|
|
2518
2882
|
: 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
2883
|
? "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) => {
|
|
2884
|
+
: 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
2885
|
const kind = e.target.value || undefined;
|
|
2522
2886
|
onEngineChange?.(kind);
|
|
2523
2887
|
}, 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 +2924,5 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
2560
2924
|
}, 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
2925
|
}
|
|
2562
2926
|
|
|
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 };
|
|
2927
|
+
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
2928
|
//# sourceMappingURL=index.js.map
|