@bian-womp/spark-workbench 0.2.36 → 0.2.37

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 CHANGED
@@ -343,7 +343,12 @@ class AbstractGraphRunner {
343
343
  setInput(nodeId, handle, value) {
344
344
  if (!this.stagedInputs[nodeId])
345
345
  this.stagedInputs[nodeId] = {};
346
- this.stagedInputs[nodeId][handle] = value;
346
+ if (value === undefined) {
347
+ delete this.stagedInputs[nodeId][handle];
348
+ }
349
+ else {
350
+ this.stagedInputs[nodeId][handle] = value;
351
+ }
347
352
  if (this.engine) {
348
353
  this.engine.setInput(nodeId, handle, value);
349
354
  }
@@ -361,7 +366,14 @@ class AbstractGraphRunner {
361
366
  return;
362
367
  if (!this.stagedInputs[nodeId])
363
368
  this.stagedInputs[nodeId] = {};
364
- Object.assign(this.stagedInputs[nodeId], inputs);
369
+ for (const [handle, value] of Object.entries(inputs)) {
370
+ if (value === undefined) {
371
+ delete this.stagedInputs[nodeId][handle];
372
+ }
373
+ else {
374
+ this.stagedInputs[nodeId][handle] = value;
375
+ }
376
+ }
365
377
  if (this.engine) {
366
378
  // Running: set all inputs
367
379
  this.engine.setInputs(nodeId, inputs);
@@ -1738,14 +1750,19 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1738
1750
  const clearEvents = React.useCallback(() => setEvents([]), []);
1739
1751
  const [systemErrors, setSystemErrors] = React.useState([]);
1740
1752
  const [registryErrors, setRegistryErrors] = React.useState([]);
1753
+ const [inputValidationErrors, setInputValidationErrors] = React.useState([]);
1741
1754
  const clearSystemErrors = React.useCallback(() => setSystemErrors([]), []);
1742
1755
  const clearRegistryErrors = React.useCallback(() => setRegistryErrors([]), []);
1756
+ const clearInputValidationErrors = React.useCallback(() => setInputValidationErrors([]), []);
1743
1757
  const removeSystemError = React.useCallback((index) => {
1744
1758
  setSystemErrors((prev) => prev.filter((_, idx) => idx !== index));
1745
1759
  }, []);
1746
1760
  const removeRegistryError = React.useCallback((index) => {
1747
1761
  setRegistryErrors((prev) => prev.filter((_, idx) => idx !== index));
1748
1762
  }, []);
1763
+ const removeInputValidationError = React.useCallback((index) => {
1764
+ setInputValidationErrors((prev) => prev.filter((_, idx) => idx !== index));
1765
+ }, []);
1749
1766
  // Fallback progress animation: drive progress to 100% over ~2 minutes
1750
1767
  const FALLBACK_TOTAL_MS = 2 * 60 * 1000;
1751
1768
  const [fallbackStarts, setFallbackStarts] = React.useState({});
@@ -1801,7 +1818,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1801
1818
  const out = {};
1802
1819
  // Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
1803
1820
  for (const n of def.nodes) {
1804
- const outputsDecl = registry.nodes.get(n.typeId)?.outputs ?? {};
1821
+ const effectiveHandles = computeEffectiveHandles(n, registry);
1822
+ const outputsDecl = effectiveHandles.outputs;
1805
1823
  const handles = Object.keys(outputsDecl);
1806
1824
  const cur = {};
1807
1825
  for (const h of handles) {
@@ -1962,10 +1980,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1962
1980
  const offRunnerValue = runner.on("value", (e) => {
1963
1981
  if (e?.io === "input") {
1964
1982
  const nodeId = e?.nodeId;
1983
+ const handle = e?.handle;
1965
1984
  setNodeStatus((s) => ({
1966
1985
  ...s,
1967
1986
  [nodeId]: { ...s[nodeId], invalidated: true },
1968
1987
  }));
1988
+ // Clear validation errors for this input when a valid value is set
1989
+ setInputValidationErrors((prev) => prev.filter((err) => !(err.nodeId === nodeId && err.handle === handle)));
1969
1990
  }
1970
1991
  return add("runner", "value")(e);
1971
1992
  });
@@ -1974,6 +1995,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
1974
1995
  const nodeError = e;
1975
1996
  const registryError = e;
1976
1997
  const systemError = e;
1998
+ const inputValidationError = e;
1977
1999
  if (edgeError.kind === "edge-convert") {
1978
2000
  const edgeId = edgeError.edgeId;
1979
2001
  setEdgeStatus((s) => ({
@@ -2009,6 +2031,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2009
2031
  return [...prev, registryError];
2010
2032
  });
2011
2033
  }
2034
+ else if (inputValidationError.kind === "input-validation") {
2035
+ // Track input validation errors for UI display
2036
+ setInputValidationErrors((prev) => {
2037
+ // Avoid duplicates by checking nodeId, handle, and typeId
2038
+ if (prev.some((err) => err.nodeId === inputValidationError.nodeId &&
2039
+ err.handle === inputValidationError.handle &&
2040
+ err.typeId === inputValidationError.typeId)) {
2041
+ return prev;
2042
+ }
2043
+ return [...prev, inputValidationError];
2044
+ });
2045
+ }
2012
2046
  else if (systemError.kind === "system") {
2013
2047
  // Track custom errors for UI display
2014
2048
  setSystemErrors((prev) => {
@@ -2132,7 +2166,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2132
2166
  }
2133
2167
  return add("runner", "stats")(s);
2134
2168
  });
2135
- const offWbGraphChanged = wb.on("graphChanged", add("workbench", "graphChanged"));
2169
+ const offWbGraphChanged = wb.on("graphChanged", (event) => {
2170
+ // Clear validation errors for removed nodes
2171
+ if (event.change?.type === "removeNode") {
2172
+ const removedNodeId = event.change.nodeId;
2173
+ setInputValidationErrors((prev) => prev.filter((err) => err.nodeId !== removedNodeId));
2174
+ }
2175
+ return add("workbench", "graphChanged")(event);
2176
+ });
2136
2177
  const offWbGraphUiChanged = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
2137
2178
  const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
2138
2179
  // Ensure newly added nodes start as invalidated until first evaluation
@@ -2302,10 +2343,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2302
2343
  clearEvents,
2303
2344
  systemErrors,
2304
2345
  registryErrors,
2346
+ inputValidationErrors,
2305
2347
  clearSystemErrors,
2306
2348
  clearRegistryErrors,
2349
+ clearInputValidationErrors,
2307
2350
  removeSystemError,
2308
2351
  removeRegistryError,
2352
+ removeInputValidationError,
2309
2353
  isRunning,
2310
2354
  engineKind,
2311
2355
  start,
@@ -2330,10 +2374,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2330
2374
  valuesTick,
2331
2375
  systemErrors,
2332
2376
  registryErrors,
2377
+ inputValidationErrors,
2333
2378
  clearSystemErrors,
2334
2379
  clearRegistryErrors,
2380
+ clearInputValidationErrors,
2335
2381
  removeSystemError,
2336
2382
  removeRegistryError,
2383
+ removeInputValidationError,
2337
2384
  inputsMap,
2338
2385
  inputDefaultsMap,
2339
2386
  outputsMap,
@@ -2421,7 +2468,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2421
2468
  return String(value ?? "");
2422
2469
  }
2423
2470
  };
2424
- const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, clearSystemErrors, clearRegistryErrors, removeSystemError, removeRegistryError, } = useWorkbenchContext();
2471
+ const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, inputValidationErrors, clearSystemErrors, clearRegistryErrors, clearInputValidationErrors, removeSystemError, removeRegistryError, removeInputValidationError, } = useWorkbenchContext();
2425
2472
  const nodeValidationIssues = validationByNode.issues;
2426
2473
  const edgeValidationIssues = validationByEdge.issues;
2427
2474
  const nodeValidationHandles = validationByNode;
@@ -2532,7 +2579,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2532
2579
  }
2533
2580
  catch { }
2534
2581
  };
2535
- return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2582
+ return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), jsxRuntime.jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2536
2583
  e.stopPropagation();
2537
2584
  deleteEdgeById(m.data?.edgeId);
2538
2585
  }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), (() => {
@@ -2570,13 +2617,20 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2570
2617
  const current = nodeInputs[h];
2571
2618
  const hasValue = current !== undefined && current !== null;
2572
2619
  const value = drafts[h] ?? safeToString(typeId, current);
2573
- const displayValue = hasValue ? value : "";
2620
+ const displayValue = value;
2574
2621
  const placeholder = hasDefault ? defaultStr : undefined;
2575
2622
  const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
2576
2623
  const commit = () => {
2577
2624
  const draft = drafts[h];
2578
2625
  if (draft === undefined)
2579
2626
  return;
2627
+ // Only commit if draft differs from current value
2628
+ const currentDisplay = safeToString(typeId, current);
2629
+ if (draft === currentDisplay) {
2630
+ // No change, just sync originals without calling setInput
2631
+ setOriginals((o) => ({ ...o, [h]: draft }));
2632
+ return;
2633
+ }
2580
2634
  setInput(h, draft);
2581
2635
  setOriginals((o) => ({ ...o, [h]: draft }));
2582
2636
  };
@@ -3394,9 +3448,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3394
3448
  state: "local",
3395
3449
  });
3396
3450
  const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
3397
- const selectedDesc = selectedNode
3451
+ selectedNode
3398
3452
  ? registry.nodes.get(selectedNode.typeId)
3399
3453
  : undefined;
3454
+ const effectiveHandles = selectedNode
3455
+ ? computeEffectiveHandles(selectedNode, registry)
3456
+ : { inputs: {}, outputs: {}, inputDefaults: {} };
3400
3457
  const [exampleState, setExampleState] = React.useState(example ?? "");
3401
3458
  const defaultExamples = React.useMemo(() => [
3402
3459
  {
@@ -3684,7 +3741,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3684
3741
  const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
3685
3742
  if (isLinked)
3686
3743
  return;
3687
- const typeId = selectedDesc?.inputs?.[handle];
3744
+ const typeId = effectiveHandles.inputs[handle];
3688
3745
  let value = raw;
3689
3746
  const parseArray = (s, map) => {
3690
3747
  const str = String(s).trim();
@@ -3761,7 +3818,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3761
3818
  }
3762
3819
  }
3763
3820
  runner.setInput(selectedNodeId, handle, value);
3764
- }, [selectedNodeId, def.edges, selectedDesc, runner]);
3821
+ }, [selectedNodeId, def.edges, effectiveHandles, runner]);
3765
3822
  const setInput = React.useMemo(() => {
3766
3823
  if (overrides?.setInput) {
3767
3824
  return overrides.setInput(baseSetInput, {