@bian-womp/spark-workbench 0.2.7 → 0.2.9

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
@@ -548,6 +548,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
548
548
  displayName: d.displayName,
549
549
  options: d.options,
550
550
  opts: d.opts,
551
+ bakeTarget: d.bakeTarget,
551
552
  });
552
553
  }
553
554
  else if (d.kind === "register-type") {
@@ -556,6 +557,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
556
557
  id: d.id,
557
558
  displayName: d.displayName,
558
559
  validate: (_v) => true,
560
+ bakeTarget: d.bakeTarget,
559
561
  });
560
562
  }
561
563
  }
@@ -678,6 +680,15 @@ class RemoteGraphRunner extends AbstractGraphRunner {
678
680
  catch { }
679
681
  });
680
682
  }
683
+ async coerce(from, to, value) {
684
+ const runner = await this.ensureRemoteRunner();
685
+ try {
686
+ return await runner.coerce(from, to, value);
687
+ }
688
+ catch {
689
+ return value;
690
+ }
691
+ }
681
692
  getOutputs(def) {
682
693
  const out = {};
683
694
  const cache = this.valueCache;
@@ -1108,6 +1119,15 @@ function toReactFlow(def, positions, registry, opts) {
1108
1119
  const EDGE_STYLE_ERROR = { stroke: "#ef4444", strokeWidth: 2 };
1109
1120
  const EDGE_STYLE_RUNNING = { stroke: "#3b82f6" };
1110
1121
  const nodeHandleMap = {};
1122
+ // Precompute which inputs are connected per node
1123
+ const connectedInputs = {};
1124
+ for (const e of def.edges) {
1125
+ const nid = e.target.nodeId;
1126
+ const hid = e.target.handle;
1127
+ if (!connectedInputs[nid])
1128
+ connectedInputs[nid] = new Set();
1129
+ connectedInputs[nid].add(hid);
1130
+ }
1111
1131
  const nodes = def.nodes.map((n) => {
1112
1132
  const desc = registry.nodes.get(n.typeId);
1113
1133
  const inputHandles = Object.entries(desc?.inputs ?? {})
@@ -1156,6 +1176,10 @@ function toReactFlow(def, positions, registry, opts) {
1156
1176
  params: n.params,
1157
1177
  inputHandles,
1158
1178
  outputHandles,
1179
+ inputConnected: Object.fromEntries(inputHandles.map((h) => [
1180
+ h.id,
1181
+ !!connectedInputs[n.nodeId]?.has(h.id),
1182
+ ])),
1159
1183
  handleLayout: [
1160
1184
  ...inputHandles.map((h, i) => ({
1161
1185
  id: h.id,
@@ -1958,12 +1982,20 @@ function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray
1958
1982
  const byId = React.useMemo(() => {
1959
1983
  const m = new Map();
1960
1984
  for (const h of layout) {
1961
- m.set(h.id, { position: h.position, y: h.y, type: h.type });
1985
+ // Prefer namespaced key to disambiguate inputs/outputs that share id
1986
+ m.set(`${h.type}:${h.id}`, {
1987
+ position: h.position,
1988
+ y: h.y,
1989
+ type: h.type,
1990
+ });
1991
+ // Back-compat: also store by id-only if not already set
1992
+ if (!m.has(h.id))
1993
+ m.set(h.id, { position: h.position, y: h.y, type: h.type });
1962
1994
  }
1963
1995
  return m;
1964
1996
  }, [layout]);
1965
1997
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(data.inputHandles ?? []).map((h) => {
1966
- const placed = byId.get(h.id);
1998
+ const placed = byId.get(`target:${h.id}`) ?? byId.get(h.id);
1967
1999
  const position = placed?.position ?? react.Position.Left;
1968
2000
  const y = placed?.y;
1969
2001
  const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ??
@@ -1976,7 +2008,7 @@ function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray
1976
2008
  textOverflow: "ellipsis",
1977
2009
  }, children: renderLabel({ kind: "input", id: h.id }) }))] }, h.id));
1978
2010
  }), (data.outputHandles ?? []).map((h) => {
1979
- const placed = byId.get(h.id);
2011
+ const placed = byId.get(`source:${h.id}`) ?? byId.get(h.id);
1980
2012
  const position = placed?.position ?? react.Position.Right;
1981
2013
  const y = placed?.y;
1982
2014
  const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ??
@@ -2021,17 +2053,37 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
2021
2053
  position: "relative",
2022
2054
  minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
2023
2055
  minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
2024
- }, children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", style: {
2025
- maxHeight: NODE_HEADER_HEIGHT_PX,
2026
- minHeight: NODE_HEADER_HEIGHT_PX,
2027
- }, children: [jsxRuntime.jsx("strong", { className: "flex-1 h-full text-sm", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, children: typeId }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
2028
- ? "error"
2029
- : "warning", size: 12, className: "w-3 h-3", title: validation.issues
2030
- .map((v) => `${v.code}: ${v.message}`)
2031
- .join("; ") })), jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }), jsxRuntime.jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
2056
+ }, children: [jsxRuntime.jsx(DefaultNodeHeader, { id: id, title: typeId, status: status, validation: validation }), jsxRuntime.jsx(DefaultNodeContent, { id: id, data: data, isConnectable: isConnectable })] }));
2032
2057
  });
2033
2058
  DefaultNode.displayName = "DefaultNode";
2034
- function DefaultNodeContent({ data, isConnectable, }) {
2059
+ function DefaultNodeHeader({ id, title, status, validation, right, onInvalidate, }) {
2060
+ const ctx = useWorkbenchContext();
2061
+ const handleInvalidate = React.useCallback(() => {
2062
+ try {
2063
+ if (onInvalidate)
2064
+ return onInvalidate();
2065
+ const kind = ctx.engineKind?.();
2066
+ if (kind === "pull")
2067
+ ctx.runner.computeNode(id);
2068
+ else
2069
+ ctx.triggerExternal?.(id, { type: "invalidate" });
2070
+ }
2071
+ catch { }
2072
+ }, [ctx, id, onInvalidate]);
2073
+ return (jsxRuntime.jsxs("div", { className: "flex items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", style: {
2074
+ maxHeight: NODE_HEADER_HEIGHT_PX,
2075
+ minHeight: NODE_HEADER_HEIGHT_PX,
2076
+ }, children: [jsxRuntime.jsx("strong", { className: "flex-1 h-full text-sm", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, children: title }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("button", { className: "w-4 h-4 border border-gray-400 rounded text-[10px] leading-3 flex items-center justify-center", title: "Invalidate and re-run", onClick: (e) => {
2077
+ e.stopPropagation();
2078
+ handleInvalidate();
2079
+ }, children: "\u21BB" }), right, validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
2080
+ ? "error"
2081
+ : "warning", size: 12, className: "w-3 h-3", title: validation.issues
2082
+ .map((v) => `${v.code}: ${v.message}`)
2083
+ .join("; ") })), jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }));
2084
+ }
2085
+ function DefaultNodeContent({ id, data, isConnectable, }) {
2086
+ const ctx = useWorkbenchContext();
2035
2087
  const { showValues, inputValues, outputValues, toString } = data;
2036
2088
  const inputEntries = data.inputHandles ?? [];
2037
2089
  const outputEntries = data.outputHandles ?? [];
@@ -2041,17 +2093,109 @@ function DefaultNodeContent({ data, isConnectable, }) {
2041
2093
  outputs: []};
2042
2094
  const isRunning = !!status.activeRuns;
2043
2095
  const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
2096
+ const handleBake = React.useCallback(async (handleId) => {
2097
+ try {
2098
+ const typeId = ctx.outputTypesMap?.[id]?.[handleId];
2099
+ const rawValue = ctx.outputsMap?.[id]?.[handleId];
2100
+ if (!typeId || rawValue === undefined)
2101
+ return;
2102
+ const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
2103
+ const clone = (v) => typeof structuredClone === "function"
2104
+ ? structuredClone(v)
2105
+ : JSON.parse(JSON.stringify(v));
2106
+ const coerceIfNeeded = async (fromType, toType, value) => {
2107
+ if (!toType || toType === fromType || !ctx.runner?.coerce)
2108
+ return value;
2109
+ try {
2110
+ return await ctx.runner.coerce(fromType, toType, value);
2111
+ }
2112
+ catch {
2113
+ return value;
2114
+ }
2115
+ };
2116
+ const positions = ctx.wb.getPositions();
2117
+ const pos = positions[id] || { x: 0, y: 0 };
2118
+ const isArray = typeId.endsWith("[]");
2119
+ const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
2120
+ const tArr = isArray ? ctx.registry?.types.get(typeId) : undefined;
2121
+ const tElem = ctx.registry?.types.get(baseTypeId);
2122
+ const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
2123
+ const arrTarget = isArray ? tArr?.bakeTarget : undefined;
2124
+ const elemTarget = isArray ? tElem?.bakeTarget : undefined;
2125
+ const makeTargetInfo = (bt) => {
2126
+ if (!bt)
2127
+ return undefined;
2128
+ const node = ctx.registry?.nodes.get(String(bt.nodeTypeId));
2129
+ const inType = sparkGraph.getInputTypeId(node?.inputs, String(bt.inputHandle || "Value"));
2130
+ return { bt, inType };
2131
+ };
2132
+ // Plan and execute
2133
+ if (singleTarget) {
2134
+ const info = makeTargetInfo(singleTarget);
2135
+ const v = unwrap(rawValue);
2136
+ const coerced = await coerceIfNeeded(typeId, info?.inType, v);
2137
+ ctx.wb.addNode({
2138
+ nodeId: undefined,
2139
+ typeId: String(singleTarget.nodeTypeId),
2140
+ position: { x: pos.x + 180, y: pos.y },
2141
+ params: {},
2142
+ initialInputs: {
2143
+ [String(singleTarget.inputHandle || "Value")]: clone(coerced),
2144
+ },
2145
+ });
2146
+ return;
2147
+ }
2148
+ if (isArray && arrTarget) {
2149
+ const info = makeTargetInfo(arrTarget);
2150
+ const v = unwrap(rawValue);
2151
+ const coerced = await coerceIfNeeded(typeId, info?.inType, v);
2152
+ ctx.wb.addNode({
2153
+ nodeId: undefined,
2154
+ typeId: String(arrTarget.nodeTypeId),
2155
+ position: { x: pos.x + 180, y: pos.y },
2156
+ params: {},
2157
+ initialInputs: {
2158
+ [String(arrTarget.inputHandle || "Value")]: clone(coerced),
2159
+ },
2160
+ });
2161
+ return;
2162
+ }
2163
+ if (isArray && elemTarget && Array.isArray(rawValue)) {
2164
+ const info = makeTargetInfo(elemTarget);
2165
+ const items = rawValue.map(unwrap);
2166
+ const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, info?.inType, v)));
2167
+ const COLS = 4;
2168
+ const DX = 180;
2169
+ const DY = 160;
2170
+ coercedItems.forEach((cv, idx) => {
2171
+ const col = idx % COLS;
2172
+ const row = Math.floor(idx / COLS);
2173
+ ctx.wb.addNode({
2174
+ nodeId: undefined,
2175
+ typeId: String(elemTarget.nodeTypeId),
2176
+ position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
2177
+ params: {},
2178
+ initialInputs: {
2179
+ [String(elemTarget.inputHandle || "Value")]: clone(cv),
2180
+ },
2181
+ });
2182
+ });
2183
+ return;
2184
+ }
2185
+ }
2186
+ catch { }
2187
+ }, [ctx, id]);
2044
2188
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: cx("h-px", (isRunning || pct > 0) && "bg-blue-200 dark:bg-blue-900"), children: jsxRuntime.jsx("div", { className: cx("h-px transition-all", (isRunning || pct > 0) && "bg-blue-500"), style: { width: isRunning || pct > 0 ? `${pct}%` : 0 } }) }), jsxRuntime.jsx(NodeHandles, { data: data, isConnectable: isConnectable, getClassName: ({ kind, id }) => {
2045
2189
  const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === id);
2046
2190
  const hasAny = vIssues.length > 0;
2047
2191
  const hasErr = vIssues.some((v) => v.level === "error");
2048
2192
  return cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", kind === "output" && "!rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500"));
2049
- }, renderLabel: ({ kind, id }) => {
2193
+ }, renderLabel: ({ kind, id: handleId }) => {
2050
2194
  const entries = kind === "input" ? inputEntries : outputEntries;
2051
- const entry = entries.find((e) => e.id === id);
2195
+ const entry = entries.find((e) => e.id === handleId);
2052
2196
  if (!entry)
2053
- return id;
2054
- const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === id);
2197
+ return handleId;
2198
+ const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === handleId);
2055
2199
  const hasAny = vIssues.length > 0;
2056
2200
  const hasErr = vIssues.some((v) => v.level === "error");
2057
2201
  const title = vIssues
@@ -2068,7 +2212,26 @@ function DefaultNodeContent({ data, isConnectable, }) {
2068
2212
  const txt = toString(resolved.typeId, resolved.value);
2069
2213
  return typeof txt === "string" ? txt : String(txt);
2070
2214
  })();
2071
- return (jsxRuntime.jsxs("span", { className: "flex items-center gap-1 w-full", children: [kind === "output" ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [valueText !== undefined && (jsxRuntime.jsx("span", { className: "opacity-60 truncate pl-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText })), jsxRuntime.jsx("span", { className: "truncate shrink-0", style: { maxWidth: "40%" }, children: id })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "truncate shrink-0", style: { maxWidth: "40%" }, children: id }), valueText !== undefined && (jsxRuntime.jsx("span", { className: "opacity-60 truncate pr-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText }))] })), hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "shrink-0", title: title }))] }));
2215
+ const tId = ctx.outputTypesMap?.[id]?.[handleId];
2216
+ let canBake = false;
2217
+ if (tId?.endsWith("[]")) {
2218
+ const base = tId.slice(0, -2);
2219
+ const tArr = ctx.registry?.types.get(tId);
2220
+ const tElem = ctx.registry?.types.get(base);
2221
+ const arrTarget = tArr?.bakeTarget;
2222
+ const elemTarget = tElem?.bakeTarget;
2223
+ canBake = !!((arrTarget && ctx.registry?.nodes?.has?.(arrTarget.nodeTypeId)) ||
2224
+ (elemTarget && ctx.registry?.nodes?.has?.(elemTarget.nodeTypeId)));
2225
+ }
2226
+ else if (tId) {
2227
+ const t = ctx.registry?.types.get(tId);
2228
+ const target = t?.bakeTarget;
2229
+ canBake = !!(target && ctx.registry?.nodes?.has?.(target.nodeTypeId));
2230
+ }
2231
+ return (jsxRuntime.jsxs("span", { className: "flex items-center gap-1 w-full", children: [kind === "output" ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [canBake && (jsxRuntime.jsx("button", { onClick: (e) => {
2232
+ e.stopPropagation();
2233
+ handleBake(handleId);
2234
+ }, title: "Bake value", className: "pointer-events-auto border border-gray-300 rounded px-1 py-0.5 text-[10px] bg-white/80 hover:bg-white mr-2", children: "Bake" })), valueText !== undefined && (jsxRuntime.jsx("span", { className: "opacity-60 truncate pl-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText })), jsxRuntime.jsx("span", { className: "truncate shrink-0", style: { maxWidth: "40%" }, children: handleId })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "truncate shrink-0", style: { maxWidth: "40%" }, children: handleId }), valueText !== undefined && (jsxRuntime.jsx("span", { className: "opacity-60 truncate pr-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText }))] })), hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "shrink-0", title: title }))] }));
2072
2235
  } })] }));
2073
2236
  }
2074
2237
 
@@ -2633,6 +2796,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2633
2796
  r.registerEnum({
2634
2797
  id: t.id,
2635
2798
  options: t.options,
2799
+ bakeTarget: t.bakeTarget,
2636
2800
  });
2637
2801
  }
2638
2802
  else {
@@ -2640,6 +2804,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2640
2804
  id: t.id,
2641
2805
  displayName: t.displayName,
2642
2806
  validate: (_v) => true,
2807
+ bakeTarget: t.bakeTarget,
2643
2808
  });
2644
2809
  }
2645
2810
  }
@@ -2947,6 +3112,7 @@ exports.AbstractWorkbench = AbstractWorkbench;
2947
3112
  exports.CLIWorkbench = CLIWorkbench;
2948
3113
  exports.DefaultNode = DefaultNode;
2949
3114
  exports.DefaultNodeContent = DefaultNodeContent;
3115
+ exports.DefaultNodeHeader = DefaultNodeHeader;
2950
3116
  exports.DefaultUIExtensionRegistry = DefaultUIExtensionRegistry;
2951
3117
  exports.InMemoryWorkbench = InMemoryWorkbench;
2952
3118
  exports.Inspector = Inspector;