@bian-womp/spark-workbench 0.1.0
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 +1748 -0
- package/lib/cjs/index.cjs.map +1 -0
- package/lib/cjs/src/adapters/cli/index.d.ts +22 -0
- package/lib/cjs/src/adapters/cli/index.d.ts.map +1 -0
- package/lib/cjs/src/adapters/react-flow/index.d.ts +31 -0
- package/lib/cjs/src/adapters/react-flow/index.d.ts.map +1 -0
- package/lib/cjs/src/core/AbstractWorkbench.d.ts +35 -0
- package/lib/cjs/src/core/AbstractWorkbench.d.ts.map +1 -0
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts +54 -0
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -0
- package/lib/cjs/src/core/contracts.d.ts +107 -0
- package/lib/cjs/src/core/contracts.d.ts.map +1 -0
- package/lib/cjs/src/core/ui-extensions.d.ts +59 -0
- package/lib/cjs/src/core/ui-extensions.d.ts.map +1 -0
- package/lib/cjs/src/examples/cli.d.ts +2 -0
- package/lib/cjs/src/examples/cli.d.ts.map +1 -0
- package/lib/cjs/src/examples/reactflow/App.d.ts +2 -0
- package/lib/cjs/src/examples/reactflow/App.d.ts.map +1 -0
- package/lib/cjs/src/examples/reactflow/WorkbenchStudio.d.ts +21 -0
- package/lib/cjs/src/examples/reactflow/WorkbenchStudio.d.ts.map +1 -0
- package/lib/cjs/src/index.d.ts +9 -0
- package/lib/cjs/src/index.d.ts.map +1 -0
- package/lib/cjs/src/misc/DebugEvents.d.ts +7 -0
- package/lib/cjs/src/misc/DebugEvents.d.ts.map +1 -0
- package/lib/cjs/src/misc/DefaultContextMenu.d.ts +13 -0
- package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +1 -0
- package/lib/cjs/src/misc/DefaultNode.d.ts +4 -0
- package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -0
- package/lib/cjs/src/misc/Inspector.d.ts +10 -0
- package/lib/cjs/src/misc/Inspector.d.ts.map +1 -0
- package/lib/cjs/src/misc/IssueBadge.d.ts +7 -0
- package/lib/cjs/src/misc/IssueBadge.d.ts.map +1 -0
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts +6 -0
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -0
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +45 -0
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -0
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts +12 -0
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -0
- package/lib/cjs/src/misc/hooks.d.ts +17 -0
- package/lib/cjs/src/misc/hooks.d.ts.map +1 -0
- package/lib/cjs/src/misc/mapping.d.ts +47 -0
- package/lib/cjs/src/misc/mapping.d.ts.map +1 -0
- package/lib/cjs/src/runtime/GraphRunner.d.ts +61 -0
- package/lib/cjs/src/runtime/GraphRunner.d.ts.map +1 -0
- package/lib/esm/index.js +1740 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/src/adapters/cli/index.d.ts +22 -0
- package/lib/esm/src/adapters/cli/index.d.ts.map +1 -0
- package/lib/esm/src/adapters/react-flow/index.d.ts +31 -0
- package/lib/esm/src/adapters/react-flow/index.d.ts.map +1 -0
- package/lib/esm/src/core/AbstractWorkbench.d.ts +35 -0
- package/lib/esm/src/core/AbstractWorkbench.d.ts.map +1 -0
- package/lib/esm/src/core/InMemoryWorkbench.d.ts +54 -0
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -0
- package/lib/esm/src/core/contracts.d.ts +107 -0
- package/lib/esm/src/core/contracts.d.ts.map +1 -0
- package/lib/esm/src/core/ui-extensions.d.ts +59 -0
- package/lib/esm/src/core/ui-extensions.d.ts.map +1 -0
- package/lib/esm/src/examples/cli.d.ts +2 -0
- package/lib/esm/src/examples/cli.d.ts.map +1 -0
- package/lib/esm/src/examples/reactflow/App.d.ts +2 -0
- package/lib/esm/src/examples/reactflow/App.d.ts.map +1 -0
- package/lib/esm/src/examples/reactflow/WorkbenchStudio.d.ts +21 -0
- package/lib/esm/src/examples/reactflow/WorkbenchStudio.d.ts.map +1 -0
- package/lib/esm/src/index.d.ts +9 -0
- package/lib/esm/src/index.d.ts.map +1 -0
- package/lib/esm/src/misc/DebugEvents.d.ts +7 -0
- package/lib/esm/src/misc/DebugEvents.d.ts.map +1 -0
- package/lib/esm/src/misc/DefaultContextMenu.d.ts +13 -0
- package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +1 -0
- package/lib/esm/src/misc/DefaultNode.d.ts +4 -0
- package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -0
- package/lib/esm/src/misc/Inspector.d.ts +10 -0
- package/lib/esm/src/misc/Inspector.d.ts.map +1 -0
- package/lib/esm/src/misc/IssueBadge.d.ts +7 -0
- package/lib/esm/src/misc/IssueBadge.d.ts.map +1 -0
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts +6 -0
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -0
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts +45 -0
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -0
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts +12 -0
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -0
- package/lib/esm/src/misc/hooks.d.ts +17 -0
- package/lib/esm/src/misc/hooks.d.ts.map +1 -0
- package/lib/esm/src/misc/mapping.d.ts +47 -0
- package/lib/esm/src/misc/mapping.d.ts.map +1 -0
- package/lib/esm/src/runtime/GraphRunner.d.ts +61 -0
- package/lib/esm/src/runtime/GraphRunner.d.ts.map +1 -0
- package/package.json +65 -0
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1,1740 @@
|
|
|
1
|
+
import { GraphBuilder, StepEngine, HybridEngine, PullEngine, BatchedEngine, PushEngine, createSimpleGraphRegistry, createSimpleGraphDef, createValidationGraphRegistry, createValidationGraphDef, createProgressGraphRegistry, createProgressGraphDef, createAsyncGraphRegistry, createAsyncGraphDef } from '@bian-womp/spark-graph';
|
|
2
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import React, { createContext, useContext, useMemo, useState, useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import ReactFlow, { Handle, Position, useReactFlow, Background, MiniMap, Controls } from 'reactflow';
|
|
5
|
+
import 'reactflow/dist/style.css';
|
|
6
|
+
import cx from 'classnames';
|
|
7
|
+
import { XCircleIcon, WarningCircleIcon } from '@phosphor-icons/react';
|
|
8
|
+
import { HttpPollingTransport, WebSocketTransport, RemoteRunner } from '@bian-womp/spark-remote';
|
|
9
|
+
|
|
10
|
+
class DefaultUIExtensionRegistry {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.nodeRenderers = new Map();
|
|
13
|
+
this.portRenderers = new Map();
|
|
14
|
+
this.edgeRenderers = new Map();
|
|
15
|
+
}
|
|
16
|
+
registerNodeRenderer(nodeTypeId, renderer) {
|
|
17
|
+
this.nodeRenderers.set(nodeTypeId, renderer);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
getNodeRenderer(nodeTypeId) {
|
|
21
|
+
return this.nodeRenderers.get(nodeTypeId);
|
|
22
|
+
}
|
|
23
|
+
registerPortRenderer(dataTypeId, renderer) {
|
|
24
|
+
this.portRenderers.set(dataTypeId, renderer);
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
getPortRenderer(dataTypeId) {
|
|
28
|
+
return this.portRenderers.get(dataTypeId);
|
|
29
|
+
}
|
|
30
|
+
registerEdgeRenderer(typeId, renderer) {
|
|
31
|
+
this.edgeRenderers.set(typeId, renderer);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
getEdgeRenderer(typeId) {
|
|
35
|
+
return this.edgeRenderers.get(typeId);
|
|
36
|
+
}
|
|
37
|
+
setInspector(renderer) {
|
|
38
|
+
this.inspector = renderer;
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
getInspector() {
|
|
42
|
+
return this.inspector;
|
|
43
|
+
}
|
|
44
|
+
setPalette(renderer) {
|
|
45
|
+
this.palette = renderer;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
getPalette() {
|
|
49
|
+
return this.palette;
|
|
50
|
+
}
|
|
51
|
+
setToolbar(renderer) {
|
|
52
|
+
this.toolbar = renderer;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
getToolbar() {
|
|
56
|
+
return this.toolbar;
|
|
57
|
+
}
|
|
58
|
+
setContextMenu(renderer) {
|
|
59
|
+
this.contextMenu = renderer;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
getContextMenu() {
|
|
63
|
+
return this.contextMenu;
|
|
64
|
+
}
|
|
65
|
+
setMiniMap(renderer) {
|
|
66
|
+
this.miniMap = renderer;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
getMiniMap() {
|
|
70
|
+
return this.miniMap;
|
|
71
|
+
}
|
|
72
|
+
setIconProvider(provider) {
|
|
73
|
+
this.iconProvider = provider;
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
getIconProvider() {
|
|
77
|
+
return this.iconProvider;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class AbstractWorkbench {
|
|
82
|
+
constructor(args) {
|
|
83
|
+
this.ui = args.ui;
|
|
84
|
+
this.layout = args.layout;
|
|
85
|
+
this.storage = args.storage;
|
|
86
|
+
this.serializer = args.serializer;
|
|
87
|
+
}
|
|
88
|
+
// Expose UI registry to adapters (React Flow, CLI) to allow overrides
|
|
89
|
+
getUI() {
|
|
90
|
+
return this.ui;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class InMemoryWorkbench extends AbstractWorkbench {
|
|
95
|
+
constructor() {
|
|
96
|
+
super(...arguments);
|
|
97
|
+
this.def = { nodes: [], edges: [] };
|
|
98
|
+
this.positions = {};
|
|
99
|
+
this.listeners = new Map();
|
|
100
|
+
this.selection = {
|
|
101
|
+
nodes: [],
|
|
102
|
+
edges: [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
setRegistry(registry) {
|
|
106
|
+
this.registry = registry;
|
|
107
|
+
}
|
|
108
|
+
async load(def) {
|
|
109
|
+
this.def = { nodes: [...def.nodes], edges: [...def.edges] };
|
|
110
|
+
if (this.layout) {
|
|
111
|
+
const { positions } = await this.layout.layout(this.def);
|
|
112
|
+
this.positions = positions;
|
|
113
|
+
}
|
|
114
|
+
this.emit("graphChanged", { def: this.def });
|
|
115
|
+
this.refreshValidation();
|
|
116
|
+
}
|
|
117
|
+
export() {
|
|
118
|
+
return this.def;
|
|
119
|
+
}
|
|
120
|
+
refreshValidation() {
|
|
121
|
+
this.emit("validationChanged", this.validate());
|
|
122
|
+
}
|
|
123
|
+
validate() {
|
|
124
|
+
if (this.registry) {
|
|
125
|
+
const builder = new GraphBuilder(this.registry);
|
|
126
|
+
const report = builder.validate(this.def);
|
|
127
|
+
return report;
|
|
128
|
+
}
|
|
129
|
+
const issues = [];
|
|
130
|
+
const nodeIds = new Set();
|
|
131
|
+
for (const n of this.def.nodes) {
|
|
132
|
+
if (nodeIds.has(n.nodeId)) {
|
|
133
|
+
issues.push({
|
|
134
|
+
level: "error",
|
|
135
|
+
code: "NODE_ID_DUP",
|
|
136
|
+
message: `Duplicate nodeId ${n.nodeId}`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else
|
|
140
|
+
nodeIds.add(n.nodeId);
|
|
141
|
+
}
|
|
142
|
+
const edgeIds = new Set();
|
|
143
|
+
for (const e of this.def.edges) {
|
|
144
|
+
if (edgeIds.has(e.id)) {
|
|
145
|
+
issues.push({
|
|
146
|
+
level: "error",
|
|
147
|
+
code: "EDGE_ID_DUP",
|
|
148
|
+
message: `Duplicate edge id ${e.id}`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
else
|
|
152
|
+
edgeIds.add(e.id);
|
|
153
|
+
}
|
|
154
|
+
return { ok: issues.every((i) => i.level !== "error"), issues };
|
|
155
|
+
}
|
|
156
|
+
addNode(node) {
|
|
157
|
+
const id = node.nodeId ?? this.generateId("n");
|
|
158
|
+
this.def.nodes.push({
|
|
159
|
+
nodeId: id,
|
|
160
|
+
typeId: node.typeId,
|
|
161
|
+
params: node.params,
|
|
162
|
+
});
|
|
163
|
+
if (node.position)
|
|
164
|
+
this.positions[id] = node.position;
|
|
165
|
+
this.emit("graphChanged", {
|
|
166
|
+
def: this.def,
|
|
167
|
+
change: { type: "addNode", nodeId: id },
|
|
168
|
+
});
|
|
169
|
+
this.refreshValidation();
|
|
170
|
+
}
|
|
171
|
+
removeNode(nodeId) {
|
|
172
|
+
this.def.nodes = this.def.nodes.filter((n) => n.nodeId !== nodeId);
|
|
173
|
+
this.def.edges = this.def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
|
|
174
|
+
delete this.positions[nodeId];
|
|
175
|
+
this.emit("graphChanged", {
|
|
176
|
+
def: this.def,
|
|
177
|
+
change: { type: "removeNode", nodeId },
|
|
178
|
+
});
|
|
179
|
+
this.refreshValidation();
|
|
180
|
+
}
|
|
181
|
+
connect(edge) {
|
|
182
|
+
const id = edge.id ?? this.generateId("e");
|
|
183
|
+
this.def.edges.push({
|
|
184
|
+
id,
|
|
185
|
+
source: { ...edge.source },
|
|
186
|
+
target: { ...edge.target },
|
|
187
|
+
typeId: edge.typeId,
|
|
188
|
+
});
|
|
189
|
+
this.emit("graphChanged", {
|
|
190
|
+
def: this.def,
|
|
191
|
+
change: { type: "connect", edgeId: id },
|
|
192
|
+
});
|
|
193
|
+
this.refreshValidation();
|
|
194
|
+
}
|
|
195
|
+
disconnect(edgeId) {
|
|
196
|
+
this.def.edges = this.def.edges.filter((e) => e.id !== edgeId);
|
|
197
|
+
this.emit("graphChanged", {
|
|
198
|
+
def: this.def,
|
|
199
|
+
change: { type: "disconnect", edgeId },
|
|
200
|
+
});
|
|
201
|
+
this.emit("validationChanged", this.validate());
|
|
202
|
+
}
|
|
203
|
+
updateParams(nodeId, params) {
|
|
204
|
+
const n = this.def.nodes.find((n) => n.nodeId === nodeId);
|
|
205
|
+
if (!n)
|
|
206
|
+
return;
|
|
207
|
+
n.params = { ...(n.params ?? {}), ...params };
|
|
208
|
+
this.emit("graphChanged", {
|
|
209
|
+
def: this.def,
|
|
210
|
+
change: { type: "updateParams", nodeId },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// Position and selection APIs for React Flow bridge
|
|
214
|
+
setPosition(nodeId, pos) {
|
|
215
|
+
this.positions[nodeId] = pos;
|
|
216
|
+
this.emit("graphUiChanged", {
|
|
217
|
+
def: this.def,
|
|
218
|
+
change: { type: "moveNode", nodeId, pos },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
setPositions(map) {
|
|
222
|
+
this.positions = { ...map };
|
|
223
|
+
this.emit("graphUiChanged", {
|
|
224
|
+
def: this.def,
|
|
225
|
+
change: { type: "moveNodes" },
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
getPositions() {
|
|
229
|
+
return { ...this.positions };
|
|
230
|
+
}
|
|
231
|
+
setSelection(sel) {
|
|
232
|
+
this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
|
|
233
|
+
this.emit("selectionChanged", this.selection);
|
|
234
|
+
}
|
|
235
|
+
getSelection() {
|
|
236
|
+
return {
|
|
237
|
+
nodes: [...this.selection.nodes],
|
|
238
|
+
edges: [...this.selection.edges],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
toggleNodeSelection(nodeId) {
|
|
242
|
+
this.selection.nodes = this.selection.nodes.includes(nodeId)
|
|
243
|
+
? this.selection.nodes.filter((id) => id !== nodeId)
|
|
244
|
+
: [...this.selection.nodes, nodeId];
|
|
245
|
+
this.emit("selectionChanged", this.selection);
|
|
246
|
+
}
|
|
247
|
+
toggleEdgeSelection(edgeId) {
|
|
248
|
+
this.selection.edges = this.selection.edges.includes(edgeId)
|
|
249
|
+
? this.selection.edges.filter((id) => id !== edgeId)
|
|
250
|
+
: [...this.selection.edges, edgeId];
|
|
251
|
+
this.emit("selectionChanged", this.selection);
|
|
252
|
+
}
|
|
253
|
+
on(event, handler) {
|
|
254
|
+
if (!this.listeners.has(event))
|
|
255
|
+
this.listeners.set(event, new Set());
|
|
256
|
+
const set = this.listeners.get(event);
|
|
257
|
+
set.add(handler);
|
|
258
|
+
return () => set.delete(handler);
|
|
259
|
+
}
|
|
260
|
+
emit(event, payload) {
|
|
261
|
+
const set = this.listeners.get(event);
|
|
262
|
+
if (set)
|
|
263
|
+
for (const h of Array.from(set))
|
|
264
|
+
h(payload);
|
|
265
|
+
}
|
|
266
|
+
generateId(prefix) {
|
|
267
|
+
return `${prefix}${Math.random().toString(36).slice(2, 8)}`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
class CLIWorkbench {
|
|
272
|
+
constructor(wb, deps = {}) {
|
|
273
|
+
this.wb = wb;
|
|
274
|
+
this.deps = deps;
|
|
275
|
+
}
|
|
276
|
+
async load(def) {
|
|
277
|
+
await this.wb.load(def);
|
|
278
|
+
}
|
|
279
|
+
print(def, options) {
|
|
280
|
+
const d = def ?? this.wb.export();
|
|
281
|
+
const detail = !!options?.detail;
|
|
282
|
+
const lines = [];
|
|
283
|
+
lines.push(`Nodes (${d.nodes.length})`);
|
|
284
|
+
for (const n of d.nodes) {
|
|
285
|
+
if (!detail) {
|
|
286
|
+
lines.push(` - ${n.nodeId}: ${n.typeId}`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const reg = this.wb["registry"];
|
|
290
|
+
const desc = reg?.nodes?.get(n.typeId);
|
|
291
|
+
const inputs = Object.entries(desc?.inputs ?? {})
|
|
292
|
+
.map(([h, t]) => `${h}:${t}`)
|
|
293
|
+
.join(", ");
|
|
294
|
+
const outputs = Object.entries(desc?.outputs ?? {})
|
|
295
|
+
.map(([h, t]) => `${h}:${t}`)
|
|
296
|
+
.join(", ");
|
|
297
|
+
const params = n.params ? JSON.stringify(n.params) : "{}";
|
|
298
|
+
lines.push(` - ${n.nodeId}: ${n.typeId}`);
|
|
299
|
+
lines.push(` inputs: ${inputs || "-"}`);
|
|
300
|
+
lines.push(` outputs: ${outputs || "-"}`);
|
|
301
|
+
lines.push(` params: ${params}`);
|
|
302
|
+
const inVals = options?.values?.inputs?.[n.nodeId];
|
|
303
|
+
const outVals = options?.values?.outputs?.[n.nodeId];
|
|
304
|
+
if (inVals && Object.keys(inVals).length > 0)
|
|
305
|
+
lines.push(` inputValues: ${JSON.stringify(inVals)}`);
|
|
306
|
+
if (outVals && Object.keys(outVals).length > 0)
|
|
307
|
+
lines.push(` outputValues: ${JSON.stringify(outVals)}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
lines.push(`Edges (${d.edges.length})`);
|
|
311
|
+
for (const e of d.edges) {
|
|
312
|
+
lines.push(` - ${e.id}: ${e.source.nodeId}.${e.source.handle} -> ${e.target.nodeId}.${e.target.handle} (${e.typeId})`);
|
|
313
|
+
}
|
|
314
|
+
return lines.join("\n");
|
|
315
|
+
}
|
|
316
|
+
get actions() {
|
|
317
|
+
return this.wb;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function toReactFlow$1(def, positions = {}) {
|
|
322
|
+
const nodes = def.nodes.map((n) => ({
|
|
323
|
+
id: n.nodeId,
|
|
324
|
+
data: { typeId: n.typeId, params: n.params },
|
|
325
|
+
position: positions[n.nodeId] ?? { x: 0, y: 0 },
|
|
326
|
+
}));
|
|
327
|
+
const edges = def.edges.map((e) => ({
|
|
328
|
+
id: e.id,
|
|
329
|
+
source: e.source.nodeId,
|
|
330
|
+
target: e.target.nodeId,
|
|
331
|
+
sourceHandle: e.source.handle,
|
|
332
|
+
targetHandle: e.target.handle,
|
|
333
|
+
}));
|
|
334
|
+
return { nodes, edges };
|
|
335
|
+
}
|
|
336
|
+
class ReactFlowWorkbench {
|
|
337
|
+
constructor(wb) {
|
|
338
|
+
this.wb = wb;
|
|
339
|
+
}
|
|
340
|
+
get actions() {
|
|
341
|
+
return this.wb;
|
|
342
|
+
}
|
|
343
|
+
async load(def) {
|
|
344
|
+
await this.wb.load(def);
|
|
345
|
+
}
|
|
346
|
+
export(def) {
|
|
347
|
+
const d = def ?? this.wb.export();
|
|
348
|
+
return toReactFlow$1(d);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const WorkbenchContext = createContext(null);
|
|
353
|
+
function useWorkbenchContext() {
|
|
354
|
+
const ctx = useContext(WorkbenchContext);
|
|
355
|
+
if (!ctx)
|
|
356
|
+
throw new Error("useWorkbenchContext must be used within WorkbenchProvider");
|
|
357
|
+
return ctx;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function useWorkbenchBridge(wb) {
|
|
361
|
+
const onConnect = useCallback((params) => {
|
|
362
|
+
if (!params.source || !params.target)
|
|
363
|
+
return;
|
|
364
|
+
if (!params.sourceHandle || !params.targetHandle)
|
|
365
|
+
return;
|
|
366
|
+
wb.connect({
|
|
367
|
+
source: { nodeId: params.source, handle: params.sourceHandle },
|
|
368
|
+
target: { nodeId: params.target, handle: params.targetHandle },
|
|
369
|
+
});
|
|
370
|
+
}, [wb]);
|
|
371
|
+
const onNodesChange = useCallback((changes) => {
|
|
372
|
+
changes.forEach((c) => {
|
|
373
|
+
if (c.type === "position" && c.position)
|
|
374
|
+
wb.setPosition(c.id, c.position);
|
|
375
|
+
if (c.type === "remove")
|
|
376
|
+
wb.removeNode(c.id);
|
|
377
|
+
if (c.type === "select")
|
|
378
|
+
wb.toggleNodeSelection(c.id);
|
|
379
|
+
});
|
|
380
|
+
}, [wb]);
|
|
381
|
+
const onEdgesDelete = useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
|
|
382
|
+
const onEdgesChange = useCallback((changes) => {
|
|
383
|
+
changes.forEach((c) => {
|
|
384
|
+
if (c.type === "remove")
|
|
385
|
+
wb.disconnect(c.id);
|
|
386
|
+
else if (c.type === "select")
|
|
387
|
+
wb.toggleEdgeSelection(c.id);
|
|
388
|
+
});
|
|
389
|
+
}, [wb]);
|
|
390
|
+
const onNodesDelete = useCallback((nodes) => {
|
|
391
|
+
for (const n of nodes)
|
|
392
|
+
wb.removeNode(n.id);
|
|
393
|
+
}, [wb]);
|
|
394
|
+
const onSelectionChange = useCallback((sel) => {
|
|
395
|
+
const next = {
|
|
396
|
+
nodes: sel.nodes.map((n) => n.id),
|
|
397
|
+
edges: sel.edges.map((e) => e.id),
|
|
398
|
+
};
|
|
399
|
+
const cur = wb.getSelection();
|
|
400
|
+
const sameLen = cur.nodes.length === next.nodes.length &&
|
|
401
|
+
cur.edges.length === next.edges.length;
|
|
402
|
+
const same = sameLen &&
|
|
403
|
+
cur.nodes.every((id, i) => id === next.nodes[i]) &&
|
|
404
|
+
cur.edges.every((id, i) => id === next.edges[i]);
|
|
405
|
+
if (!same)
|
|
406
|
+
wb.setSelection(next);
|
|
407
|
+
}, [wb]);
|
|
408
|
+
return {
|
|
409
|
+
onConnect,
|
|
410
|
+
onNodesChange,
|
|
411
|
+
onEdgesChange,
|
|
412
|
+
onEdgesDelete,
|
|
413
|
+
onNodesDelete,
|
|
414
|
+
onSelectionChange,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function useWorkbenchGraphTick(wb) {
|
|
418
|
+
const [tick, setTick] = useState(0);
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
const bump = () => setTick((t) => t + 1);
|
|
421
|
+
const off = wb.on("graphChanged", bump);
|
|
422
|
+
return () => off();
|
|
423
|
+
}, [wb]);
|
|
424
|
+
return tick;
|
|
425
|
+
}
|
|
426
|
+
function useWorkbenchGraphUiTick(wb) {
|
|
427
|
+
const [tick, setTick] = useState(0);
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
const bump = () => setTick((t) => t + 1);
|
|
430
|
+
const off = wb.on("graphUiChanged", bump);
|
|
431
|
+
return () => off();
|
|
432
|
+
}, [wb]);
|
|
433
|
+
return tick;
|
|
434
|
+
}
|
|
435
|
+
function useWorkbenchVersionTick(runner) {
|
|
436
|
+
const [version, setVersion] = useState(0);
|
|
437
|
+
useEffect(() => {
|
|
438
|
+
const bump = () => setVersion((v) => v + 1);
|
|
439
|
+
const u1 = runner.on("value", bump);
|
|
440
|
+
const u2 = runner.on("error", bump);
|
|
441
|
+
const u3 = runner.on("invalidate", bump);
|
|
442
|
+
const u4 = runner.on("status", bump);
|
|
443
|
+
const u5 = runner.on("stats", bump);
|
|
444
|
+
return () => {
|
|
445
|
+
u1();
|
|
446
|
+
u2();
|
|
447
|
+
u3();
|
|
448
|
+
u4();
|
|
449
|
+
u5();
|
|
450
|
+
};
|
|
451
|
+
}, [runner]);
|
|
452
|
+
return version;
|
|
453
|
+
}
|
|
454
|
+
// Query param helpers
|
|
455
|
+
function setSearchParam(key, val) {
|
|
456
|
+
if (typeof window === "undefined")
|
|
457
|
+
return;
|
|
458
|
+
const url = new URL(window.location.href);
|
|
459
|
+
if (val === undefined || val === "")
|
|
460
|
+
url.searchParams.delete(key);
|
|
461
|
+
else
|
|
462
|
+
url.searchParams.set(key, val);
|
|
463
|
+
window.history.replaceState({}, "", url.toString());
|
|
464
|
+
}
|
|
465
|
+
function useQueryParamBoolean(key, defaultValue) {
|
|
466
|
+
const initial = useMemo(() => {
|
|
467
|
+
if (typeof window === "undefined")
|
|
468
|
+
return defaultValue;
|
|
469
|
+
const sp = new URLSearchParams(window.location.search);
|
|
470
|
+
const v = sp.get(key);
|
|
471
|
+
if (v === null)
|
|
472
|
+
return defaultValue;
|
|
473
|
+
return v === "1" || v === "true";
|
|
474
|
+
}, [key, defaultValue]);
|
|
475
|
+
const [val, setVal] = useState(initial);
|
|
476
|
+
const set = useCallback((v) => {
|
|
477
|
+
setVal(v);
|
|
478
|
+
setSearchParam(key, v ? "1" : undefined);
|
|
479
|
+
}, [key]);
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
const onPop = () => {
|
|
482
|
+
const sp = new URLSearchParams(window.location.search);
|
|
483
|
+
const v = sp.get(key);
|
|
484
|
+
setVal(v === "1" || v === "true");
|
|
485
|
+
};
|
|
486
|
+
window.addEventListener("popstate", onPop);
|
|
487
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
488
|
+
}, [key]);
|
|
489
|
+
return [val, set];
|
|
490
|
+
}
|
|
491
|
+
function useQueryParamString(key, defaultValue) {
|
|
492
|
+
const initial = useMemo(() => {
|
|
493
|
+
if (typeof window === "undefined")
|
|
494
|
+
return defaultValue;
|
|
495
|
+
const sp = new URLSearchParams(window.location.search);
|
|
496
|
+
const v = sp.get(key);
|
|
497
|
+
return v ?? defaultValue;
|
|
498
|
+
}, [key, defaultValue]);
|
|
499
|
+
const [val, setVal] = useState(initial);
|
|
500
|
+
const set = useCallback((v) => {
|
|
501
|
+
setVal(v);
|
|
502
|
+
setSearchParam(key, v);
|
|
503
|
+
}, [key]);
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
const onPop = () => {
|
|
506
|
+
const sp = new URLSearchParams(window.location.search);
|
|
507
|
+
const v = sp.get(key) ?? undefined;
|
|
508
|
+
setVal(v);
|
|
509
|
+
};
|
|
510
|
+
window.addEventListener("popstate", onPop);
|
|
511
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
512
|
+
}, [key]);
|
|
513
|
+
return [val, set];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
517
|
+
const [nodeStatus, setNodeStatus] = useState({});
|
|
518
|
+
const [edgeStatus, setEdgeStatus] = useState({});
|
|
519
|
+
const [events, setEvents] = useState([]);
|
|
520
|
+
const clearEvents = useCallback(() => setEvents([]), []);
|
|
521
|
+
// Validation
|
|
522
|
+
const [validation, setValidation] = useState(undefined);
|
|
523
|
+
// Selection (mirror workbench selectionChanged)
|
|
524
|
+
const [selectedNodeId, setSelectedNodeId] = useState();
|
|
525
|
+
const [selectedEdgeId, setSelectedEdgeId] = useState();
|
|
526
|
+
const setSelection = useCallback((sel) => wb.setSelection(sel), [wb]);
|
|
527
|
+
// Ticks
|
|
528
|
+
const graphTick = useWorkbenchGraphTick(wb);
|
|
529
|
+
const graphUiTick = useWorkbenchGraphUiTick(wb);
|
|
530
|
+
const versionTick = useWorkbenchVersionTick(runner);
|
|
531
|
+
const valuesTick = versionTick + graphTick + graphUiTick;
|
|
532
|
+
// Def and IO values
|
|
533
|
+
const def = wb.export();
|
|
534
|
+
const inputsMap = useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
|
|
535
|
+
const outputsMap = useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
|
|
536
|
+
// Auto layout (simple layered layout)
|
|
537
|
+
const runAutoLayout = useCallback(() => {
|
|
538
|
+
const cur = wb.export();
|
|
539
|
+
const indegree = {};
|
|
540
|
+
const adj = {};
|
|
541
|
+
for (const n of cur.nodes) {
|
|
542
|
+
indegree[n.nodeId] = 0;
|
|
543
|
+
adj[n.nodeId] = [];
|
|
544
|
+
}
|
|
545
|
+
for (const e of cur.edges) {
|
|
546
|
+
indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
|
|
547
|
+
adj[e.source.nodeId].push(e.target.nodeId);
|
|
548
|
+
}
|
|
549
|
+
const q = Object.keys(indegree).filter((k) => indegree[k] === 0);
|
|
550
|
+
const layers = [];
|
|
551
|
+
while (q.length) {
|
|
552
|
+
const layer = [];
|
|
553
|
+
const next = [];
|
|
554
|
+
for (const id of q) {
|
|
555
|
+
layer.push(id);
|
|
556
|
+
for (const nb of adj[id]) {
|
|
557
|
+
indegree[nb] -= 1;
|
|
558
|
+
if (indegree[nb] === 0)
|
|
559
|
+
next.push(nb);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
layers.push(layer);
|
|
563
|
+
q.splice(0, q.length, ...next);
|
|
564
|
+
}
|
|
565
|
+
const X = 360;
|
|
566
|
+
const Y = 180;
|
|
567
|
+
const pos = {};
|
|
568
|
+
layers.forEach((layer, layerIndex) => {
|
|
569
|
+
layer.forEach((id, itemIndex) => {
|
|
570
|
+
pos[id] = { x: layerIndex * X, y: itemIndex * Y };
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
wb.setPositions(pos);
|
|
574
|
+
}, [wb]);
|
|
575
|
+
// Subscribe to runner/workbench events
|
|
576
|
+
useEffect(() => {
|
|
577
|
+
const add = (source, type) => (payload) => setEvents((prev) => {
|
|
578
|
+
if (source === "workbench" &&
|
|
579
|
+
(type === "graphChanged" || type === "graphUiChanged")) {
|
|
580
|
+
const changeType = payload?.change?.type;
|
|
581
|
+
if (changeType === "moveNode" || changeType === "moveNodes")
|
|
582
|
+
return prev;
|
|
583
|
+
}
|
|
584
|
+
const next = [
|
|
585
|
+
{ at: Date.now(), source, type, payload: structuredClone(payload) },
|
|
586
|
+
...prev,
|
|
587
|
+
];
|
|
588
|
+
return next.length > 200 ? next.slice(0, 200) : next;
|
|
589
|
+
});
|
|
590
|
+
const off1 = runner.on("value", (e) => {
|
|
591
|
+
if (e?.io === "input") {
|
|
592
|
+
const nodeId = e?.nodeId;
|
|
593
|
+
setNodeStatus((s) => ({
|
|
594
|
+
...s,
|
|
595
|
+
[nodeId]: { ...(s[nodeId] ?? {}), invalidated: true },
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
return add("runner", "value")(e);
|
|
599
|
+
});
|
|
600
|
+
const off2 = runner.on("error", (e) => {
|
|
601
|
+
const edgeError = e;
|
|
602
|
+
const nodeError = e;
|
|
603
|
+
if (edgeError?.kind === "edge-convert") {
|
|
604
|
+
const edgeId = edgeError.edgeId;
|
|
605
|
+
setEdgeStatus((s) => ({
|
|
606
|
+
...s,
|
|
607
|
+
[edgeId]: { ...(s[edgeId] ?? {}), lastError: edgeError.err },
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
610
|
+
else if (nodeError?.nodeId) {
|
|
611
|
+
const nodeId = nodeError?.nodeId;
|
|
612
|
+
setNodeStatus((s) => ({
|
|
613
|
+
...s,
|
|
614
|
+
[nodeId]: {
|
|
615
|
+
...(s[nodeId] ?? {}),
|
|
616
|
+
lastError: nodeError?.err,
|
|
617
|
+
},
|
|
618
|
+
}));
|
|
619
|
+
}
|
|
620
|
+
return add("runner", "error")(e);
|
|
621
|
+
});
|
|
622
|
+
const off3 = runner.on("invalidate", (e) => {
|
|
623
|
+
if (e?.reason === "graph-updated") {
|
|
624
|
+
setNodeStatus((s) => {
|
|
625
|
+
const next = {};
|
|
626
|
+
for (const n of wb.export().nodes) {
|
|
627
|
+
next[n.nodeId] = { ...(s[n.nodeId] ?? {}), invalidated: true };
|
|
628
|
+
}
|
|
629
|
+
return next;
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
return add("runner", "invalidate")(e);
|
|
633
|
+
});
|
|
634
|
+
const off3b = runner.on("stats", (s) => {
|
|
635
|
+
if (!s)
|
|
636
|
+
return;
|
|
637
|
+
if (s.kind === "node-start") {
|
|
638
|
+
const id = s.nodeId;
|
|
639
|
+
setNodeStatus((prev) => ({
|
|
640
|
+
...prev,
|
|
641
|
+
[id]: {
|
|
642
|
+
...(prev[id] ?? {}),
|
|
643
|
+
running: true,
|
|
644
|
+
progress: 0,
|
|
645
|
+
invalidated: false,
|
|
646
|
+
},
|
|
647
|
+
}));
|
|
648
|
+
}
|
|
649
|
+
else if (s.kind === "node-progress") {
|
|
650
|
+
const id = s.nodeId;
|
|
651
|
+
setNodeStatus((prev) => ({
|
|
652
|
+
...prev,
|
|
653
|
+
[id]: {
|
|
654
|
+
...(prev[id] ?? {}),
|
|
655
|
+
running: true,
|
|
656
|
+
progress: Number(s.progress) || 0,
|
|
657
|
+
},
|
|
658
|
+
}));
|
|
659
|
+
}
|
|
660
|
+
else if (s.kind === "node-done") {
|
|
661
|
+
const id = s.nodeId;
|
|
662
|
+
setNodeStatus((prev) => ({
|
|
663
|
+
...prev,
|
|
664
|
+
[id]: { ...(prev[id] ?? {}), running: false },
|
|
665
|
+
}));
|
|
666
|
+
}
|
|
667
|
+
else if (s.kind === "edge-start") {
|
|
668
|
+
const id = s.edgeId;
|
|
669
|
+
setEdgeStatus((prev) => ({
|
|
670
|
+
...prev,
|
|
671
|
+
[id]: { ...(prev[id] ?? {}), running: true },
|
|
672
|
+
}));
|
|
673
|
+
}
|
|
674
|
+
else if (s.kind === "edge-done") {
|
|
675
|
+
const id = s.edgeId;
|
|
676
|
+
setEdgeStatus((prev) => ({
|
|
677
|
+
...prev,
|
|
678
|
+
[id]: { ...(prev[id] ?? {}), running: false },
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
return add("runner", "stats")(s);
|
|
682
|
+
});
|
|
683
|
+
const off4 = wb.on("graphChanged", add("workbench", "graphChanged"));
|
|
684
|
+
const off4b = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
685
|
+
const off5 = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
686
|
+
const off5b = wb.on("validationChanged", (r) => setValidation(r));
|
|
687
|
+
const off6 = wb.on("selectionChanged", (sel) => {
|
|
688
|
+
setSelectedNodeId(sel.nodes?.[0]);
|
|
689
|
+
setSelectedEdgeId(sel.edges?.[0]);
|
|
690
|
+
});
|
|
691
|
+
const off7 = wb.on("error", add("workbench", "error"));
|
|
692
|
+
wb.refreshValidation();
|
|
693
|
+
return () => {
|
|
694
|
+
off1();
|
|
695
|
+
off2();
|
|
696
|
+
off3();
|
|
697
|
+
off3b();
|
|
698
|
+
off4();
|
|
699
|
+
off4b();
|
|
700
|
+
off5();
|
|
701
|
+
off5b();
|
|
702
|
+
off6();
|
|
703
|
+
off7();
|
|
704
|
+
};
|
|
705
|
+
}, [runner, wb]);
|
|
706
|
+
// Push incremental updates into running engine without full reload
|
|
707
|
+
useEffect(() => {
|
|
708
|
+
if (runner.isRunning()) {
|
|
709
|
+
try {
|
|
710
|
+
runner.update(def);
|
|
711
|
+
}
|
|
712
|
+
catch { }
|
|
713
|
+
}
|
|
714
|
+
}, [runner, def, graphTick]);
|
|
715
|
+
const validationByNode = useMemo(() => {
|
|
716
|
+
const inputs = {};
|
|
717
|
+
const outputs = {};
|
|
718
|
+
const issues = {};
|
|
719
|
+
if (!validation)
|
|
720
|
+
return { inputs, outputs, issues };
|
|
721
|
+
for (const is of validation.issues ?? []) {
|
|
722
|
+
const d = is?.data;
|
|
723
|
+
const level = is?.level;
|
|
724
|
+
const code = String(is?.code ?? "");
|
|
725
|
+
const message = String(is?.message ?? code);
|
|
726
|
+
if (!d)
|
|
727
|
+
continue;
|
|
728
|
+
if (d.nodeId) {
|
|
729
|
+
if (d.input) {
|
|
730
|
+
const arr = inputs[d.nodeId] ?? (inputs[d.nodeId] = []);
|
|
731
|
+
arr.push({ handle: String(d.input), level, message, code });
|
|
732
|
+
const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
|
|
733
|
+
nodeArr.push({ level, message, code });
|
|
734
|
+
}
|
|
735
|
+
if (d.output) {
|
|
736
|
+
const arr = outputs[d.nodeId] ?? (outputs[d.nodeId] = []);
|
|
737
|
+
arr.push({ handle: String(d.output), level, message, code });
|
|
738
|
+
const nodeArr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
|
|
739
|
+
nodeArr.push({ level, message, code });
|
|
740
|
+
}
|
|
741
|
+
if (!d.input && !d.output) {
|
|
742
|
+
const arr = issues[d.nodeId] ?? (issues[d.nodeId] = []);
|
|
743
|
+
arr.push({ level, message, code });
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return { inputs, outputs, issues };
|
|
748
|
+
}, [validation]);
|
|
749
|
+
const validationGlobal = useMemo(() => {
|
|
750
|
+
const list = [];
|
|
751
|
+
if (!validation)
|
|
752
|
+
return list;
|
|
753
|
+
for (const is of validation.issues ?? []) {
|
|
754
|
+
const d = is?.data;
|
|
755
|
+
const level = is?.level;
|
|
756
|
+
const code = String(is?.code ?? "");
|
|
757
|
+
const message = String(is?.message ?? code);
|
|
758
|
+
if (!d || (!d.nodeId && !d.edgeId)) {
|
|
759
|
+
list.push({ level, code, message });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return list;
|
|
763
|
+
}, [validation]);
|
|
764
|
+
const validationByEdge = useMemo(() => {
|
|
765
|
+
const errors = {};
|
|
766
|
+
const issues = {};
|
|
767
|
+
if (!validation)
|
|
768
|
+
return { errors, issues };
|
|
769
|
+
for (const is of validation.issues ?? []) {
|
|
770
|
+
const d = is?.data;
|
|
771
|
+
const level = is?.level;
|
|
772
|
+
const code = String(is?.code ?? "");
|
|
773
|
+
const message = String(is?.message ?? code);
|
|
774
|
+
if (d?.edgeId) {
|
|
775
|
+
if (level === "error")
|
|
776
|
+
errors[d.edgeId] = true;
|
|
777
|
+
const arr = issues[d.edgeId] ?? (issues[d.edgeId] = []);
|
|
778
|
+
arr.push({ level, message, code });
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return { errors, issues };
|
|
782
|
+
}, [validation]);
|
|
783
|
+
const isRunning = useCallback(() => runner.isRunning(), [runner]);
|
|
784
|
+
const engineKind = useCallback(() => runner.getRunningEngine(), [runner]);
|
|
785
|
+
const start = useCallback((engine) => {
|
|
786
|
+
try {
|
|
787
|
+
runner.launch(wb.export(), { engine });
|
|
788
|
+
}
|
|
789
|
+
catch { }
|
|
790
|
+
}, [runner, wb]);
|
|
791
|
+
const stop = useCallback(() => runner.dispose(), [runner]);
|
|
792
|
+
const step = useCallback(() => runner.step(), [runner]);
|
|
793
|
+
const flush = useCallback(() => runner.flush(), [runner]);
|
|
794
|
+
const value = useMemo(() => ({
|
|
795
|
+
wb,
|
|
796
|
+
runner,
|
|
797
|
+
registry,
|
|
798
|
+
setRegistry,
|
|
799
|
+
def,
|
|
800
|
+
selectedNodeId,
|
|
801
|
+
selectedEdgeId,
|
|
802
|
+
setSelection,
|
|
803
|
+
nodeStatus,
|
|
804
|
+
edgeStatus,
|
|
805
|
+
valuesTick,
|
|
806
|
+
inputsMap,
|
|
807
|
+
outputsMap,
|
|
808
|
+
validationByNode,
|
|
809
|
+
validationByEdge,
|
|
810
|
+
validationGlobal,
|
|
811
|
+
events,
|
|
812
|
+
clearEvents,
|
|
813
|
+
isRunning,
|
|
814
|
+
engineKind,
|
|
815
|
+
start,
|
|
816
|
+
stop,
|
|
817
|
+
step,
|
|
818
|
+
flush,
|
|
819
|
+
runAutoLayout,
|
|
820
|
+
}), [
|
|
821
|
+
wb,
|
|
822
|
+
runner,
|
|
823
|
+
registry,
|
|
824
|
+
setRegistry,
|
|
825
|
+
def,
|
|
826
|
+
selectedNodeId,
|
|
827
|
+
selectedEdgeId,
|
|
828
|
+
setSelection,
|
|
829
|
+
nodeStatus,
|
|
830
|
+
edgeStatus,
|
|
831
|
+
valuesTick,
|
|
832
|
+
inputsMap,
|
|
833
|
+
outputsMap,
|
|
834
|
+
validationByNode,
|
|
835
|
+
validationByEdge,
|
|
836
|
+
validationGlobal,
|
|
837
|
+
events,
|
|
838
|
+
clearEvents,
|
|
839
|
+
isRunning,
|
|
840
|
+
engineKind,
|
|
841
|
+
start,
|
|
842
|
+
stop,
|
|
843
|
+
step,
|
|
844
|
+
flush,
|
|
845
|
+
runAutoLayout,
|
|
846
|
+
]);
|
|
847
|
+
return (jsx(WorkbenchContext.Provider, { value: value, children: children }));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function toReactFlow(def, positions, registry, selectedNodeIds, selectedEdgeIds, opts) {
|
|
851
|
+
const nodeHandleMap = {};
|
|
852
|
+
const nodes = def.nodes.map((n) => {
|
|
853
|
+
const desc = registry.nodes.get(n.typeId);
|
|
854
|
+
const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
855
|
+
const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
|
|
856
|
+
nodeHandleMap[n.nodeId] = {
|
|
857
|
+
inputs: new Set(inputHandles.map((h) => h.id)),
|
|
858
|
+
outputs: new Set(outputHandles.map((h) => h.id)),
|
|
859
|
+
};
|
|
860
|
+
return {
|
|
861
|
+
id: n.nodeId,
|
|
862
|
+
data: {
|
|
863
|
+
typeId: n.typeId,
|
|
864
|
+
params: n.params,
|
|
865
|
+
inputHandles,
|
|
866
|
+
outputHandles,
|
|
867
|
+
showValues: opts?.showValues,
|
|
868
|
+
inputValues: opts?.inputs?.[n.nodeId],
|
|
869
|
+
outputValues: opts?.outputs?.[n.nodeId],
|
|
870
|
+
status: opts?.nodeStatus?.[n.nodeId],
|
|
871
|
+
validation: {
|
|
872
|
+
inputs: opts?.nodeValidation?.inputs?.[n.nodeId] ?? [],
|
|
873
|
+
outputs: opts?.nodeValidation?.outputs?.[n.nodeId] ?? [],
|
|
874
|
+
issues: opts?.nodeValidation?.issues?.[n.nodeId] ?? [],
|
|
875
|
+
},
|
|
876
|
+
toDisplay: opts?.toDisplay,
|
|
877
|
+
},
|
|
878
|
+
position: positions[n.nodeId] ?? { x: 0, y: 0 },
|
|
879
|
+
type: opts?.resolveNodeType?.(n.typeId) ?? "@bian-womp/spark:default",
|
|
880
|
+
selected: selectedNodeIds ? selectedNodeIds.has(n.nodeId) : undefined,
|
|
881
|
+
};
|
|
882
|
+
});
|
|
883
|
+
const edges = def.edges
|
|
884
|
+
.filter((e) => {
|
|
885
|
+
const src = nodeHandleMap[e.source.nodeId];
|
|
886
|
+
const dst = nodeHandleMap[e.target.nodeId];
|
|
887
|
+
if (!src || !dst)
|
|
888
|
+
return false;
|
|
889
|
+
return src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle);
|
|
890
|
+
})
|
|
891
|
+
.map((e) => {
|
|
892
|
+
const st = opts?.edgeStatus?.[e.id];
|
|
893
|
+
const isRunning = !!st?.running;
|
|
894
|
+
const hasError = !!st?.lastError;
|
|
895
|
+
const isInvalidEdge = !!opts?.edgeValidation?.[e.id];
|
|
896
|
+
const style = hasError || isInvalidEdge
|
|
897
|
+
? { stroke: "#ef4444", strokeWidth: 2 }
|
|
898
|
+
: isRunning
|
|
899
|
+
? { stroke: "#3b82f6" }
|
|
900
|
+
: undefined;
|
|
901
|
+
return {
|
|
902
|
+
id: e.id,
|
|
903
|
+
source: e.source.nodeId,
|
|
904
|
+
target: e.target.nodeId,
|
|
905
|
+
sourceHandle: e.source.handle,
|
|
906
|
+
targetHandle: e.target.handle,
|
|
907
|
+
selected: selectedEdgeIds ? selectedEdgeIds.has(e.id) : undefined,
|
|
908
|
+
animated: isRunning,
|
|
909
|
+
style,
|
|
910
|
+
};
|
|
911
|
+
});
|
|
912
|
+
return { nodes, edges };
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function IssueBadge({ level, title, size = 12, className, }) {
|
|
916
|
+
const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
|
|
917
|
+
return (jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsx(XCircleIcon, { size: size, weight: "fill" })) : (jsx(WarningCircleIcon, { size: size, weight: "fill" })) }));
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
|
|
921
|
+
const typeId = data.typeId;
|
|
922
|
+
const inputEntries = data.inputHandles ?? [];
|
|
923
|
+
const outputEntries = data.outputHandles ?? [];
|
|
924
|
+
const status = data.status ?? {};
|
|
925
|
+
const validation = data.validation ?? {
|
|
926
|
+
inputs: [],
|
|
927
|
+
outputs: [],
|
|
928
|
+
issues: [],
|
|
929
|
+
};
|
|
930
|
+
const HEADER_SIZE = 24;
|
|
931
|
+
const ROW_SIZE = 22;
|
|
932
|
+
const maxRows = Math.max(inputEntries.length, outputEntries.length);
|
|
933
|
+
const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
|
|
934
|
+
const minWidth = data.showValues ? 320 : 160;
|
|
935
|
+
const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
|
|
936
|
+
const hasError = !!status.lastError;
|
|
937
|
+
const isRunning = !!status.running;
|
|
938
|
+
const isInvalid = !!status.invalidated && !isRunning && !hasError;
|
|
939
|
+
const borderClasses = selected
|
|
940
|
+
? "border-2 border-gray-900 dark:border-gray-100"
|
|
941
|
+
: hasError
|
|
942
|
+
? "border-2 border-red-500"
|
|
943
|
+
: isRunning
|
|
944
|
+
? "border-2 border-blue-500 ring-2 ring-blue-200 dark:ring-blue-900"
|
|
945
|
+
: isInvalid
|
|
946
|
+
? "border-2 border-amber-500 border-dashed"
|
|
947
|
+
: "border border-gray-500 dark:border-gray-400";
|
|
948
|
+
const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
|
|
949
|
+
return (jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900 border-solid", borderClasses), style: { position: "relative", minHeight: minHeight, minWidth }, children: [jsxs("div", { className: "flex h-6 items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", children: [jsx("strong", { className: "flex-1 h-full leading-6 text-xs", 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")
|
|
950
|
+
? "error"
|
|
951
|
+
: "warning", size: 12, className: "w-3 h-3", title: validation.issues
|
|
952
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
953
|
+
.join("; ") })), jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }), (isRunning || pct > 0) && (jsx("div", { className: "h-1 bg-blue-200 dark:bg-blue-900", children: jsx("div", { className: "h-1 bg-blue-500 transition-all", style: { width: `${pct}%` } }) })), inputEntries.map((entry, i) => {
|
|
954
|
+
const vIssues = validation.inputs.filter((v) => v.handle === entry.id);
|
|
955
|
+
const hasAny = vIssues.length > 0;
|
|
956
|
+
const hasErr = vIssues.some((v) => v.level === "error");
|
|
957
|
+
const title = vIssues
|
|
958
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
959
|
+
.join("; ");
|
|
960
|
+
return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "target", position: Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8 }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), data.showValues && (jsx("span", { className: "ml-1 opacity-60", children: data.toDisplay
|
|
961
|
+
? data.toDisplay(entry.typeId, data.inputValues?.[entry.id])
|
|
962
|
+
: String(data.inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
|
|
963
|
+
}), outputEntries.map((entry, i) => {
|
|
964
|
+
const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
|
|
965
|
+
const hasAny = vIssues.length > 0;
|
|
966
|
+
const hasErr = vIssues.some((v) => v.level === "error");
|
|
967
|
+
const title = vIssues
|
|
968
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
969
|
+
.join("; ");
|
|
970
|
+
return (jsxs(React.Fragment, { children: [jsx(Handle, { id: entry.id, type: "source", position: Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8, textAlign: "right" }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), data.showValues && (jsx("span", { className: "ml-1 opacity-60", children: data.toDisplay
|
|
971
|
+
? data.toDisplay(entry.typeId, data.outputValues?.[entry.id])
|
|
972
|
+
: String(data.outputValues?.[entry.id]) }))] })] }, `out-${entry.id}`));
|
|
973
|
+
})] }));
|
|
974
|
+
});
|
|
975
|
+
DefaultNode.displayName = "DefaultNode";
|
|
976
|
+
|
|
977
|
+
function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
|
|
978
|
+
const { registry } = useWorkbenchContext();
|
|
979
|
+
const rf = useReactFlow();
|
|
980
|
+
if (!open || !clientPos)
|
|
981
|
+
return null;
|
|
982
|
+
const items = Array.from(registry.nodes.keys());
|
|
983
|
+
const handleClick = (typeId) => {
|
|
984
|
+
const p = rf.project({ x: clientPos.x, y: clientPos.y });
|
|
985
|
+
onAdd(typeId, p);
|
|
986
|
+
onClose();
|
|
987
|
+
};
|
|
988
|
+
return (jsxs("div", { className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: clientPos.x, top: clientPos.y }, onMouseLeave: onClose, children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Add Node" }), jsx("div", { className: "max-h-60 overflow-auto", children: items.map((id) => (jsx("button", { onClick: () => handleClick(id), className: "block w-full text-left px-2 py-1 hover:bg-gray-100 cursor-pointer", children: id }, id))) })] }));
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function WorkbenchCanvas({ showValues, toDisplay, }) {
|
|
992
|
+
const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
|
|
993
|
+
const ioValues = { inputs: inputsMap, outputs: outputsMap };
|
|
994
|
+
const nodeValidation = validationByNode;
|
|
995
|
+
const edgeValidation = validationByEdge.errors;
|
|
996
|
+
const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
|
|
997
|
+
const { nodeTypes, resolveNodeType } = useMemo(() => {
|
|
998
|
+
// Build nodeTypes map using UI extension registry
|
|
999
|
+
const ui = wb.getUI();
|
|
1000
|
+
const custom = new Map();
|
|
1001
|
+
for (const typeId of Array.from(registry.nodes.keys())) {
|
|
1002
|
+
const renderer = ui.getNodeRenderer(typeId);
|
|
1003
|
+
if (renderer)
|
|
1004
|
+
custom.set(typeId, renderer);
|
|
1005
|
+
}
|
|
1006
|
+
const types = { "@bian-womp/spark:default": DefaultNode };
|
|
1007
|
+
for (const [typeId, comp] of custom.entries()) {
|
|
1008
|
+
types[`spark:${typeId}`] = comp;
|
|
1009
|
+
}
|
|
1010
|
+
const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark:${nodeTypeId}` : "@bian-womp/spark:default";
|
|
1011
|
+
return { nodeTypes: types, resolveNodeType: resolver };
|
|
1012
|
+
// registry is stable; ui renderers expected to be set up before mount
|
|
1013
|
+
}, [wb, registry]);
|
|
1014
|
+
const { nodes, edges } = useMemo(() => {
|
|
1015
|
+
const def = wb.export();
|
|
1016
|
+
const sel = wb.getSelection();
|
|
1017
|
+
return toReactFlow(def, wb.getPositions(), registry, new Set(sel.nodes), new Set(sel.edges), {
|
|
1018
|
+
showValues,
|
|
1019
|
+
inputs: ioValues.inputs,
|
|
1020
|
+
outputs: ioValues.outputs,
|
|
1021
|
+
resolveNodeType,
|
|
1022
|
+
toDisplay,
|
|
1023
|
+
nodeStatus,
|
|
1024
|
+
edgeStatus,
|
|
1025
|
+
nodeValidation,
|
|
1026
|
+
edgeValidation,
|
|
1027
|
+
});
|
|
1028
|
+
}, [
|
|
1029
|
+
showValues,
|
|
1030
|
+
ioValues,
|
|
1031
|
+
valuesTick,
|
|
1032
|
+
toDisplay,
|
|
1033
|
+
nodeStatus,
|
|
1034
|
+
edgeStatus,
|
|
1035
|
+
nodeValidation,
|
|
1036
|
+
edgeValidation,
|
|
1037
|
+
]);
|
|
1038
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
1039
|
+
const [menuPos, setMenuPos] = useState(null);
|
|
1040
|
+
const onContextMenu = (e) => {
|
|
1041
|
+
e.preventDefault();
|
|
1042
|
+
setMenuPos({ x: e.clientX, y: e.clientY });
|
|
1043
|
+
setMenuOpen(true);
|
|
1044
|
+
};
|
|
1045
|
+
const addNodeAt = (typeId, pos) => {
|
|
1046
|
+
wb.addNode({ typeId, position: pos });
|
|
1047
|
+
};
|
|
1048
|
+
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, onSelectionChange: onSelectionChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, children: [jsx(Background, {}), jsx(MiniMap, {}), jsx(Controls, {}), jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) })] }) }));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWorkbenchChange, }) {
|
|
1052
|
+
const { events, clearEvents } = useWorkbenchContext();
|
|
1053
|
+
const scrollRef = useRef(null);
|
|
1054
|
+
const rows = useMemo(() => {
|
|
1055
|
+
const filtered = hideWorkbench
|
|
1056
|
+
? events.filter((e) => e.source !== "workbench")
|
|
1057
|
+
: events;
|
|
1058
|
+
return filtered.slice().reverse();
|
|
1059
|
+
}, [events, hideWorkbench]);
|
|
1060
|
+
useEffect(() => {
|
|
1061
|
+
if (!autoScroll)
|
|
1062
|
+
return;
|
|
1063
|
+
const el = scrollRef.current;
|
|
1064
|
+
if (!el)
|
|
1065
|
+
return;
|
|
1066
|
+
el.scrollTop = el.scrollHeight;
|
|
1067
|
+
}, [rows, autoScroll]);
|
|
1068
|
+
const renderPayload = (v) => {
|
|
1069
|
+
try {
|
|
1070
|
+
return JSON.stringify(v, null, 0);
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
return String(v);
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
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, idx) => (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-8 shrink-0 text-right text-gray-500 select-none", children: idx + 1 }), 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-10", children: renderPayload(ev.payload) })] }, `${ev.at}:${idx}`))) })] }));
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toDisplay, setInput, }) {
|
|
1080
|
+
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
|
|
1081
|
+
const nodeValidationIssues = validationByNode.issues;
|
|
1082
|
+
const edgeValidationIssues = validationByEdge.issues;
|
|
1083
|
+
const nodeValidationHandles = validationByNode;
|
|
1084
|
+
const globalValidationIssues = validationGlobal;
|
|
1085
|
+
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
1086
|
+
const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
|
|
1087
|
+
const selectedDesc = selectedNode
|
|
1088
|
+
? registry.nodes.get(selectedNode.typeId)
|
|
1089
|
+
: undefined;
|
|
1090
|
+
const inputHandles = Object.keys(selectedDesc?.inputs ?? {});
|
|
1091
|
+
const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
|
|
1092
|
+
const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
|
|
1093
|
+
const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
|
|
1094
|
+
const selectedNodeStatus = selectedNodeId
|
|
1095
|
+
? nodeStatus?.[selectedNodeId]
|
|
1096
|
+
: undefined;
|
|
1097
|
+
const selectedNodeValidation = selectedNodeId
|
|
1098
|
+
? nodeValidationIssues?.[selectedNodeId] ?? []
|
|
1099
|
+
: [];
|
|
1100
|
+
const selectedEdgeValidation = selectedEdge
|
|
1101
|
+
? edgeValidationIssues?.[selectedEdge.id] ?? []
|
|
1102
|
+
: [];
|
|
1103
|
+
const selectedNodeHandleValidation = selectedNodeId
|
|
1104
|
+
? {
|
|
1105
|
+
inputs: nodeValidationHandles?.inputs?.[selectedNodeId] ?? [],
|
|
1106
|
+
outputs: nodeValidationHandles?.outputs?.[selectedNodeId] ?? [],
|
|
1107
|
+
}
|
|
1108
|
+
: { inputs: [], outputs: [] };
|
|
1109
|
+
// Local drafts and originals for commit-on-blur/enter behavior
|
|
1110
|
+
const [drafts, setDrafts] = useState({});
|
|
1111
|
+
const [originals, setOriginals] = useState({});
|
|
1112
|
+
// Initialize drafts from current inputs whenever selection or valuesTick change,
|
|
1113
|
+
// but do not clobber fields currently being edited (dirty drafts)
|
|
1114
|
+
useEffect(() => {
|
|
1115
|
+
const shallowEqual = (a, b) => {
|
|
1116
|
+
const ak = Object.keys(a);
|
|
1117
|
+
const bk = Object.keys(b);
|
|
1118
|
+
if (ak.length !== bk.length)
|
|
1119
|
+
return false;
|
|
1120
|
+
for (const k of ak)
|
|
1121
|
+
if (a[k] !== b[k])
|
|
1122
|
+
return false;
|
|
1123
|
+
return true;
|
|
1124
|
+
};
|
|
1125
|
+
if (!selectedNodeId) {
|
|
1126
|
+
if (Object.keys(drafts).length || Object.keys(originals).length) {
|
|
1127
|
+
setDrafts({});
|
|
1128
|
+
setOriginals({});
|
|
1129
|
+
}
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const desc = selectedDesc;
|
|
1133
|
+
const handles = Object.keys(desc?.inputs ?? {});
|
|
1134
|
+
const nextDrafts = { ...drafts };
|
|
1135
|
+
const nextOriginals = { ...originals };
|
|
1136
|
+
for (const h of handles) {
|
|
1137
|
+
const typeId = desc?.inputs?.[h];
|
|
1138
|
+
const current = nodeInputs[h];
|
|
1139
|
+
const display = toDisplay(typeId, current);
|
|
1140
|
+
const wasOriginal = originals[h];
|
|
1141
|
+
const isDirty = drafts[h] !== undefined &&
|
|
1142
|
+
wasOriginal !== undefined &&
|
|
1143
|
+
drafts[h] !== wasOriginal;
|
|
1144
|
+
if (!isDirty) {
|
|
1145
|
+
nextDrafts[h] = display;
|
|
1146
|
+
nextOriginals[h] = display;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (!shallowEqual(drafts, nextDrafts))
|
|
1150
|
+
setDrafts(nextDrafts);
|
|
1151
|
+
if (!shallowEqual(originals, nextOriginals))
|
|
1152
|
+
setOriginals(nextOriginals);
|
|
1153
|
+
}, [selectedNodeId, selectedDesc, valuesTick]);
|
|
1154
|
+
const widthClass = debug ? "w-[480px]" : "w-[320px]";
|
|
1155
|
+
return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [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 ??
|
|
1156
|
+
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) => {
|
|
1157
|
+
const typeId = (selectedDesc?.inputs ?? {})[h];
|
|
1158
|
+
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === h);
|
|
1159
|
+
const commonProps = {
|
|
1160
|
+
style: { flex: 1 },
|
|
1161
|
+
disabled: isLinked,
|
|
1162
|
+
};
|
|
1163
|
+
const current = nodeInputs[h];
|
|
1164
|
+
const value = drafts[h] ?? toDisplay(typeId, current);
|
|
1165
|
+
const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
|
|
1166
|
+
const commit = () => {
|
|
1167
|
+
const draft = drafts[h];
|
|
1168
|
+
if (draft === undefined)
|
|
1169
|
+
return;
|
|
1170
|
+
setInput(h, draft);
|
|
1171
|
+
setOriginals((o) => ({ ...o, [h]: draft }));
|
|
1172
|
+
};
|
|
1173
|
+
const revert = () => {
|
|
1174
|
+
const orig = originals[h] ?? toDisplay(typeId, current);
|
|
1175
|
+
setDrafts((d) => ({ ...d, [h]: orig }));
|
|
1176
|
+
};
|
|
1177
|
+
const isEnum = typeId?.startsWith("enum:");
|
|
1178
|
+
const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
|
|
1179
|
+
const hasValidation = inIssues.length > 0;
|
|
1180
|
+
const hasErr = inIssues.some((m) => m.level === "error");
|
|
1181
|
+
const title = inIssues
|
|
1182
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
1183
|
+
.join("; ");
|
|
1184
|
+
return (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxs("label", { className: "w-28", children: [h, jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (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: drafts[h] ?? toDisplay(typeId, current), onChange: (e) => {
|
|
1185
|
+
const label = String(e.target.value);
|
|
1186
|
+
const byLabel = registry.getEnumValue(typeId, label);
|
|
1187
|
+
let raw = (byLabel !== undefined ? byLabel : Number(label));
|
|
1188
|
+
if (!Number.isFinite(raw))
|
|
1189
|
+
raw = undefined;
|
|
1190
|
+
setInput(h, raw);
|
|
1191
|
+
const display = toDisplay(typeId, raw);
|
|
1192
|
+
setDrafts((d) => ({ ...d, [h]: display }));
|
|
1193
|
+
setOriginals((o) => ({ ...o, [h]: display }));
|
|
1194
|
+
}, ...commonProps, children: [jsx("option", { value: "", children: "(select)" }), registry.getEnumOptions?.(typeId).map((opt) => (jsx("option", { value: opt.label, children: opt.label }, opt.value)))] })) : (jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", placeholder: isLinked ? "wired" : undefined, value: value, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
|
|
1195
|
+
if (e.key === "Enter")
|
|
1196
|
+
commit();
|
|
1197
|
+
if (e.key === "Escape")
|
|
1198
|
+
revert();
|
|
1199
|
+
}, ...commonProps }))] }, h));
|
|
1200
|
+
}))] }), jsxs("div", { children: [jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsx("label", { className: "w-20", children: h }), jsx("div", { className: "flex-1", children: toDisplay(selectedDesc?.outputs?.[h], nodeOutputs[h]) }), (() => {
|
|
1201
|
+
const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
|
|
1202
|
+
if (outIssues.length === 0)
|
|
1203
|
+
return null;
|
|
1204
|
+
const outErr = outIssues.some((m) => m.level === "error");
|
|
1205
|
+
const outTitle = outIssues
|
|
1206
|
+
.map((v) => `${v.code}: ${v.message}`)
|
|
1207
|
+
.join("; ");
|
|
1208
|
+
return (jsx(IssueBadge, { level: outErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: outTitle }));
|
|
1209
|
+
})()] }, 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 }) }))] }));
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
class GraphRunner {
|
|
1213
|
+
constructor(registry, backend) {
|
|
1214
|
+
this.registry = registry;
|
|
1215
|
+
this.listeners = new Map();
|
|
1216
|
+
this.stagedInputs = {};
|
|
1217
|
+
this.backend = { kind: "local" };
|
|
1218
|
+
if (backend)
|
|
1219
|
+
this.backend = backend;
|
|
1220
|
+
}
|
|
1221
|
+
build(def) {
|
|
1222
|
+
if (this.backend.kind === "local") {
|
|
1223
|
+
const builder = new GraphBuilder(this.registry);
|
|
1224
|
+
this.runtime = builder.build(def);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
// Remote: no-op here; build is performed on remote server during launch
|
|
1228
|
+
}
|
|
1229
|
+
update(def) {
|
|
1230
|
+
if (this.backend.kind === "local") {
|
|
1231
|
+
if (!this.runtime)
|
|
1232
|
+
return;
|
|
1233
|
+
this.runtime.update(def, this.registry);
|
|
1234
|
+
this.emit("invalidate", { reason: "graph-updated" });
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
// Remote: forward update; ignore errors (fire-and-forget)
|
|
1238
|
+
void this.ensureRemote().then(async (rc) => {
|
|
1239
|
+
try {
|
|
1240
|
+
await rc.runner.update(def);
|
|
1241
|
+
this.emit("invalidate", { reason: "graph-updated" });
|
|
1242
|
+
}
|
|
1243
|
+
catch { }
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
launch(def, opts) {
|
|
1247
|
+
if (this.engine) {
|
|
1248
|
+
throw new Error("Engine already running. Stop the current engine first.");
|
|
1249
|
+
}
|
|
1250
|
+
if (this.backend.kind === "local") {
|
|
1251
|
+
this.build(def);
|
|
1252
|
+
if (!this.runtime)
|
|
1253
|
+
throw new Error("Runtime not built");
|
|
1254
|
+
const rt = this.runtime;
|
|
1255
|
+
switch (opts.engine) {
|
|
1256
|
+
case "push":
|
|
1257
|
+
this.engine = new PushEngine(rt);
|
|
1258
|
+
break;
|
|
1259
|
+
case "batched":
|
|
1260
|
+
this.engine = new BatchedEngine(rt, {
|
|
1261
|
+
flushIntervalMs: opts.batched?.flushIntervalMs ?? 0,
|
|
1262
|
+
});
|
|
1263
|
+
break;
|
|
1264
|
+
case "pull":
|
|
1265
|
+
this.engine = new PullEngine(rt);
|
|
1266
|
+
break;
|
|
1267
|
+
case "hybrid":
|
|
1268
|
+
this.engine = new HybridEngine(rt, {
|
|
1269
|
+
windowMs: opts.hybrid?.windowMs ?? 250,
|
|
1270
|
+
batchThreshold: opts.hybrid?.batchThreshold ?? 3,
|
|
1271
|
+
});
|
|
1272
|
+
break;
|
|
1273
|
+
case "step":
|
|
1274
|
+
this.engine = new StepEngine(rt);
|
|
1275
|
+
break;
|
|
1276
|
+
default:
|
|
1277
|
+
throw new Error("Unknown engine kind");
|
|
1278
|
+
}
|
|
1279
|
+
this.engine.on("value", (e) => this.emit("value", e));
|
|
1280
|
+
this.engine.on("error", (e) => this.emit("error", e));
|
|
1281
|
+
this.engine.on("invalidate", (e) => this.emit("invalidate", e));
|
|
1282
|
+
this.engine.on("stats", (e) => this.emit("stats", e));
|
|
1283
|
+
this.engine.launch();
|
|
1284
|
+
this.runningKind = opts.engine;
|
|
1285
|
+
this.emit("status", { running: true, engine: this.runningKind });
|
|
1286
|
+
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
1287
|
+
for (const [handle, value] of Object.entries(map)) {
|
|
1288
|
+
this.engine.setInput(nodeId, handle, value);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
// Remote: build remotely then launch
|
|
1294
|
+
void this.ensureRemote().then(async (rc) => {
|
|
1295
|
+
await rc.runner.build(def);
|
|
1296
|
+
const eng = rc.runner.getEngine();
|
|
1297
|
+
if (!rc.listenersBound) {
|
|
1298
|
+
eng.on("value", (e) => {
|
|
1299
|
+
rc.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
1300
|
+
io: e.io,
|
|
1301
|
+
value: e.value,
|
|
1302
|
+
});
|
|
1303
|
+
this.emit("value", e);
|
|
1304
|
+
});
|
|
1305
|
+
eng.on("error", (e) => this.emit("error", e));
|
|
1306
|
+
eng.on("invalidate", (e) => this.emit("invalidate", e));
|
|
1307
|
+
eng.on("stats", (e) => this.emit("stats", e));
|
|
1308
|
+
rc.listenersBound = true;
|
|
1309
|
+
}
|
|
1310
|
+
this.engine = eng;
|
|
1311
|
+
this.engine.launch();
|
|
1312
|
+
this.runningKind = "push";
|
|
1313
|
+
this.emit("status", { running: true, engine: this.runningKind });
|
|
1314
|
+
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
1315
|
+
for (const [handle, value] of Object.entries(map)) {
|
|
1316
|
+
this.engine.setInput(nodeId, handle, value);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
setInput(nodeId, handle, value) {
|
|
1322
|
+
if (!this.stagedInputs[nodeId])
|
|
1323
|
+
this.stagedInputs[nodeId] = {};
|
|
1324
|
+
this.stagedInputs[nodeId][handle] = value;
|
|
1325
|
+
if (this.engine)
|
|
1326
|
+
this.engine.setInput(nodeId, handle, value);
|
|
1327
|
+
}
|
|
1328
|
+
async step() {
|
|
1329
|
+
if (this.backend.kind !== "local")
|
|
1330
|
+
return; // unsupported remotely
|
|
1331
|
+
const eng = this.engine;
|
|
1332
|
+
if (eng instanceof StepEngine)
|
|
1333
|
+
await eng.step();
|
|
1334
|
+
}
|
|
1335
|
+
async computeNode(nodeId) {
|
|
1336
|
+
if (this.backend.kind !== "local")
|
|
1337
|
+
return; // unsupported remotely
|
|
1338
|
+
const eng = this.engine;
|
|
1339
|
+
if (eng instanceof PullEngine)
|
|
1340
|
+
await eng.computeNode(nodeId);
|
|
1341
|
+
}
|
|
1342
|
+
flush() {
|
|
1343
|
+
if (this.backend.kind !== "local")
|
|
1344
|
+
return; // unsupported remotely
|
|
1345
|
+
const eng = this.engine;
|
|
1346
|
+
if (eng instanceof BatchedEngine)
|
|
1347
|
+
eng.flush();
|
|
1348
|
+
}
|
|
1349
|
+
getOutputs(def) {
|
|
1350
|
+
const out = {};
|
|
1351
|
+
if (this.backend.kind === "local") {
|
|
1352
|
+
if (!this.runtime)
|
|
1353
|
+
return out;
|
|
1354
|
+
for (const n of def.nodes) {
|
|
1355
|
+
const desc = this.registry.nodes.get(n.typeId);
|
|
1356
|
+
const handles = Object.keys(desc?.outputs ?? {});
|
|
1357
|
+
for (const h of handles) {
|
|
1358
|
+
const v = this.runtime.getOutput(n.nodeId, h);
|
|
1359
|
+
if (v !== undefined) {
|
|
1360
|
+
if (!out[n.nodeId])
|
|
1361
|
+
out[n.nodeId] = {};
|
|
1362
|
+
out[n.nodeId][h] = v;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return out;
|
|
1367
|
+
}
|
|
1368
|
+
const cache = this.remote?.valueCache;
|
|
1369
|
+
if (!cache)
|
|
1370
|
+
return out;
|
|
1371
|
+
for (const n of def.nodes) {
|
|
1372
|
+
const desc = this.registry.nodes.get(n.typeId);
|
|
1373
|
+
const handles = Object.keys(desc?.outputs ?? {});
|
|
1374
|
+
for (const h of handles) {
|
|
1375
|
+
const key = `${n.nodeId}.${h}`;
|
|
1376
|
+
const rec = cache.get(key);
|
|
1377
|
+
if (rec && rec.io === "output") {
|
|
1378
|
+
if (!out[n.nodeId])
|
|
1379
|
+
out[n.nodeId] = {};
|
|
1380
|
+
out[n.nodeId][h] = rec.value;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return out;
|
|
1385
|
+
}
|
|
1386
|
+
getInputs(def) {
|
|
1387
|
+
const out = {};
|
|
1388
|
+
if (this.backend.kind === "local") {
|
|
1389
|
+
for (const n of def.nodes) {
|
|
1390
|
+
const staged = this.stagedInputs[n.nodeId] ?? {};
|
|
1391
|
+
const runtimeInputs = this.runtime
|
|
1392
|
+
? this.runtime.__unsafe_getNodeData?.(n.nodeId)?.inputs ?? {}
|
|
1393
|
+
: {};
|
|
1394
|
+
if (this.isRunning()) {
|
|
1395
|
+
out[n.nodeId] = runtimeInputs;
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
const merged = { ...runtimeInputs, ...staged };
|
|
1399
|
+
if (Object.keys(merged).length > 0)
|
|
1400
|
+
out[n.nodeId] = merged;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return out;
|
|
1404
|
+
}
|
|
1405
|
+
const cache = this.remote?.valueCache;
|
|
1406
|
+
for (const n of def.nodes) {
|
|
1407
|
+
const staged = this.stagedInputs[n.nodeId] ?? {};
|
|
1408
|
+
const desc = this.registry.nodes.get(n.typeId);
|
|
1409
|
+
const handles = Object.keys(desc?.inputs ?? {});
|
|
1410
|
+
const cur = {};
|
|
1411
|
+
for (const h of handles) {
|
|
1412
|
+
const rec = cache?.get(`${n.nodeId}.${h}`);
|
|
1413
|
+
if (rec && rec.io === "input")
|
|
1414
|
+
cur[h] = rec.value;
|
|
1415
|
+
}
|
|
1416
|
+
const merged = this.isRunning() ? cur : { ...cur, ...staged };
|
|
1417
|
+
if (Object.keys(merged).length > 0)
|
|
1418
|
+
out[n.nodeId] = merged;
|
|
1419
|
+
}
|
|
1420
|
+
return out;
|
|
1421
|
+
}
|
|
1422
|
+
async whenIdle() {
|
|
1423
|
+
await this.engine?.whenIdle();
|
|
1424
|
+
}
|
|
1425
|
+
on(event, handler) {
|
|
1426
|
+
if (!this.listeners.has(event))
|
|
1427
|
+
this.listeners.set(event, new Set());
|
|
1428
|
+
const set = this.listeners.get(event);
|
|
1429
|
+
set.add(handler);
|
|
1430
|
+
return () => set.delete(handler);
|
|
1431
|
+
}
|
|
1432
|
+
emit(event, payload) {
|
|
1433
|
+
const set = this.listeners.get(event);
|
|
1434
|
+
if (set)
|
|
1435
|
+
for (const h of Array.from(set))
|
|
1436
|
+
h(payload);
|
|
1437
|
+
}
|
|
1438
|
+
dispose() {
|
|
1439
|
+
this.engine?.dispose();
|
|
1440
|
+
this.engine = undefined;
|
|
1441
|
+
this.runtime?.dispose();
|
|
1442
|
+
this.runtime = undefined;
|
|
1443
|
+
this.remote = undefined;
|
|
1444
|
+
if (this.runningKind) {
|
|
1445
|
+
this.runningKind = undefined;
|
|
1446
|
+
this.emit("status", { running: false, engine: undefined });
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
isRunning() {
|
|
1450
|
+
return !!this.engine;
|
|
1451
|
+
}
|
|
1452
|
+
getRunningEngine() {
|
|
1453
|
+
return this.runningKind;
|
|
1454
|
+
}
|
|
1455
|
+
// Ensure remote transport/runner
|
|
1456
|
+
async ensureRemote() {
|
|
1457
|
+
if (this.remote)
|
|
1458
|
+
return this.remote;
|
|
1459
|
+
let transport;
|
|
1460
|
+
if (this.backend.kind === "remote-http") {
|
|
1461
|
+
if (!HttpPollingTransport)
|
|
1462
|
+
throw new Error("HttpPollingTransport not available");
|
|
1463
|
+
transport = new HttpPollingTransport(this.backend.baseUrl);
|
|
1464
|
+
await transport.connect();
|
|
1465
|
+
}
|
|
1466
|
+
else if (this.backend.kind === "remote-ws") {
|
|
1467
|
+
if (!WebSocketTransport)
|
|
1468
|
+
throw new Error("WebSocketTransport not available");
|
|
1469
|
+
transport = new WebSocketTransport(this.backend.url);
|
|
1470
|
+
await transport.connect();
|
|
1471
|
+
}
|
|
1472
|
+
else {
|
|
1473
|
+
throw new Error("Remote backend not configured");
|
|
1474
|
+
}
|
|
1475
|
+
const runner = new RemoteRunner(transport);
|
|
1476
|
+
this.remote = {
|
|
1477
|
+
runner,
|
|
1478
|
+
transport,
|
|
1479
|
+
valueCache: new Map(),
|
|
1480
|
+
listenersBound: false,
|
|
1481
|
+
};
|
|
1482
|
+
return this.remote;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, }) {
|
|
1487
|
+
const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
|
|
1488
|
+
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
1489
|
+
const selectedDesc = selectedNode
|
|
1490
|
+
? registry.nodes.get(selectedNode.typeId)
|
|
1491
|
+
: undefined;
|
|
1492
|
+
const [exampleState, setExampleState] = useState(example ?? "simple");
|
|
1493
|
+
const lastAutoLaunched = useRef(undefined);
|
|
1494
|
+
const autoLayoutRan = useRef(false);
|
|
1495
|
+
const applyExample = useCallback(async (key) => {
|
|
1496
|
+
if (runner.isRunning()) {
|
|
1497
|
+
alert(`Stop engine before switching example.`);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
switch (key) {
|
|
1501
|
+
case "simple": {
|
|
1502
|
+
const r = createSimpleGraphRegistry();
|
|
1503
|
+
setRegistry(r);
|
|
1504
|
+
wb.setRegistry(r);
|
|
1505
|
+
await wb.load(createSimpleGraphDef());
|
|
1506
|
+
break;
|
|
1507
|
+
}
|
|
1508
|
+
case "async": {
|
|
1509
|
+
const r = createAsyncGraphRegistry();
|
|
1510
|
+
setRegistry(r);
|
|
1511
|
+
wb.setRegistry(r);
|
|
1512
|
+
await wb.load(createAsyncGraphDef());
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
case "progress": {
|
|
1516
|
+
const r = createProgressGraphRegistry();
|
|
1517
|
+
setRegistry(r);
|
|
1518
|
+
wb.setRegistry(r);
|
|
1519
|
+
await wb.load(createProgressGraphDef());
|
|
1520
|
+
break;
|
|
1521
|
+
}
|
|
1522
|
+
case "validation": {
|
|
1523
|
+
const r = createValidationGraphRegistry();
|
|
1524
|
+
setRegistry(r);
|
|
1525
|
+
wb.setRegistry(r);
|
|
1526
|
+
await wb.load(createValidationGraphDef());
|
|
1527
|
+
break;
|
|
1528
|
+
}
|
|
1529
|
+
default: {
|
|
1530
|
+
const r = createSimpleGraphRegistry();
|
|
1531
|
+
setRegistry(r);
|
|
1532
|
+
wb.setRegistry(r);
|
|
1533
|
+
await wb.load(createSimpleGraphDef());
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
runAutoLayout();
|
|
1537
|
+
setExampleState(key);
|
|
1538
|
+
onExampleChange?.(key);
|
|
1539
|
+
}, [runner, wb, onExampleChange, runAutoLayout]);
|
|
1540
|
+
// Ensure initial example is loaded (and sync when example prop changes)
|
|
1541
|
+
useEffect(() => {
|
|
1542
|
+
applyExample(example ?? "simple");
|
|
1543
|
+
}, [example, wb]);
|
|
1544
|
+
useEffect(() => {
|
|
1545
|
+
if (!engine)
|
|
1546
|
+
return;
|
|
1547
|
+
if (runner.isRunning())
|
|
1548
|
+
return;
|
|
1549
|
+
const d = wb.export();
|
|
1550
|
+
if (!d.nodes || d.nodes.length === 0)
|
|
1551
|
+
return;
|
|
1552
|
+
if (lastAutoLaunched.current === engine)
|
|
1553
|
+
return;
|
|
1554
|
+
try {
|
|
1555
|
+
runner.launch(d, { engine: engine });
|
|
1556
|
+
lastAutoLaunched.current = engine;
|
|
1557
|
+
}
|
|
1558
|
+
catch {
|
|
1559
|
+
// ignore
|
|
1560
|
+
}
|
|
1561
|
+
}, [engine, runner, wb]);
|
|
1562
|
+
useEffect(() => {
|
|
1563
|
+
if (autoLayoutRan.current)
|
|
1564
|
+
return;
|
|
1565
|
+
const cur = wb.export();
|
|
1566
|
+
const allMissing = cur.nodes.every((n) => !wb.getPositions()[n.nodeId]);
|
|
1567
|
+
if (allMissing) {
|
|
1568
|
+
autoLayoutRan.current = true;
|
|
1569
|
+
runAutoLayout();
|
|
1570
|
+
}
|
|
1571
|
+
}, [wb, runAutoLayout]);
|
|
1572
|
+
const setInput = useCallback((handle, raw) => {
|
|
1573
|
+
if (!selectedNodeId)
|
|
1574
|
+
return;
|
|
1575
|
+
// If selected input is wired (has inbound edge), ignore user input to respect runtime value
|
|
1576
|
+
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
|
|
1577
|
+
if (isLinked)
|
|
1578
|
+
return;
|
|
1579
|
+
const typeId = selectedDesc?.inputs?.[handle];
|
|
1580
|
+
let value = raw;
|
|
1581
|
+
const parseArray = (s, map) => {
|
|
1582
|
+
const str = String(s).trim();
|
|
1583
|
+
try {
|
|
1584
|
+
const parsed = JSON.parse(str);
|
|
1585
|
+
if (Array.isArray(parsed))
|
|
1586
|
+
return parsed.map((x) => map(String(x)));
|
|
1587
|
+
}
|
|
1588
|
+
catch { }
|
|
1589
|
+
if (!str)
|
|
1590
|
+
return [];
|
|
1591
|
+
return str
|
|
1592
|
+
.split(",")
|
|
1593
|
+
.map((t) => t.trim())
|
|
1594
|
+
.filter((t) => t.length > 0)
|
|
1595
|
+
.map(map);
|
|
1596
|
+
};
|
|
1597
|
+
switch (typeId) {
|
|
1598
|
+
case "float": {
|
|
1599
|
+
const n = Number(raw);
|
|
1600
|
+
value = Number.isFinite(n) ? n : 0;
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1603
|
+
case "bool": {
|
|
1604
|
+
value = Boolean(raw);
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
case "string": {
|
|
1608
|
+
value = String(raw);
|
|
1609
|
+
break;
|
|
1610
|
+
}
|
|
1611
|
+
case "float[]": {
|
|
1612
|
+
value = parseArray(String(raw), (x) => Number(x));
|
|
1613
|
+
break;
|
|
1614
|
+
}
|
|
1615
|
+
case "bool[]": {
|
|
1616
|
+
value = parseArray(String(raw), (x) => /^(true|1)$/i.test(x));
|
|
1617
|
+
break;
|
|
1618
|
+
}
|
|
1619
|
+
case "vec3": {
|
|
1620
|
+
const arr = parseArray(String(raw), (x) => Number(x));
|
|
1621
|
+
value = [arr[0] ?? 0, arr[1] ?? 0, arr[2] ?? 0];
|
|
1622
|
+
break;
|
|
1623
|
+
}
|
|
1624
|
+
case "vec3[]": {
|
|
1625
|
+
try {
|
|
1626
|
+
const parsed = JSON.parse(String(raw));
|
|
1627
|
+
if (Array.isArray(parsed)) {
|
|
1628
|
+
value = parsed.map((v) => [
|
|
1629
|
+
Number(v?.[0] ?? 0),
|
|
1630
|
+
Number(v?.[1] ?? 0),
|
|
1631
|
+
Number(v?.[2] ?? 0),
|
|
1632
|
+
]);
|
|
1633
|
+
break;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
catch { }
|
|
1637
|
+
// fallback CSV triples: "x1,y1,z1; x2,y2,z2"
|
|
1638
|
+
value = String(raw)
|
|
1639
|
+
.split(";")
|
|
1640
|
+
.map((seg) => seg.trim())
|
|
1641
|
+
.filter(Boolean)
|
|
1642
|
+
.map((seg) => seg.split(",").map((n) => Number(n.trim())))
|
|
1643
|
+
.map((a) => [a[0] ?? 0, a[1] ?? 0, a[2] ?? 0]);
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
default: {
|
|
1647
|
+
// fallback to string
|
|
1648
|
+
value = raw;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
runner.setInput(selectedNodeId, handle, value);
|
|
1652
|
+
}, [selectedNodeId, def.edges, selectedDesc, runner]);
|
|
1653
|
+
const toDisplay = useCallback((typeId, value) => {
|
|
1654
|
+
if (value === undefined || value === null)
|
|
1655
|
+
return "";
|
|
1656
|
+
if (typeId && typeId.startsWith("enum:")) {
|
|
1657
|
+
const n = Number(value);
|
|
1658
|
+
const label = registry.getEnumLabel(typeId, n);
|
|
1659
|
+
return label ?? String(n);
|
|
1660
|
+
}
|
|
1661
|
+
const round4 = (n) => Math.round(Number(n) * 10000) / 10000;
|
|
1662
|
+
if (typeId === "vec3" && Array.isArray(value)) {
|
|
1663
|
+
const a = value;
|
|
1664
|
+
return [round4(a[0] ?? 0), round4(a[1] ?? 0), round4(a[2] ?? 0)].join(",");
|
|
1665
|
+
}
|
|
1666
|
+
const stringifyRounded = (v) => {
|
|
1667
|
+
try {
|
|
1668
|
+
return JSON.stringify(v, (_k, val) => typeof val === "number" ? round4(val) : val);
|
|
1669
|
+
}
|
|
1670
|
+
catch {
|
|
1671
|
+
return String(v);
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
if (typeId?.endsWith("[]") ||
|
|
1675
|
+
Array.isArray(value) ||
|
|
1676
|
+
(typeof value === "object" && value !== null)) {
|
|
1677
|
+
return stringifyRounded(value);
|
|
1678
|
+
}
|
|
1679
|
+
if (typeof value === "number") {
|
|
1680
|
+
const rounded = Math.round(Number(value) * 10000) / 10000;
|
|
1681
|
+
return String(rounded);
|
|
1682
|
+
}
|
|
1683
|
+
return String(value);
|
|
1684
|
+
}, [registry]);
|
|
1685
|
+
return (jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
|
|
1686
|
+
? "Stop engine before switching example"
|
|
1687
|
+
: undefined, children: [jsx("option", { value: "simple", children: "Simple" }), jsx("option", { value: "async", children: "Async Chain" }), jsx("option", { value: "progress", children: "Progress + Errors" }), jsx("option", { value: "validation", children: "Validation" })] }), 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()
|
|
1688
|
+
? "Stop engine before switching backend"
|
|
1689
|
+
: 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) => {
|
|
1690
|
+
const kind = e.target.value || undefined;
|
|
1691
|
+
onEngineChange?.(kind);
|
|
1692
|
+
}, 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", onClick: () => runner.step(), disabled: !runner.isRunning(), children: "Step" })), runner.getRunningEngine() === "batched" && (jsx("button", { className: "ml-2", onClick: () => runner.flush(), disabled: !runner.isRunning(), children: "Flush" })), runner.isRunning() ? (jsx("button", { onClick: () => runner.dispose(), disabled: !runner.isRunning(), children: "Stop" })) : (jsx("button", { onClick: () => {
|
|
1693
|
+
const kind = engine;
|
|
1694
|
+
if (!kind)
|
|
1695
|
+
return alert("Select an engine first.");
|
|
1696
|
+
try {
|
|
1697
|
+
runner.launch(wb.export(), { engine: kind });
|
|
1698
|
+
}
|
|
1699
|
+
catch (err) {
|
|
1700
|
+
alert(String(err?.message ?? err));
|
|
1701
|
+
}
|
|
1702
|
+
}, disabled: !engine, children: "Start" })), jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), 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, { showValues: showValues, toDisplay: toDisplay }, exampleState) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toDisplay: toDisplay })] })] }));
|
|
1703
|
+
}
|
|
1704
|
+
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, }) {
|
|
1705
|
+
const [registry, setRegistry] = useState(createSimpleGraphRegistry());
|
|
1706
|
+
const [wb] = useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
|
|
1707
|
+
const runner = useMemo(() => {
|
|
1708
|
+
const backend = backendKind === "remote-http"
|
|
1709
|
+
? { kind: "remote-http", baseUrl: httpBaseUrl }
|
|
1710
|
+
: backendKind === "remote-ws"
|
|
1711
|
+
? { kind: "remote-ws", url: wsUrl }
|
|
1712
|
+
: { kind: "local" };
|
|
1713
|
+
return new GraphRunner(registry, backend);
|
|
1714
|
+
}, [registry, backendKind, httpBaseUrl, wsUrl]);
|
|
1715
|
+
return (jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, children: jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
|
|
1716
|
+
if (runner.isRunning())
|
|
1717
|
+
runner.dispose();
|
|
1718
|
+
onBackendKindChange(v);
|
|
1719
|
+
}, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange }) }));
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function App() {
|
|
1723
|
+
const [engine, setEngine] = useQueryParamString("engine", "");
|
|
1724
|
+
const [example, setExample] = useQueryParamString("example", "simple");
|
|
1725
|
+
const [debug, setDebug] = useQueryParamBoolean("debug", false);
|
|
1726
|
+
const [showValues, setShowValues] = useQueryParamBoolean("values", false);
|
|
1727
|
+
const [hideWorkbench, setHideWorkbench] = useQueryParamBoolean("hideWb", false);
|
|
1728
|
+
const [autoScroll, setAutoScroll] = useQueryParamBoolean("autoScroll", true);
|
|
1729
|
+
// Backend selection via URL params
|
|
1730
|
+
const [backendKind, setBackendKind] = useQueryParamString("backend", "local");
|
|
1731
|
+
const [httpBaseUrl, setHttpBaseUrl] = useQueryParamString("sparkHttp", "http://127.0.0.1:18080");
|
|
1732
|
+
const [wsUrl, setWsUrl] = useQueryParamString("sparkWs", "ws://127.0.0.1:18081");
|
|
1733
|
+
useEffect(() => {
|
|
1734
|
+
document.getElementById("loading-screen")?.remove();
|
|
1735
|
+
}, []);
|
|
1736
|
+
return (jsx(WorkbenchStudio, { engine: engine, onEngineChange: setEngine, example: example, onExampleChange: setExample, backendKind: (backendKind || "local"), onBackendKindChange: (v) => setBackendKind(v), httpBaseUrl: httpBaseUrl || "http://127.0.0.1:18080", onHttpBaseUrlChange: setHttpBaseUrl, wsUrl: wsUrl || "ws://127.0.0.1:18081", onWsUrlChange: setWsUrl, debug: debug, onDebugChange: setDebug, showValues: showValues, onShowValuesChange: setShowValues, hideWorkbench: hideWorkbench, onHideWorkbenchChange: setHideWorkbench, autoScroll: autoScroll, onAutoScrollChange: setAutoScroll }));
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
export { AbstractWorkbench, App, CLIWorkbench, DefaultUIExtensionRegistry, InMemoryWorkbench, ReactFlowWorkbench, toReactFlow$1 as toReactFlow };
|
|
1740
|
+
//# sourceMappingURL=index.js.map
|