@bian-womp/spark-workbench 0.2.9 → 0.2.11
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 +337 -146
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/AbstractWorkbench.d.ts +2 -2
- package/lib/cjs/src/core/AbstractWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts +2 -2
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/core/contracts.d.ts +2 -2
- package/lib/cjs/src/core/contracts.d.ts.map +1 -1
- package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/cjs/src/misc/NodeContextMenu.d.ts +1 -1
- package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +7 -0
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/IGraphRunner.d.ts +7 -0
- package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +12 -0
- package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +7 -0
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +337 -146
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/AbstractWorkbench.d.ts +2 -2
- package/lib/esm/src/core/AbstractWorkbench.d.ts.map +1 -1
- package/lib/esm/src/core/InMemoryWorkbench.d.ts +2 -2
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/esm/src/core/contracts.d.ts +2 -2
- package/lib/esm/src/core/contracts.d.ts.map +1 -1
- package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/esm/src/misc/NodeContextMenu.d.ts +1 -1
- package/lib/esm/src/misc/NodeContextMenu.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +7 -0
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/IGraphRunner.d.ts +7 -0
- package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/LocalGraphRunner.d.ts +12 -0
- package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +7 -0
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/package.json +4 -4
package/lib/esm/index.js
CHANGED
|
@@ -167,6 +167,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
167
167
|
change: { type: "addNode", nodeId: id },
|
|
168
168
|
});
|
|
169
169
|
this.refreshValidation();
|
|
170
|
+
return id;
|
|
170
171
|
}
|
|
171
172
|
removeNode(nodeId) {
|
|
172
173
|
this.def.nodes = this.def.nodes.filter((n) => n.nodeId !== nodeId);
|
|
@@ -191,6 +192,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
191
192
|
change: { type: "connect", edgeId: id },
|
|
192
193
|
});
|
|
193
194
|
this.refreshValidation();
|
|
195
|
+
return id;
|
|
194
196
|
}
|
|
195
197
|
disconnect(edgeId) {
|
|
196
198
|
this.def.edges = this.def.edges.filter((e) => e.id !== edgeId);
|
|
@@ -407,6 +409,21 @@ class AbstractGraphRunner {
|
|
|
407
409
|
class LocalGraphRunner extends AbstractGraphRunner {
|
|
408
410
|
constructor(registry) {
|
|
409
411
|
super(registry, { kind: "local" });
|
|
412
|
+
this.setEnvironment = (env, opts) => {
|
|
413
|
+
if (!this.runtime)
|
|
414
|
+
return;
|
|
415
|
+
if (opts?.merge) {
|
|
416
|
+
const current = this.runtime.getEnvironment();
|
|
417
|
+
const next = { ...(current || {}), ...(env || {}) };
|
|
418
|
+
this.runtime.setEnvironment(next);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
this.runtime.setEnvironment(env);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
this.getEnvironment = () => {
|
|
425
|
+
return this.runtime?.getEnvironment?.();
|
|
426
|
+
};
|
|
410
427
|
this.emit("transport", { state: "local" });
|
|
411
428
|
}
|
|
412
429
|
build(def) {
|
|
@@ -505,17 +522,36 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
505
522
|
const runtimeInputs = this.runtime
|
|
506
523
|
? this.runtime.getNodeData?.(n.nodeId)?.inputs ?? {}
|
|
507
524
|
: {};
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
else {
|
|
512
|
-
const merged = { ...runtimeInputs, ...staged };
|
|
513
|
-
if (Object.keys(merged).length > 0)
|
|
514
|
-
out[n.nodeId] = merged;
|
|
515
|
-
}
|
|
525
|
+
const merged = { ...runtimeInputs, ...staged };
|
|
526
|
+
if (Object.keys(merged).length > 0)
|
|
527
|
+
out[n.nodeId] = merged;
|
|
516
528
|
}
|
|
517
529
|
return out;
|
|
518
530
|
}
|
|
531
|
+
async snapshotFull() {
|
|
532
|
+
const def = undefined; // UI will supply def/positions on download for local
|
|
533
|
+
const inputs = this.getInputs(this.runtime
|
|
534
|
+
? {
|
|
535
|
+
nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({ nodeId: id, typeId: "" })),
|
|
536
|
+
edges: [],
|
|
537
|
+
}
|
|
538
|
+
: { nodes: [], edges: [] });
|
|
539
|
+
const outputs = this.getOutputs(this.runtime
|
|
540
|
+
? {
|
|
541
|
+
nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({ nodeId: id, typeId: "" })),
|
|
542
|
+
edges: [],
|
|
543
|
+
}
|
|
544
|
+
: { nodes: [], edges: [] });
|
|
545
|
+
const environment = this.getEnvironment() || {};
|
|
546
|
+
return { def, environment, inputs, outputs };
|
|
547
|
+
}
|
|
548
|
+
async applySnapshotFull(payload) {
|
|
549
|
+
if (payload.def)
|
|
550
|
+
this.build(payload.def);
|
|
551
|
+
this.setEnvironment?.(payload.environment || {}, { merge: false });
|
|
552
|
+
// Hydrate via runtime for exact restore and re-emit
|
|
553
|
+
this.runtime?.hydrate({ inputs: payload.inputs || {}, outputs: payload.outputs || {} }, { reemit: true });
|
|
554
|
+
}
|
|
519
555
|
dispose() {
|
|
520
556
|
super.dispose();
|
|
521
557
|
this.runtime = undefined;
|
|
@@ -687,6 +723,36 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
687
723
|
return value;
|
|
688
724
|
}
|
|
689
725
|
}
|
|
726
|
+
async snapshotFull() {
|
|
727
|
+
const runner = await this.ensureRemoteRunner();
|
|
728
|
+
try {
|
|
729
|
+
return await runner.snapshotFull();
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
return { def: undefined, environment: {}, inputs: {}, outputs: {} };
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
async applySnapshotFull(payload) {
|
|
736
|
+
const runner = await this.ensureRemoteRunner();
|
|
737
|
+
await runner.applySnapshotFull(payload);
|
|
738
|
+
}
|
|
739
|
+
setEnvironment(env, opts) {
|
|
740
|
+
const t = this.transport;
|
|
741
|
+
if (!t)
|
|
742
|
+
return;
|
|
743
|
+
t.request({
|
|
744
|
+
message: {
|
|
745
|
+
type: "SetEnvironment",
|
|
746
|
+
payload: { environment: env, merge: opts?.merge },
|
|
747
|
+
},
|
|
748
|
+
}).catch(() => { });
|
|
749
|
+
}
|
|
750
|
+
getEnvironment() {
|
|
751
|
+
// Fetch from remote via lightweight command
|
|
752
|
+
// Note: returns undefined synchronously; callers needing value should use snapshotFull or call runner directly
|
|
753
|
+
// For now, we expose an async helper on RemoteRunner. Keep sync signature per interface.
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
690
756
|
getOutputs(def) {
|
|
691
757
|
const out = {};
|
|
692
758
|
const cache = this.valueCache;
|
|
@@ -720,7 +786,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
720
786
|
if (rec && rec.io === "input")
|
|
721
787
|
cur[h] = rec.value;
|
|
722
788
|
}
|
|
723
|
-
const merged =
|
|
789
|
+
const merged = { ...cur, ...staged };
|
|
724
790
|
if (Object.keys(merged).length > 0)
|
|
725
791
|
out[n.nodeId] = merged;
|
|
726
792
|
}
|
|
@@ -2081,7 +2147,7 @@ function DefaultNodeHeader({ id, title, status, validation, right, onInvalidate,
|
|
|
2081
2147
|
.join("; ") })), jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }));
|
|
2082
2148
|
}
|
|
2083
2149
|
function DefaultNodeContent({ id, data, isConnectable, }) {
|
|
2084
|
-
|
|
2150
|
+
useWorkbenchContext();
|
|
2085
2151
|
const { showValues, inputValues, outputValues, toString } = data;
|
|
2086
2152
|
const inputEntries = data.inputHandles ?? [];
|
|
2087
2153
|
const outputEntries = data.outputHandles ?? [];
|
|
@@ -2091,98 +2157,6 @@ function DefaultNodeContent({ id, data, isConnectable, }) {
|
|
|
2091
2157
|
outputs: []};
|
|
2092
2158
|
const isRunning = !!status.activeRuns;
|
|
2093
2159
|
const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
|
|
2094
|
-
const handleBake = React.useCallback(async (handleId) => {
|
|
2095
|
-
try {
|
|
2096
|
-
const typeId = ctx.outputTypesMap?.[id]?.[handleId];
|
|
2097
|
-
const rawValue = ctx.outputsMap?.[id]?.[handleId];
|
|
2098
|
-
if (!typeId || rawValue === undefined)
|
|
2099
|
-
return;
|
|
2100
|
-
const unwrap = (v) => isTypedOutput(v) ? getTypedOutputValue(v) : v;
|
|
2101
|
-
const clone = (v) => typeof structuredClone === "function"
|
|
2102
|
-
? structuredClone(v)
|
|
2103
|
-
: JSON.parse(JSON.stringify(v));
|
|
2104
|
-
const coerceIfNeeded = async (fromType, toType, value) => {
|
|
2105
|
-
if (!toType || toType === fromType || !ctx.runner?.coerce)
|
|
2106
|
-
return value;
|
|
2107
|
-
try {
|
|
2108
|
-
return await ctx.runner.coerce(fromType, toType, value);
|
|
2109
|
-
}
|
|
2110
|
-
catch {
|
|
2111
|
-
return value;
|
|
2112
|
-
}
|
|
2113
|
-
};
|
|
2114
|
-
const positions = ctx.wb.getPositions();
|
|
2115
|
-
const pos = positions[id] || { x: 0, y: 0 };
|
|
2116
|
-
const isArray = typeId.endsWith("[]");
|
|
2117
|
-
const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
|
|
2118
|
-
const tArr = isArray ? ctx.registry?.types.get(typeId) : undefined;
|
|
2119
|
-
const tElem = ctx.registry?.types.get(baseTypeId);
|
|
2120
|
-
const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
|
|
2121
|
-
const arrTarget = isArray ? tArr?.bakeTarget : undefined;
|
|
2122
|
-
const elemTarget = isArray ? tElem?.bakeTarget : undefined;
|
|
2123
|
-
const makeTargetInfo = (bt) => {
|
|
2124
|
-
if (!bt)
|
|
2125
|
-
return undefined;
|
|
2126
|
-
const node = ctx.registry?.nodes.get(String(bt.nodeTypeId));
|
|
2127
|
-
const inType = getInputTypeId(node?.inputs, String(bt.inputHandle || "Value"));
|
|
2128
|
-
return { bt, inType };
|
|
2129
|
-
};
|
|
2130
|
-
// Plan and execute
|
|
2131
|
-
if (singleTarget) {
|
|
2132
|
-
const info = makeTargetInfo(singleTarget);
|
|
2133
|
-
const v = unwrap(rawValue);
|
|
2134
|
-
const coerced = await coerceIfNeeded(typeId, info?.inType, v);
|
|
2135
|
-
ctx.wb.addNode({
|
|
2136
|
-
nodeId: undefined,
|
|
2137
|
-
typeId: String(singleTarget.nodeTypeId),
|
|
2138
|
-
position: { x: pos.x + 180, y: pos.y },
|
|
2139
|
-
params: {},
|
|
2140
|
-
initialInputs: {
|
|
2141
|
-
[String(singleTarget.inputHandle || "Value")]: clone(coerced),
|
|
2142
|
-
},
|
|
2143
|
-
});
|
|
2144
|
-
return;
|
|
2145
|
-
}
|
|
2146
|
-
if (isArray && arrTarget) {
|
|
2147
|
-
const info = makeTargetInfo(arrTarget);
|
|
2148
|
-
const v = unwrap(rawValue);
|
|
2149
|
-
const coerced = await coerceIfNeeded(typeId, info?.inType, v);
|
|
2150
|
-
ctx.wb.addNode({
|
|
2151
|
-
nodeId: undefined,
|
|
2152
|
-
typeId: String(arrTarget.nodeTypeId),
|
|
2153
|
-
position: { x: pos.x + 180, y: pos.y },
|
|
2154
|
-
params: {},
|
|
2155
|
-
initialInputs: {
|
|
2156
|
-
[String(arrTarget.inputHandle || "Value")]: clone(coerced),
|
|
2157
|
-
},
|
|
2158
|
-
});
|
|
2159
|
-
return;
|
|
2160
|
-
}
|
|
2161
|
-
if (isArray && elemTarget && Array.isArray(rawValue)) {
|
|
2162
|
-
const info = makeTargetInfo(elemTarget);
|
|
2163
|
-
const items = rawValue.map(unwrap);
|
|
2164
|
-
const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, info?.inType, v)));
|
|
2165
|
-
const COLS = 4;
|
|
2166
|
-
const DX = 180;
|
|
2167
|
-
const DY = 160;
|
|
2168
|
-
coercedItems.forEach((cv, idx) => {
|
|
2169
|
-
const col = idx % COLS;
|
|
2170
|
-
const row = Math.floor(idx / COLS);
|
|
2171
|
-
ctx.wb.addNode({
|
|
2172
|
-
nodeId: undefined,
|
|
2173
|
-
typeId: String(elemTarget.nodeTypeId),
|
|
2174
|
-
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
2175
|
-
params: {},
|
|
2176
|
-
initialInputs: {
|
|
2177
|
-
[String(elemTarget.inputHandle || "Value")]: clone(cv),
|
|
2178
|
-
},
|
|
2179
|
-
});
|
|
2180
|
-
});
|
|
2181
|
-
return;
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
catch { }
|
|
2185
|
-
}, [ctx, id]);
|
|
2186
2160
|
return (jsxs(Fragment, { children: [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 }) => {
|
|
2187
2161
|
const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === id);
|
|
2188
2162
|
const hasAny = vIssues.length > 0;
|
|
@@ -2210,26 +2184,7 @@ function DefaultNodeContent({ id, data, isConnectable, }) {
|
|
|
2210
2184
|
const txt = toString(resolved.typeId, resolved.value);
|
|
2211
2185
|
return typeof txt === "string" ? txt : String(txt);
|
|
2212
2186
|
})();
|
|
2213
|
-
|
|
2214
|
-
let canBake = false;
|
|
2215
|
-
if (tId?.endsWith("[]")) {
|
|
2216
|
-
const base = tId.slice(0, -2);
|
|
2217
|
-
const tArr = ctx.registry?.types.get(tId);
|
|
2218
|
-
const tElem = ctx.registry?.types.get(base);
|
|
2219
|
-
const arrTarget = tArr?.bakeTarget;
|
|
2220
|
-
const elemTarget = tElem?.bakeTarget;
|
|
2221
|
-
canBake = !!((arrTarget && ctx.registry?.nodes?.has?.(arrTarget.nodeTypeId)) ||
|
|
2222
|
-
(elemTarget && ctx.registry?.nodes?.has?.(elemTarget.nodeTypeId)));
|
|
2223
|
-
}
|
|
2224
|
-
else if (tId) {
|
|
2225
|
-
const t = ctx.registry?.types.get(tId);
|
|
2226
|
-
const target = t?.bakeTarget;
|
|
2227
|
-
canBake = !!(target && ctx.registry?.nodes?.has?.(target.nodeTypeId));
|
|
2228
|
-
}
|
|
2229
|
-
return (jsxs("span", { className: "flex items-center gap-1 w-full", children: [kind === "output" ? (jsxs(Fragment, { children: [canBake && (jsx("button", { onClick: (e) => {
|
|
2230
|
-
e.stopPropagation();
|
|
2231
|
-
handleBake(handleId);
|
|
2232
|
-
}, title: "Bake value", className: "pointer-events-auto border border-gray-300 rounded px-1 py-0.5 text-[10px] bg-white/80 hover:bg-white mr-2", children: "Bake" })), 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: handleId })] })) : (jsxs(Fragment, { children: [jsx("span", { className: "truncate shrink-0", style: { maxWidth: "40%" }, children: handleId }), 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 }))] }));
|
|
2187
|
+
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: handleId })] })) : (jsxs(Fragment, { children: [jsx("span", { className: "truncate shrink-0", style: { maxWidth: "40%" }, children: handleId }), 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 }))] }));
|
|
2233
2188
|
} })] }));
|
|
2234
2189
|
}
|
|
2235
2190
|
|
|
@@ -2320,7 +2275,7 @@ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
|
|
|
2320
2275
|
}
|
|
2321
2276
|
|
|
2322
2277
|
function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
|
|
2323
|
-
const { wb, runner, engineKind } = useWorkbenchContext();
|
|
2278
|
+
const { wb, runner, engineKind, registry, outputsMap, outputTypesMap } = useWorkbenchContext();
|
|
2324
2279
|
const ref = useRef(null);
|
|
2325
2280
|
// outside click + ESC
|
|
2326
2281
|
useEffect(() => {
|
|
@@ -2347,47 +2302,182 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
|
|
|
2347
2302
|
if (open)
|
|
2348
2303
|
ref.current?.focus();
|
|
2349
2304
|
}, [open]);
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2305
|
+
// Bake helpers
|
|
2306
|
+
const getBakeableOutputs = () => {
|
|
2307
|
+
try {
|
|
2308
|
+
const def = wb.export();
|
|
2309
|
+
const node = def.nodes.find((n) => n.nodeId === nodeId);
|
|
2310
|
+
if (!node)
|
|
2311
|
+
return [];
|
|
2312
|
+
const desc = registry.nodes.get(node.typeId);
|
|
2313
|
+
const handles = Object.keys(desc?.outputs || {});
|
|
2314
|
+
const out = [];
|
|
2315
|
+
for (const h of handles) {
|
|
2316
|
+
const tId = outputTypesMap?.[nodeId]?.[h];
|
|
2317
|
+
if (!tId)
|
|
2318
|
+
continue;
|
|
2319
|
+
if (tId.endsWith("[]")) {
|
|
2320
|
+
const base = tId.slice(0, -2);
|
|
2321
|
+
const tArr = registry.types.get(tId);
|
|
2322
|
+
const tElem = registry.types.get(base);
|
|
2323
|
+
const arrT = tArr?.bakeTarget;
|
|
2324
|
+
const elemT = tElem?.bakeTarget;
|
|
2325
|
+
if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
|
|
2326
|
+
(elemT && registry.nodes.has(elemT.nodeTypeId)))
|
|
2327
|
+
out.push(h);
|
|
2328
|
+
}
|
|
2329
|
+
else {
|
|
2330
|
+
const t = registry.types.get(tId);
|
|
2331
|
+
const bt = t?.bakeTarget;
|
|
2332
|
+
if (bt && registry.nodes.has(bt.nodeTypeId))
|
|
2333
|
+
out.push(h);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
return out;
|
|
2337
|
+
}
|
|
2338
|
+
catch {
|
|
2339
|
+
return [];
|
|
2340
|
+
}
|
|
2341
|
+
};
|
|
2342
|
+
const doBake = async (handleId) => {
|
|
2343
|
+
try {
|
|
2344
|
+
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
2345
|
+
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
2346
|
+
if (!typeId || raw === undefined)
|
|
2347
|
+
return;
|
|
2348
|
+
const unwrap = (v) => isTypedOutput(v) ? getTypedOutputValue(v) : v;
|
|
2349
|
+
const clone = (v) => typeof structuredClone === "function"
|
|
2350
|
+
? structuredClone(v)
|
|
2351
|
+
: JSON.parse(JSON.stringify(v));
|
|
2352
|
+
const coerceIfNeeded = async (fromType, toType, value) => {
|
|
2353
|
+
if (!toType || toType === fromType || !runner?.coerce)
|
|
2354
|
+
return value;
|
|
2355
|
+
try {
|
|
2356
|
+
return await runner.coerce(fromType, toType, value);
|
|
2357
|
+
}
|
|
2358
|
+
catch {
|
|
2359
|
+
return value;
|
|
2360
|
+
}
|
|
2361
|
+
};
|
|
2362
|
+
const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
|
|
2363
|
+
const isArray = typeId.endsWith("[]");
|
|
2364
|
+
const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
|
|
2365
|
+
const tArr = isArray ? registry.types.get(typeId) : undefined;
|
|
2366
|
+
const tElem = registry.types.get(baseTypeId);
|
|
2367
|
+
const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
|
|
2368
|
+
const arrTarget = isArray ? tArr?.bakeTarget : undefined;
|
|
2369
|
+
const elemTarget = isArray ? tElem?.bakeTarget : undefined;
|
|
2370
|
+
if (singleTarget) {
|
|
2371
|
+
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
2372
|
+
const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
2373
|
+
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
2374
|
+
const newId = wb.addNode({
|
|
2375
|
+
typeId: singleTarget.nodeTypeId,
|
|
2376
|
+
position: { x: pos.x + 180, y: pos.y },
|
|
2377
|
+
params: {},
|
|
2378
|
+
});
|
|
2379
|
+
runner.update(wb.export());
|
|
2380
|
+
await runner.whenIdle();
|
|
2381
|
+
runner.setInputs(newId, { [singleTarget.inputHandle]: coerced });
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
if (isArray && arrTarget) {
|
|
2385
|
+
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
2386
|
+
const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
2387
|
+
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
2388
|
+
const newId = `n${Math.random().toString(36).slice(2, 8)}`;
|
|
2389
|
+
wb.addNode({
|
|
2390
|
+
nodeId: newId,
|
|
2391
|
+
typeId: arrTarget.nodeTypeId,
|
|
2392
|
+
position: { x: pos.x + 180, y: pos.y },
|
|
2393
|
+
params: {},
|
|
2394
|
+
});
|
|
2395
|
+
runner.update(wb.export());
|
|
2396
|
+
await runner.whenIdle();
|
|
2397
|
+
runner.setInputs(newId, { [arrTarget.inputHandle]: coerced });
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
if (isArray && elemTarget) {
|
|
2401
|
+
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
2402
|
+
const inType = getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
2403
|
+
const src = unwrap(raw);
|
|
2404
|
+
const items = Array.isArray(src) ? src : [src];
|
|
2405
|
+
const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
|
|
2406
|
+
const COLS = 4;
|
|
2407
|
+
const DX = 180;
|
|
2408
|
+
const DY = 160;
|
|
2409
|
+
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
2410
|
+
const cv = coercedItems[idx];
|
|
2411
|
+
const col = idx % COLS;
|
|
2412
|
+
const row = Math.floor(idx / COLS);
|
|
2413
|
+
const newId = wb.addNode({
|
|
2414
|
+
typeId: elemTarget.nodeTypeId,
|
|
2415
|
+
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
2416
|
+
params: {},
|
|
2417
|
+
initialInputs: { [elemTarget.inputHandle]: clone(cv) },
|
|
2418
|
+
});
|
|
2419
|
+
runner.update(wb.export());
|
|
2420
|
+
await runner.whenIdle();
|
|
2421
|
+
runner.setInputs(newId, { [elemTarget.inputHandle]: cv });
|
|
2422
|
+
}
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
catch { }
|
|
2427
|
+
};
|
|
2358
2428
|
// actions
|
|
2359
|
-
const handleDelete = () => {
|
|
2429
|
+
const handleDelete = useCallback(() => {
|
|
2360
2430
|
wb.removeNode(nodeId);
|
|
2361
2431
|
onClose();
|
|
2362
|
-
};
|
|
2363
|
-
const handleDuplicate = () => {
|
|
2432
|
+
}, [nodeId, wb, onClose]);
|
|
2433
|
+
const handleDuplicate = useCallback(() => {
|
|
2364
2434
|
const def = wb.export();
|
|
2365
2435
|
const n = def.nodes.find((n) => n.nodeId === nodeId);
|
|
2366
2436
|
if (!n)
|
|
2367
2437
|
return onClose();
|
|
2368
2438
|
const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
|
|
2369
|
-
wb.addNode({
|
|
2439
|
+
wb.addNode({
|
|
2440
|
+
typeId: n.typeId,
|
|
2441
|
+
params: n.params,
|
|
2442
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
2443
|
+
});
|
|
2370
2444
|
onClose();
|
|
2371
|
-
};
|
|
2372
|
-
|
|
2445
|
+
}, [nodeId, wb, onClose]);
|
|
2446
|
+
useCallback(async (handleId) => {
|
|
2447
|
+
await doBake(handleId);
|
|
2448
|
+
onClose();
|
|
2449
|
+
}, [doBake, onClose]);
|
|
2450
|
+
const handleCopyId = useCallback(async () => {
|
|
2373
2451
|
try {
|
|
2374
2452
|
await navigator.clipboard.writeText(nodeId);
|
|
2375
2453
|
}
|
|
2376
2454
|
catch { }
|
|
2377
2455
|
onClose();
|
|
2378
|
-
};
|
|
2379
|
-
const
|
|
2380
|
-
const handleRunPull = async () => {
|
|
2456
|
+
}, [nodeId, onClose]);
|
|
2457
|
+
const handleRunPull = useCallback(async () => {
|
|
2381
2458
|
try {
|
|
2382
2459
|
await runner.computeNode(nodeId);
|
|
2383
2460
|
}
|
|
2384
2461
|
catch { }
|
|
2385
2462
|
onClose();
|
|
2386
|
-
};
|
|
2463
|
+
}, [nodeId, runner, onClose]);
|
|
2464
|
+
if (!open || !clientPos || !nodeId)
|
|
2465
|
+
return null;
|
|
2466
|
+
// clamp
|
|
2467
|
+
const MENU_MIN_WIDTH = 180;
|
|
2468
|
+
const PADDING = 16;
|
|
2469
|
+
const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
|
|
2470
|
+
(MENU_MIN_WIDTH + PADDING));
|
|
2471
|
+
const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
|
|
2472
|
+
const canRunPull = engineKind()?.toString() === "pull";
|
|
2473
|
+
const outs = getBakeableOutputs();
|
|
2387
2474
|
return (jsxs("div", { ref: ref, tabIndex: -1, 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: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
|
|
2388
2475
|
e.preventDefault();
|
|
2389
2476
|
e.stopPropagation();
|
|
2390
|
-
}, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDuplicate, children: "Duplicate" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick:
|
|
2477
|
+
}, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDuplicate, children: "Duplicate" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), outs.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), outs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: async () => {
|
|
2478
|
+
await doBake(h);
|
|
2479
|
+
onClose();
|
|
2480
|
+
}, children: ["Bake: ", h] }, h))), jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleCopyId, children: "Copy Node ID" })] }));
|
|
2391
2481
|
}
|
|
2392
2482
|
|
|
2393
2483
|
const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
|
|
@@ -2428,6 +2518,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
2428
2518
|
outputValues: n.data.outputValues,
|
|
2429
2519
|
status: n.data.status,
|
|
2430
2520
|
validation: n.data.validation,
|
|
2521
|
+
inputConnected: n.data.inputConnected,
|
|
2431
2522
|
},
|
|
2432
2523
|
});
|
|
2433
2524
|
return isEqual(pick(a), pick(b));
|
|
@@ -2617,10 +2708,21 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
2617
2708
|
setNodeMenuOpen(false);
|
|
2618
2709
|
}
|
|
2619
2710
|
};
|
|
2620
|
-
const addNodeAt = (typeId, pos) => {
|
|
2711
|
+
const addNodeAt = useCallback((typeId, pos) => {
|
|
2621
2712
|
wb.addNode({ typeId, position: pos });
|
|
2622
|
-
};
|
|
2623
|
-
|
|
2713
|
+
}, [wb]);
|
|
2714
|
+
useCallback((inst) => {
|
|
2715
|
+
rfInstanceRef.current = inst;
|
|
2716
|
+
}, []);
|
|
2717
|
+
const onCloseMenu = useCallback(() => {
|
|
2718
|
+
setMenuOpen(false);
|
|
2719
|
+
}, []);
|
|
2720
|
+
const onCloseNodeMenu = useCallback(() => {
|
|
2721
|
+
setNodeMenuOpen(false);
|
|
2722
|
+
}, []);
|
|
2723
|
+
return (jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsx(ReactFlowProvider, { children: jsxs(ReactFlow, { nodes: throttled.nodes, edges: throttled.edges, nodeTypes: nodeTypes, selectionOnDrag: true, onInit: (inst) => {
|
|
2724
|
+
rfInstanceRef.current = inst;
|
|
2725
|
+
}, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [jsx(Background, { id: "workbench-canvas-background", variant: BackgroundVariant.Dots, gap: 12, size: 1 }), jsx(MiniMap, {}), jsx(Controls, {}), jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: onCloseMenu }), !!nodeAtMenu && (jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: onCloseNodeMenu }))] }) }) }));
|
|
2624
2726
|
});
|
|
2625
2727
|
|
|
2626
2728
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
|
|
@@ -2675,6 +2777,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2675
2777
|
const lastAutoLaunched = useRef(undefined);
|
|
2676
2778
|
const autoLayoutRan = useRef(false);
|
|
2677
2779
|
const canvasRef = useRef(null);
|
|
2780
|
+
const uploadInputRef = useRef(null);
|
|
2678
2781
|
// Expose init callback with setInitialGraph helper
|
|
2679
2782
|
const initCalled = useRef(false);
|
|
2680
2783
|
useEffect(() => {
|
|
@@ -2779,6 +2882,66 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2779
2882
|
alert(String(err?.message ?? err));
|
|
2780
2883
|
}
|
|
2781
2884
|
}, [wb, runner]);
|
|
2885
|
+
const onUploadPicked = useCallback(async (e) => {
|
|
2886
|
+
try {
|
|
2887
|
+
const file = e.target.files?.[0];
|
|
2888
|
+
if (!file)
|
|
2889
|
+
return;
|
|
2890
|
+
const text = await file.text();
|
|
2891
|
+
const parsed = JSON.parse(text);
|
|
2892
|
+
// Support both Graph and Snapshot payloads
|
|
2893
|
+
const isSnapshot = parsed &&
|
|
2894
|
+
typeof parsed === "object" &&
|
|
2895
|
+
(parsed.def || parsed.inputs || parsed.outputs || parsed.environment);
|
|
2896
|
+
if (isSnapshot) {
|
|
2897
|
+
const def = parsed.def;
|
|
2898
|
+
const positions = parsed.positions || {};
|
|
2899
|
+
const environment = parsed.environment || {};
|
|
2900
|
+
const inputs = parsed.inputs || {};
|
|
2901
|
+
if (def) {
|
|
2902
|
+
// Remote exact restore path
|
|
2903
|
+
await runner.applySnapshotFull({
|
|
2904
|
+
def,
|
|
2905
|
+
environment,
|
|
2906
|
+
inputs,
|
|
2907
|
+
outputs: parsed.outputs || {},
|
|
2908
|
+
});
|
|
2909
|
+
await wb.load(def);
|
|
2910
|
+
if (positions && typeof positions === "object")
|
|
2911
|
+
wb.setPositions(positions);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
else {
|
|
2915
|
+
const def = parsed?.def ?? parsed;
|
|
2916
|
+
const inputs = parsed?.inputs ?? {};
|
|
2917
|
+
await wb.load(def);
|
|
2918
|
+
try {
|
|
2919
|
+
runner.build(wb.export());
|
|
2920
|
+
}
|
|
2921
|
+
catch { }
|
|
2922
|
+
if (inputs && typeof inputs === "object") {
|
|
2923
|
+
for (const [nodeId, map] of Object.entries(inputs)) {
|
|
2924
|
+
try {
|
|
2925
|
+
runner.setInputs(nodeId, map);
|
|
2926
|
+
}
|
|
2927
|
+
catch { }
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
runAutoLayout();
|
|
2932
|
+
}
|
|
2933
|
+
catch (err) {
|
|
2934
|
+
alert(String(err?.message ?? err));
|
|
2935
|
+
}
|
|
2936
|
+
finally {
|
|
2937
|
+
// reset input so same file can be picked again
|
|
2938
|
+
if (uploadInputRef.current)
|
|
2939
|
+
uploadInputRef.current.value = "";
|
|
2940
|
+
}
|
|
2941
|
+
}, [wb, runner, runAutoLayout]);
|
|
2942
|
+
const triggerUpload = useCallback(() => {
|
|
2943
|
+
uploadInputRef.current?.click();
|
|
2944
|
+
}, []);
|
|
2782
2945
|
const hydrateFromBackend = useCallback(async (kind, base) => {
|
|
2783
2946
|
try {
|
|
2784
2947
|
const transport = kind === "remote-http"
|
|
@@ -3076,7 +3239,35 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3076
3239
|
catch (err) {
|
|
3077
3240
|
alert(String(err?.message ?? err));
|
|
3078
3241
|
}
|
|
3079
|
-
}, disabled: !engine, children: "Start" })), jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: "Fit View" }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }),
|
|
3242
|
+
}, disabled: !engine, children: "Start" })), jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: "Fit View" }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: async () => {
|
|
3243
|
+
try {
|
|
3244
|
+
const def = wb.export();
|
|
3245
|
+
const positions = wb.getPositions();
|
|
3246
|
+
const snapshot = await runner.snapshotFull();
|
|
3247
|
+
const payload = {
|
|
3248
|
+
...snapshot,
|
|
3249
|
+
def,
|
|
3250
|
+
positions,
|
|
3251
|
+
schemaVersion: 1,
|
|
3252
|
+
};
|
|
3253
|
+
const pretty = JSON.stringify(payload, null, 2);
|
|
3254
|
+
const blob = new Blob([pretty], { type: "application/json" });
|
|
3255
|
+
const url = URL.createObjectURL(blob);
|
|
3256
|
+
const a = document.createElement("a");
|
|
3257
|
+
const d = new Date();
|
|
3258
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
3259
|
+
const ts = `${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
|
|
3260
|
+
a.href = url;
|
|
3261
|
+
a.download = `spark-snapshot-${ts}.json`;
|
|
3262
|
+
document.body.appendChild(a);
|
|
3263
|
+
a.click();
|
|
3264
|
+
a.remove();
|
|
3265
|
+
URL.revokeObjectURL(url);
|
|
3266
|
+
}
|
|
3267
|
+
catch (err) {
|
|
3268
|
+
alert(String(err?.message ?? err));
|
|
3269
|
+
}
|
|
3270
|
+
}, children: "Download Snapshot" }), jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: triggerUpload, children: "Upload Graph/Snapshot" }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx("span", { children: "Debug events" })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx("span", { children: "Show values in nodes" })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement, contextPanel: overrides?.contextPanel })] })] }));
|
|
3080
3271
|
}
|
|
3081
3272
|
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, onInit, onChange, }) {
|
|
3082
3273
|
const [registry, setRegistry] = useState(createSimpleGraphRegistry());
|