@bian-womp/spark-workbench 0.1.21 → 0.1.23

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
@@ -327,6 +327,9 @@ class GraphRunner {
327
327
  this.backend = { kind: "local" };
328
328
  if (backend)
329
329
  this.backend = backend;
330
+ // Emit initial transport status
331
+ if (this.backend.kind === "local")
332
+ this.emit("transport", { state: "local" });
330
333
  }
331
334
  build(def) {
332
335
  if (this.backend.kind === "local") {
@@ -615,6 +618,13 @@ class GraphRunner {
615
618
  this.runningKind = undefined;
616
619
  this.emit("status", { running: false, engine: undefined });
617
620
  }
621
+ const kind = this.backend.kind === "local"
622
+ ? undefined
623
+ : this.backend.kind;
624
+ this.emit("transport", {
625
+ state: this.backend.kind === "local" ? "local" : "disconnected",
626
+ kind,
627
+ });
618
628
  }
619
629
  isRunning() {
620
630
  return !!this.engine;
@@ -627,6 +637,8 @@ class GraphRunner {
627
637
  if (this.remote)
628
638
  return this.remote;
629
639
  let transport;
640
+ const kind = this.backend.kind === "remote-http" ? "remote-http" : "remote-ws";
641
+ this.emit("transport", { state: "connecting", kind });
630
642
  if (this.backend.kind === "remote-http") {
631
643
  if (!sparkRemote.HttpPollingTransport)
632
644
  throw new Error("HttpPollingTransport not available");
@@ -649,6 +661,7 @@ class GraphRunner {
649
661
  valueCache: new Map(),
650
662
  listenersBound: false,
651
663
  };
664
+ this.emit("transport", { state: "connected", kind });
652
665
  return this.remote;
653
666
  }
654
667
  }
@@ -817,6 +830,19 @@ function useQueryParamString(key, defaultValue) {
817
830
  return [val, set];
818
831
  }
819
832
 
833
+ function formatDataUrlAsLabel(dataUrl) {
834
+ try {
835
+ const semi = dataUrl.indexOf(";");
836
+ const comma = dataUrl.indexOf(",");
837
+ const mime = dataUrl.slice(5, semi > 0 ? semi : undefined).toUpperCase();
838
+ const b64 = comma >= 0 ? dataUrl.slice(comma + 1) : "";
839
+ const bytes = Math.floor((b64.length * 3) / 4);
840
+ return `${mime} Data (${bytes} bytes)`;
841
+ }
842
+ catch {
843
+ return dataUrl.length > 64 ? dataUrl.slice(0, 64) + "…" : dataUrl;
844
+ }
845
+ }
820
846
  function resolveOutputDisplay(raw, declared) {
821
847
  if (sparkGraph.isTypedOutput(raw)) {
822
848
  return {
@@ -847,7 +873,7 @@ function preformatValueForDisplay(typeId, value, registry) {
847
873
  return preformatValueForDisplay(sparkGraph.getTypedOutputTypeId(value), sparkGraph.getTypedOutputValue(value), registry);
848
874
  }
849
875
  // Enums
850
- if (typeId && typeId.includes("enum:") && registry) {
876
+ if (typeId && typeId.startsWith("enum:") && registry) {
851
877
  const n = Number(value);
852
878
  const label = registry.enums.get(typeId)?.valueToLabel.get(n);
853
879
  if (label)
@@ -871,19 +897,8 @@ function preformatValueForDisplay(typeId, value, registry) {
871
897
  function summarizeDeep(value) {
872
898
  // Strings: summarize data URLs and trim extremely long strings
873
899
  if (typeof value === "string") {
874
- if (value.startsWith("data:")) {
875
- try {
876
- const semi = value.indexOf(";");
877
- const comma = value.indexOf(",");
878
- const mime = value.slice(5, semi > 0 ? semi : undefined).toUpperCase();
879
- const b64 = comma >= 0 ? value.slice(comma + 1) : "";
880
- const bytes = Math.floor((b64.length * 3) / 4);
881
- return `${mime} Data (${bytes} bytes)`;
882
- }
883
- catch {
884
- return value.length > 64 ? value.slice(0, 64) + "…" : value;
885
- }
886
- }
900
+ if (value.startsWith("data:"))
901
+ return formatDataUrlAsLabel(value);
887
902
  return value.length > 512 ? value.slice(0, 512) + "…" : value;
888
903
  }
889
904
  // Typed output wrapper
@@ -903,18 +918,8 @@ function summarizeDeep(value) {
903
918
  if (typeof v === "string" &&
904
919
  k.toLowerCase() === "url" &&
905
920
  v.startsWith("data:")) {
906
- try {
907
- const semi = v.indexOf(";");
908
- const comma = v.indexOf(",");
909
- const mime = v.slice(5, semi > 0 ? semi : undefined).toUpperCase();
910
- const b64 = comma >= 0 ? v.slice(comma + 1) : "";
911
- const bytes = Math.floor((b64.length * 3) / 4);
912
- out[k] = `${mime} Data (${bytes} bytes)`;
913
- continue;
914
- }
915
- catch {
916
- // fallthrough
917
- }
921
+ out[k] = formatDataUrlAsLabel(v);
922
+ continue;
918
923
  }
919
924
  out[k] = summarizeDeep(v);
920
925
  }
@@ -1085,11 +1090,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1085
1090
  const out = {};
1086
1091
  // Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
1087
1092
  for (const n of def.nodes) {
1088
- const handles = Object.keys(registry.nodes.get(n.typeId)?.outputs ?? {});
1093
+ const outputsDecl = registry.nodes.get(n.typeId)?.outputs ?? {};
1094
+ const handles = Object.keys(outputsDecl);
1089
1095
  const cur = {};
1090
1096
  for (const h of handles) {
1091
1097
  const v = outputsMap[n.nodeId]?.[h];
1092
- cur[h] = sparkGraph.getTypedOutputTypeId(v);
1098
+ const declared = outputsDecl[h];
1099
+ const { typeId } = resolveOutputDisplay(v, declared);
1100
+ cur[h] = typeId;
1093
1101
  }
1094
1102
  if (Object.keys(cur).length > 0)
1095
1103
  out[n.nodeId] = cur;
@@ -1524,14 +1532,6 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1524
1532
  const safeToString = (typeId, value) => {
1525
1533
  try {
1526
1534
  if (typeof toString === "function") {
1527
- // Special-case data URLs for readability
1528
- if (typeof value === "string" && value.startsWith("data:image/")) {
1529
- const comma = value.indexOf(",");
1530
- const b64 = comma >= 0 ? value.slice(comma + 1) : "";
1531
- const bytes = Math.floor((b64.length * 3) / 4);
1532
- const fmt = value.slice(5, value.indexOf(";")) || "image";
1533
- return `${fmt.toUpperCase()} Data (${bytes} bytes)`;
1534
- }
1535
1535
  return toString(typeId, value);
1536
1536
  }
1537
1537
  return String(value ?? "");
@@ -1640,7 +1640,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1640
1640
  const orig = originals[h] ?? safeToString(typeId, current);
1641
1641
  setDrafts((d) => ({ ...d, [h]: orig }));
1642
1642
  };
1643
- const isEnum = typeId?.includes("enum:");
1643
+ const isEnum = typeId?.startsWith("enum:");
1644
1644
  const inIssues = selectedNodeHandleValidation.inputs.filter((m) => m.handle === h);
1645
1645
  const hasValidation = inIssues.length > 0;
1646
1646
  const hasErr = inIssues.some((m) => m.level === "error");
@@ -1747,7 +1747,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
1747
1747
  whiteSpace: "nowrap",
1748
1748
  overflow: "hidden",
1749
1749
  textOverflow: "ellipsis",
1750
- }, 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}`));
1750
+ }, 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(resolved.typeId, resolved.value) }))] })] }, `out-${entry.id}`));
1751
1751
  })] }));
1752
1752
  });
1753
1753
  DefaultNode.displayName = "DefaultNode";
@@ -1756,16 +1756,18 @@ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
1756
1756
  const { registry } = useWorkbenchContext();
1757
1757
  const rf = ReactFlow.useReactFlow();
1758
1758
  const ids = Array.from(registry.nodes.keys());
1759
- // Group node ids by the segment before the first '.'
1760
- const grouped = {};
1759
+ const root = { __children: {} };
1761
1760
  for (const id of ids) {
1762
1761
  const parts = id.split(".");
1763
- const cat = parts.length > 1 ? parts[0] : "other";
1764
- const label = parts.length > 1 ? parts.slice(1).join(".") : id;
1765
- (grouped[cat] = grouped[cat] || []).push({ id, label });
1762
+ let node = root;
1763
+ for (let i = 0; i < parts.length; i++) {
1764
+ const key = parts[i];
1765
+ node.__children[key] = node.__children[key] || { __children: {} };
1766
+ node = node.__children[key];
1767
+ if (i === parts.length - 1)
1768
+ node.__self = id;
1769
+ }
1766
1770
  }
1767
- const cats = Object.keys(grouped).sort((a, b) => a.localeCompare(b));
1768
- cats.forEach((c) => grouped[c].sort((a, b) => a.label.localeCompare(b.label)));
1769
1771
  const totalCount = ids.length;
1770
1772
  // Ref for focus/outside click handling
1771
1773
  const ref = React.useRef(null);
@@ -1809,10 +1811,19 @@ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
1809
1811
  onAdd(typeId, p);
1810
1812
  onClose();
1811
1813
  };
1814
+ const renderTree = (tree, path = []) => {
1815
+ const entries = Object.entries(tree.__children).sort((a, b) => a[0].localeCompare(b[0]));
1816
+ return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
1817
+ const label = key;
1818
+ const hasChildren = Object.keys(child.__children).length > 0;
1819
+ !!child.__self && !hasChildren;
1820
+ return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "px-2 py-1 text-[11px] uppercase tracking-wide text-gray-400", children: label }), child.__self && (jsxRuntime.jsx("button", { onClick: () => handleClick(child.__self), className: "block w-full text-left px-3 py-1 hover:bg-gray-100 cursor-pointer", title: child.__self, children: child.__self.split(".").slice(-1)[0] })), hasChildren && (jsxRuntime.jsx("div", { className: "pl-2 border-l border-gray-200 ml-2", children: renderTree(child, [...path, key]) }))] }, [...path, key].join(".")));
1821
+ }) }));
1822
+ };
1812
1823
  return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-none 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) => {
1813
1824
  e.preventDefault();
1814
1825
  e.stopPropagation();
1815
- }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: cats.map((cat) => (jsxRuntime.jsxs("div", { className: "py-1", children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 text-[11px] uppercase tracking-wide text-gray-400", children: [cat, " ", jsxRuntime.jsxs("span", { className: "opacity-60 normal-case", children: ["(", grouped[cat].length, ")"] })] }), grouped[cat].map(({ id, label }) => (jsxRuntime.jsx("button", { onClick: () => handleClick(id), className: "block w-full text-left px-3 py-1 hover:bg-gray-100 cursor-pointer", title: id, children: label }, id)))] }, cat))) })] }));
1826
+ }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: renderTree(root) })] }));
1816
1827
  }
1817
1828
 
1818
1829
  function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
@@ -1981,6 +1992,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
1981
1992
 
1982
1993
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
1983
1994
  const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
1995
+ const [transportStatus, setTransportStatus] = React.useState({
1996
+ state: "local",
1997
+ });
1984
1998
  const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
1985
1999
  const selectedDesc = selectedNode
1986
2000
  ? registry.nodes.get(selectedNode.typeId)
@@ -2194,6 +2208,10 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2194
2208
  return;
2195
2209
  applyExample(example);
2196
2210
  }, [example, wb]);
2211
+ React.useEffect(() => {
2212
+ const off = runner.on("transport", (s) => setTransportStatus(s));
2213
+ return () => off();
2214
+ }, [runner]);
2197
2215
  React.useEffect(() => {
2198
2216
  if (!engine)
2199
2217
  return;
@@ -2337,26 +2355,10 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2337
2355
  typeof value.url === "string") {
2338
2356
  const title = value.title || "";
2339
2357
  const url = String(value.url || "");
2340
- if (url.startsWith("data:image/")) {
2341
- try {
2342
- const semi = url.indexOf(";");
2343
- const comma = url.indexOf(",");
2344
- const mime = url
2345
- .slice(5, semi > 0 ? semi : undefined)
2346
- .toUpperCase();
2347
- const b64 = comma >= 0 ? url.slice(comma + 1) : "";
2348
- const bytes = Math.floor((b64.length * 3) / 4);
2349
- return title
2350
- ? `${title} (${mime} ${bytes} bytes)`
2351
- : `${mime} Data (${bytes} bytes)`;
2352
- }
2353
- catch {
2354
- return title || url.slice(0, 32) + (url.length > 32 ? "…" : "");
2355
- }
2356
- }
2358
+ // value.ts handles data URL formatting
2357
2359
  return title || url.slice(0, 32) + (url.length > 32 ? "…" : "");
2358
2360
  }
2359
- if (typeId && typeId.includes("enum:")) {
2361
+ if (typeId && typeId.startsWith("enum:")) {
2360
2362
  const n = Number(value);
2361
2363
  const label = registry.enums.get(typeId)?.valueToLabel.get(n);
2362
2364
  return label ?? String(n);
@@ -2400,7 +2402,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2400
2402
  return overrides.toElement(baseToElement, { registry });
2401
2403
  return baseToElement;
2402
2404
  }, [overrides, baseToElement, registry]);
2403
- 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()
2405
+ 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.jsxs("span", { className: "ml-2 flex items-center gap-1 text-xs", title: transportStatus.kind || undefined, children: [transportStatus.state === "local" && (jsxRuntime.jsx(react.PlugsConnectedIcon, { size: 14, className: "text-gray-500" })), transportStatus.state === "connecting" && (jsxRuntime.jsx(react.ClockClockwiseIcon, { size: 14, className: "text-amber-600 animate-pulse" })), transportStatus.state === "connected" && (jsxRuntime.jsx(react.WifiHighIcon, { size: 14, className: "text-green-600" })), transportStatus.state === "disconnected" && (jsxRuntime.jsx(react.WifiSlashIcon, { size: 14, className: "text-red-600" })), transportStatus.state === "retrying" && (jsxRuntime.jsx(react.ClockClockwiseIcon, { size: 14, className: "text-amber-700 animate-pulse" }))] }), 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()
2404
2406
  ? "Stop engine before switching example"
2405
2407
  : 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()
2406
2408
  ? "Stop engine before switching backend"
@@ -2453,6 +2455,7 @@ exports.WorkbenchCanvas = WorkbenchCanvas;
2453
2455
  exports.WorkbenchContext = WorkbenchContext;
2454
2456
  exports.WorkbenchProvider = WorkbenchProvider;
2455
2457
  exports.WorkbenchStudio = WorkbenchStudio;
2458
+ exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
2456
2459
  exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
2457
2460
  exports.getNodeBorderClassNames = getNodeBorderClassNames;
2458
2461
  exports.preformatValueForDisplay = preformatValueForDisplay;