@bian-womp/spark-workbench 0.1.17 → 0.1.18

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
@@ -857,7 +857,7 @@ function preformatValueForDisplay(typeId, value, registry) {
857
857
  }
858
858
  return undefined;
859
859
  }
860
- function summarizeDeep(value, registry) {
860
+ function summarizeDeep(value) {
861
861
  // Strings: summarize data URLs and trim extremely long strings
862
862
  if (typeof value === "string") {
863
863
  if (value.startsWith("data:")) {
@@ -1661,7 +1661,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
1661
1661
  const ROW_SIZE = 22;
1662
1662
  const maxRows = Math.max(inputEntries.length, outputEntries.length);
1663
1663
  const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
1664
- const minWidth = data.showValues ? 320 : 160;
1664
+ const minWidth = data.showValues ? 320 : 240;
1665
1665
  const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
1666
1666
  const hasError = !!status.lastError;
1667
1667
  const hasValidationError = validation.issues.some((i) => i.level === "error");
@@ -1694,7 +1694,13 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
1694
1694
  const title = vIssues
1695
1695
  .map((v) => `${v.code}: ${v.message}`)
1696
1696
  .join("; ");
1697
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.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) } }), jsxRuntime.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 && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
1697
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.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) } }), jsxRuntime.jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: {
1698
+ top: topFor(i) - 8,
1699
+ right: "50%",
1700
+ whiteSpace: "nowrap",
1701
+ overflow: "hidden",
1702
+ textOverflow: "ellipsis",
1703
+ }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
1698
1704
  }), outputEntries.map((entry, i) => {
1699
1705
  const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
1700
1706
  const hasAny = vIssues.length > 0;
@@ -1703,7 +1709,14 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
1703
1709
  .map((v) => `${v.code}: ${v.message}`)
1704
1710
  .join("; ");
1705
1711
  const resolved = resolveOutputDisplay(outputValues?.[entry.id], entry.typeId);
1706
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.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) } }), jsxRuntime.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, resolved.typeId && (jsxRuntime.jsxs("span", { className: "ml-1 opacity-60", children: ["(", resolved.typeId, ")"] })), hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(resolved.typeId, resolved.value) }))] })] }, `out-${entry.id}`));
1712
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.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) } }), jsxRuntime.jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: {
1713
+ top: topFor(i) - 8,
1714
+ textAlign: "right",
1715
+ left: "50%",
1716
+ whiteSpace: "nowrap",
1717
+ overflow: "hidden",
1718
+ textOverflow: "ellipsis",
1719
+ }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, resolved.typeId && (jsxRuntime.jsxs("span", { className: "ml-1 opacity-60", children: ["(", resolved.typeId, ")"] })), hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(resolved.typeId, resolved.value) }))] })] }, `out-${entry.id}`));
1707
1720
  })] }));
1708
1721
  });
1709
1722
  DefaultNode.displayName = "DefaultNode";
@@ -1842,11 +1855,21 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
1842
1855
  }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDelete, children: "Delete" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDuplicate, children: "Duplicate" }), canRunPull && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleRunPull, children: "Run (pull)" })), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleCopyId, children: "Copy Node ID" })] }));
1843
1856
  }
1844
1857
 
1845
- function WorkbenchCanvas({ showValues, toString, toElement, }) {
1858
+ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, ref) => {
1846
1859
  const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
1847
1860
  const ioValues = { inputs: inputsMap, outputs: outputsMap };
1848
1861
  const nodeValidation = validationByNode;
1849
1862
  const edgeValidation = validationByEdge.errors;
1863
+ // Expose imperative API
1864
+ const rfInstanceRef = React.useRef(null);
1865
+ React.useImperativeHandle(ref, () => ({
1866
+ fitView: () => {
1867
+ try {
1868
+ rfInstanceRef.current?.fitView({ padding: 0.2 });
1869
+ }
1870
+ catch { }
1871
+ },
1872
+ }));
1850
1873
  const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
1851
1874
  const { nodeTypes, resolveNodeType } = React.useMemo(() => {
1852
1875
  // Build nodeTypes map using UI extension registry
@@ -1922,8 +1945,8 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
1922
1945
  const addNodeAt = (typeId, pos) => {
1923
1946
  wb.addNode({ typeId, position: pos });
1924
1947
  };
1925
- return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.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: [jsxRuntime.jsx(ReactFlow.Background, {}), jsxRuntime.jsx(ReactFlow.MiniMap, {}), jsxRuntime.jsx(ReactFlow.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) }), jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: () => setNodeMenuOpen(false) })] }) }));
1926
- }
1948
+ return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.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, onInit: (inst) => (rfInstanceRef.current = inst), children: [jsxRuntime.jsx(ReactFlow.Background, {}), jsxRuntime.jsx(ReactFlow.MiniMap, {}), jsxRuntime.jsx(ReactFlow.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) }), jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: () => setNodeMenuOpen(false) })] }) }));
1949
+ });
1927
1950
 
1928
1951
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, }) {
1929
1952
  const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
@@ -1973,6 +1996,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1973
1996
  }, [overrides, defaultExamples]);
1974
1997
  const lastAutoLaunched = React.useRef(undefined);
1975
1998
  const autoLayoutRan = React.useRef(false);
1999
+ const canvasRef = React.useRef(null);
1976
2000
  const applyExample = React.useCallback(async (key) => {
1977
2001
  if (runner.isRunning()) {
1978
2002
  alert(`Stop engine before switching example.`);
@@ -1993,6 +2017,27 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1993
2017
  setExampleState(key);
1994
2018
  onExampleChange?.(key);
1995
2019
  }, [runner, wb, onExampleChange, runAutoLayout, examples, setRegistry]);
2020
+ const downloadGraph = React.useCallback(() => {
2021
+ try {
2022
+ const def = wb.export();
2023
+ const pretty = JSON.stringify(def, null, 2);
2024
+ const blob = new Blob([pretty], { type: "application/json" });
2025
+ const url = URL.createObjectURL(blob);
2026
+ const a = document.createElement("a");
2027
+ const d = new Date();
2028
+ const pad = (n) => String(n).padStart(2, "0");
2029
+ const ts = `${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
2030
+ a.href = url;
2031
+ a.download = `spark-graph-${ts}.json`;
2032
+ document.body.appendChild(a);
2033
+ a.click();
2034
+ a.remove();
2035
+ URL.revokeObjectURL(url);
2036
+ }
2037
+ catch (err) {
2038
+ alert(String(err?.message ?? err));
2039
+ }
2040
+ }, [wb]);
1996
2041
  const hydrateFromBackend = React.useCallback(async (kind, base) => {
1997
2042
  try {
1998
2043
  const transport = kind === "remote-http"
@@ -2291,7 +2336,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2291
2336
  catch (err) {
2292
2337
  alert(String(err?.message ?? err));
2293
2338
  }
2294
- }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), 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, { 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 })] })] }));
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 })] })] }));
2295
2340
  }
2296
2341
  function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
2297
2342
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());