@bian-womp/spark-workbench 0.1.14 → 0.1.16

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
@@ -439,6 +439,7 @@ class GraphRunner {
439
439
  rc.valueCache.set(`${e.nodeId}.${e.handle}`, {
440
440
  io: e.io,
441
441
  value: e.value,
442
+ runtimeTypeId: e.runtimeTypeId,
442
443
  });
443
444
  this.emit("value", e);
444
445
  });
@@ -808,12 +809,31 @@ function useQueryParamString(key, defaultValue) {
808
809
  return [val, set];
809
810
  }
810
811
 
812
+ function resolveOutputDisplay(raw, declared) {
813
+ if (sparkGraph.isTypedOutput(raw)) {
814
+ return { typeId: String(raw.__spark_type), value: raw.__spark_value };
815
+ }
816
+ let typeId = undefined;
817
+ if (Array.isArray(declared)) {
818
+ typeId = declared.length === 1 ? declared[0] : undefined;
819
+ }
820
+ else if (typeof declared === "string") {
821
+ typeId = declared.includes("|") ? undefined : declared;
822
+ }
823
+ return { typeId, value: raw };
824
+ }
825
+ function formatDeclaredTypeSignature(declared) {
826
+ if (Array.isArray(declared))
827
+ return declared.join(" | ");
828
+ return declared ?? "";
829
+ }
830
+
811
831
  function toReactFlow(def, positions, registry, opts) {
812
832
  const nodeHandleMap = {};
813
833
  const nodes = def.nodes.map((n) => {
814
834
  const desc = registry.nodes.get(n.typeId);
815
835
  const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
816
- const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
836
+ const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId: formatDeclaredTypeSignature(typeId) }));
817
837
  nodeHandleMap[n.nodeId] = {
818
838
  inputs: new Set(inputHandles.map((h) => h.id)),
819
839
  outputs: new Set(outputHandles.map((h) => h.id)),
@@ -913,6 +933,41 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
913
933
  const [edgeStatus, setEdgeStatus] = React.useState({});
914
934
  const [events, setEvents] = React.useState([]);
915
935
  const clearEvents = React.useCallback(() => setEvents([]), []);
936
+ // Fallback progress animation: drive progress to 100% over ~2 minutes
937
+ const FALLBACK_TOTAL_MS = 2 * 60 * 1000;
938
+ const [fallbackStarts, setFallbackStarts] = React.useState({});
939
+ // Track runs that emitted an error so we can keep progress on completion
940
+ const [errorRuns, setErrorRuns] = React.useState({});
941
+ // Periodically advance fallback progress for running nodes without explicit progress
942
+ React.useEffect(() => {
943
+ const interval = setInterval(() => {
944
+ setNodeStatus((prev) => {
945
+ let changed = false;
946
+ const next = { ...prev };
947
+ const now = Date.now();
948
+ for (const id of Object.keys(prev)) {
949
+ const st = prev[id];
950
+ if (!st)
951
+ continue;
952
+ const runs = st.activeRuns ?? 0;
953
+ const startAt = fallbackStarts[id];
954
+ if (runs > 0 && startAt) {
955
+ const cur = Math.max(0, Math.min(1, Number(st.progress) || 0));
956
+ const elapsed = Math.max(0, now - startAt);
957
+ // Approach 100% over the target window, but cap below 1 until done
958
+ const target = Math.max(0, Math.min(0.99, elapsed / FALLBACK_TOTAL_MS));
959
+ const merged = Math.max(cur, target);
960
+ if (merged > cur + 0.005 && merged <= 1) {
961
+ next[id] = { ...st, progress: merged };
962
+ changed = true;
963
+ }
964
+ }
965
+ }
966
+ return changed ? next : prev;
967
+ });
968
+ }, 200);
969
+ return () => clearInterval(interval);
970
+ }, [fallbackStarts]);
916
971
  // Validation
917
972
  const [validation, setValidation] = React.useState(undefined);
918
973
  // Selection (mirror workbench selectionChanged)
@@ -1025,6 +1080,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1025
1080
  lastError: nodeError.err,
1026
1081
  },
1027
1082
  }));
1083
+ // Mark this runId as errored
1084
+ if (nodeError.runId) {
1085
+ setErrorRuns((prev) => ({
1086
+ ...prev,
1087
+ [nodeId]: { ...(prev[nodeId] || {}), [nodeError.runId]: true },
1088
+ }));
1089
+ }
1028
1090
  }
1029
1091
  return add("runner", "error")(e);
1030
1092
  });
@@ -1057,6 +1119,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1057
1119
  },
1058
1120
  };
1059
1121
  });
1122
+ // Start fallback animation window
1123
+ setFallbackStarts((prev) => ({ ...prev, [id]: Date.now() }));
1060
1124
  }
1061
1125
  else if (s.kind === "node-progress") {
1062
1126
  const id = s.nodeId;
@@ -1070,16 +1134,42 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1070
1134
  }
1071
1135
  else if (s.kind === "node-done") {
1072
1136
  const id = s.nodeId;
1137
+ const runId = s.runId;
1073
1138
  setNodeStatus((prev) => {
1074
1139
  const current = prev[id]?.activeRuns ?? 0;
1140
+ const nextActive = current - 1;
1141
+ const hadError = !!(runId && errorRuns[id]?.[runId]);
1142
+ const keepProgress = hadError || nextActive > 0;
1075
1143
  return {
1076
1144
  ...prev,
1077
1145
  [id]: {
1078
1146
  ...prev[id],
1079
- activeRuns: current - 1,
1147
+ activeRuns: nextActive,
1148
+ progress: keepProgress ? prev[id]?.progress : 0,
1080
1149
  },
1081
1150
  };
1082
1151
  });
1152
+ // Clear fallback start timestamp if no more active runs
1153
+ setFallbackStarts((prev) => {
1154
+ prev[id];
1155
+ const nextPrev = { ...prev };
1156
+ // If we don't know nextActive here, conservatively clear to stop animation
1157
+ delete nextPrev[id];
1158
+ return nextPrev;
1159
+ });
1160
+ // Clear error flag for this runId
1161
+ if (runId) {
1162
+ setErrorRuns((prev) => {
1163
+ const nodeMap = { ...(prev[id] || {}) };
1164
+ delete nodeMap[runId];
1165
+ const next = { ...prev };
1166
+ if (Object.keys(nodeMap).length === 0)
1167
+ delete next[id];
1168
+ else
1169
+ next[id] = nodeMap;
1170
+ return next;
1171
+ });
1172
+ }
1083
1173
  }
1084
1174
  else if (s.kind === "edge-start") {
1085
1175
  const id = s.edgeId;
@@ -1447,7 +1537,10 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1447
1537
  if (e.key === "Escape")
1448
1538
  revert();
1449
1539
  }, ...commonProps }))] }, h));
1450
- }))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsx("label", { className: "w-20", children: h }), jsxRuntime.jsx("div", { className: "flex-1", children: toElement(selectedDesc?.outputs?.[h], nodeOutputs[h]) }), (() => {
1540
+ }))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsx("label", { className: "w-20", children: h }), jsxRuntime.jsx("div", { className: "flex-1", children: (() => {
1541
+ const { typeId, value } = resolveOutputDisplay(nodeOutputs[h], selectedDesc?.outputs?.[h]);
1542
+ return toElement(typeId, value);
1543
+ })() }), (() => {
1451
1544
  const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
1452
1545
  if (outIssues.length === 0)
1453
1546
  return null;
@@ -1514,7 +1607,10 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
1514
1607
  const title = vIssues
1515
1608
  .map((v) => `${v.code}: ${v.message}`)
1516
1609
  .join("; ");
1517
- 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, 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, outputValues?.[entry.id]) }))] })] }, `out-${entry.id}`));
1610
+ 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, 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: (() => {
1611
+ const { typeId, value } = resolveOutputDisplay(outputValues?.[entry.id], entry.typeId);
1612
+ return toString(typeId, value);
1613
+ })() }))] })] }, `out-${entry.id}`));
1518
1614
  })] }));
1519
1615
  });
1520
1616
  DefaultNode.displayName = "DefaultNode";