@bian-womp/spark-workbench 0.1.18 → 0.1.19

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
@@ -659,6 +659,14 @@ function useWorkbenchBridge(wb) {
659
659
  return;
660
660
  if (!params.sourceHandle || !params.targetHandle)
661
661
  return;
662
+ // Prevent duplicate edges between the same endpoints
663
+ const def = wb.export();
664
+ const exists = def.edges.some((e) => e.source.nodeId === params.source &&
665
+ e.source.handle === params.sourceHandle &&
666
+ e.target.nodeId === params.target &&
667
+ e.target.handle === params.targetHandle);
668
+ if (exists)
669
+ return;
662
670
  wb.connect({
663
671
  source: { nodeId: params.source, handle: params.sourceHandle },
664
672
  target: { nodeId: params.target, handle: params.targetHandle },
@@ -916,7 +924,9 @@ function toReactFlow(def, positions, registry, opts) {
916
924
  const nodeHandleMap = {};
917
925
  const nodes = def.nodes.map((n) => {
918
926
  const desc = registry.nodes.get(n.typeId);
919
- const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
927
+ const inputHandles = Object.entries(desc?.inputs ?? {})
928
+ .filter(([id]) => !sparkGraph.isInputPrivate(desc?.inputs, id))
929
+ .map(([id, v]) => ({ id, typeId: sparkGraph.getInputTypeId(desc?.inputs, id) }));
920
930
  const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId: formatDeclaredTypeSignature(typeId) }));
921
931
  nodeHandleMap[n.nodeId] = {
922
932
  inputs: new Set(inputHandles.map((h) => h.id)),
@@ -1110,8 +1120,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1110
1120
  layers.push(layer);
1111
1121
  q.splice(0, q.length, ...next);
1112
1122
  }
1113
- const X = 480;
1114
- const Y = 240;
1123
+ const X = 960;
1124
+ const Y = 480;
1115
1125
  const pos = {};
1116
1126
  layers.forEach((layer, layerIndex) => {
1117
1127
  layer.forEach((id, itemIndex) => {
@@ -1521,7 +1531,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1521
1531
  const selectedDesc = selectedNode
1522
1532
  ? registry.nodes.get(selectedNode.typeId)
1523
1533
  : undefined;
1524
- const inputHandles = Object.keys(selectedDesc?.inputs ?? {});
1534
+ const inputHandles = Object.entries(selectedDesc?.inputs ?? {})
1535
+ .filter(([k]) => !sparkGraph.isInputPrivate(selectedDesc?.inputs, k))
1536
+ .map(([k]) => k);
1525
1537
  const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
1526
1538
  const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
1527
1539
  const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
@@ -1568,7 +1580,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1568
1580
  const nextDrafts = { ...drafts };
1569
1581
  const nextOriginals = { ...originals };
1570
1582
  for (const h of handles) {
1571
- const typeId = desc?.inputs?.[h];
1583
+ const typeId = sparkGraph.getInputTypeId(desc?.inputs, h);
1572
1584
  const current = nodeInputs[h];
1573
1585
  const display = safeToString(typeId, current);
1574
1586
  const wasOriginal = originals[h];
@@ -1588,7 +1600,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1588
1600
  const widthClass = debug ? "w-[480px]" : "w-[320px]";
1589
1601
  return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", 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}` })] }, 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] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.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: selectedEdgeValidation.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}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.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 ??
1590
1602
  selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
1591
- const typeId = (selectedDesc?.inputs ?? {})[h];
1603
+ const typeId = sparkGraph.getInputTypeId(selectedDesc?.inputs, h);
1592
1604
  const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
1593
1605
  e.target.handle === h);
1594
1606
  const commonProps = {
@@ -1616,7 +1628,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1616
1628
  const title = inIssues
1617
1629
  .map((v) => `${v.code}: ${v.message}`)
1618
1630
  .join("; ");
1619
- return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32", children: [h, jsxRuntime.jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.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: current !== undefined && current !== null
1631
+ return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: h }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.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: current !== undefined && current !== null
1620
1632
  ? String(current)
1621
1633
  : "", onChange: (e) => {
1622
1634
  const val = e.target.value;
@@ -1954,7 +1966,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1954
1966
  const selectedDesc = selectedNode
1955
1967
  ? registry.nodes.get(selectedNode.typeId)
1956
1968
  : undefined;
1957
- const [exampleState, setExampleState] = React.useState(example ?? "simple");
1969
+ const [exampleState, setExampleState] = React.useState(example ?? "");
1958
1970
  const defaultExamples = React.useMemo(() => [
1959
1971
  {
1960
1972
  id: "simple",
@@ -2111,7 +2123,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2111
2123
  }, [setRegistry, wb]);
2112
2124
  // Ensure initial example is loaded (and sync when example prop changes)
2113
2125
  React.useEffect(() => {
2114
- applyExample(example ?? "simple");
2126
+ if (!example)
2127
+ return;
2128
+ applyExample(example);
2115
2129
  }, [example, wb]);
2116
2130
  React.useEffect(() => {
2117
2131
  if (!engine)
@@ -2182,7 +2196,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2182
2196
  break;
2183
2197
  }
2184
2198
  case "base.bool": {
2185
- value = Boolean(raw);
2199
+ value = raw === "true" || raw === "1" ? true : false;
2186
2200
  break;
2187
2201
  }
2188
2202
  case "base.string": {
@@ -2319,14 +2333,14 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2319
2333
  return overrides.toElement(baseToElement, { registry });
2320
2334
  return baseToElement;
2321
2335
  }, [overrides, baseToElement, registry]);
2322
- return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.jsx("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()
2336
+ return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.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()
2323
2337
  ? "Stop engine before switching example"
2324
- : undefined, children: examples.map((ex) => (jsxRuntime.jsx("option", { value: ex.id, children: ex.label }, ex.id))) }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxRuntime.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()
2338
+ : undefined, children: [jsxRuntime.jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsxRuntime.jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxRuntime.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()
2325
2339
  ? "Stop engine before switching backend"
2326
2340
  : undefined, children: [jsxRuntime.jsx("option", { value: "local", children: "Local" }), jsxRuntime.jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsxRuntime.jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && (jsxRuntime.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" && (jsxRuntime.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) })), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: (e) => {
2327
2341
  const kind = e.target.value || undefined;
2328
2342
  onEngineChange?.(kind);
2329
- }, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2", onClick: () => runner.step(), disabled: !runner.isRunning(), children: "Step" })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2", onClick: () => runner.flush(), disabled: !runner.isRunning(), children: "Flush" })), runner.isRunning() ? (jsxRuntime.jsx("button", { onClick: () => runner.dispose(), disabled: !runner.isRunning(), children: "Stop" })) : (jsxRuntime.jsx("button", { onClick: () => {
2343
+ }, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.step(), disabled: !runner.isRunning(), children: "Step" })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.flush(), disabled: !runner.isRunning(), children: "Flush" })), runner.isRunning() ? (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.dispose(), disabled: !runner.isRunning(), children: "Stop" })) : (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: () => {
2330
2344
  const kind = engine;
2331
2345
  if (!kind)
2332
2346
  return alert("Select an engine first.");
@@ -2336,7 +2350,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2336
2350
  catch (err) {
2337
2351
  alert(String(err?.message ?? err));
2338
2352
  }
2339
- }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsx("button", { className: "ml-2", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: "Fit View" }), jsxRuntime.jsx("button", { className: "ml-2", onClick: downloadGraph, children: "Download Graph" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
2353
+ }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.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" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
2340
2354
  }
2341
2355
  function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
2342
2356
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
@@ -2352,9 +2366,9 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
2352
2366
  // Allow external UI registration (e.g., node renderers) with access to wb
2353
2367
  React.useEffect(() => {
2354
2368
  const baseRegisterUI = (_wb) => { };
2355
- overrides?.registerUI?.(baseRegisterUI, { wb });
2369
+ overrides?.registerUI?.(baseRegisterUI, { wb, wbRunner: runner });
2356
2370
  // eslint-disable-next-line react-hooks/exhaustive-deps
2357
- }, [wb, overrides]);
2371
+ }, [wb, runner, overrides]);
2358
2372
  return (jsxRuntime.jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, children: jsxRuntime.jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
2359
2373
  if (runner.isRunning())
2360
2374
  runner.dispose();