@bian-womp/spark-workbench 0.2.8 → 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,
@@ -2029,17 +2053,37 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
2029
2053
  position: "relative",
2030
2054
  minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
2031
2055
  minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
2032
- }, 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: {
2033
- maxHeight: NODE_HEADER_HEIGHT_PX,
2034
- minHeight: NODE_HEADER_HEIGHT_PX,
2035
- }, 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")
2036
- ? "error"
2037
- : "warning", size: 12, className: "w-3 h-3", title: validation.issues
2038
- .map((v) => `${v.code}: ${v.message}`)
2039
- .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 })] }));
2040
2057
  });
2041
2058
  DefaultNode.displayName = "DefaultNode";
2042
- 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();
2043
2087
  const { showValues, inputValues, outputValues, toString } = data;
2044
2088
  const inputEntries = data.inputHandles ?? [];
2045
2089
  const outputEntries = data.outputHandles ?? [];
@@ -2049,17 +2093,109 @@ function DefaultNodeContent({ data, isConnectable, }) {
2049
2093
  outputs: []};
2050
2094
  const isRunning = !!status.activeRuns;
2051
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]);
2052
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 }) => {
2053
2189
  const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === id);
2054
2190
  const hasAny = vIssues.length > 0;
2055
2191
  const hasErr = vIssues.some((v) => v.level === "error");
2056
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"));
2057
- }, renderLabel: ({ kind, id }) => {
2193
+ }, renderLabel: ({ kind, id: handleId }) => {
2058
2194
  const entries = kind === "input" ? inputEntries : outputEntries;
2059
- const entry = entries.find((e) => e.id === id);
2195
+ const entry = entries.find((e) => e.id === handleId);
2060
2196
  if (!entry)
2061
- return id;
2062
- 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);
2063
2199
  const hasAny = vIssues.length > 0;
2064
2200
  const hasErr = vIssues.some((v) => v.level === "error");
2065
2201
  const title = vIssues
@@ -2076,7 +2212,26 @@ function DefaultNodeContent({ data, isConnectable, }) {
2076
2212
  const txt = toString(resolved.typeId, resolved.value);
2077
2213
  return typeof txt === "string" ? txt : String(txt);
2078
2214
  })();
2079
- 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 }))] }));
2080
2235
  } })] }));
2081
2236
  }
2082
2237
 
@@ -2641,6 +2796,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2641
2796
  r.registerEnum({
2642
2797
  id: t.id,
2643
2798
  options: t.options,
2799
+ bakeTarget: t.bakeTarget,
2644
2800
  });
2645
2801
  }
2646
2802
  else {
@@ -2648,6 +2804,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2648
2804
  id: t.id,
2649
2805
  displayName: t.displayName,
2650
2806
  validate: (_v) => true,
2807
+ bakeTarget: t.bakeTarget,
2651
2808
  });
2652
2809
  }
2653
2810
  }
@@ -2955,6 +3112,7 @@ exports.AbstractWorkbench = AbstractWorkbench;
2955
3112
  exports.CLIWorkbench = CLIWorkbench;
2956
3113
  exports.DefaultNode = DefaultNode;
2957
3114
  exports.DefaultNodeContent = DefaultNodeContent;
3115
+ exports.DefaultNodeHeader = DefaultNodeHeader;
2958
3116
  exports.DefaultUIExtensionRegistry = DefaultUIExtensionRegistry;
2959
3117
  exports.InMemoryWorkbench = InMemoryWorkbench;
2960
3118
  exports.Inspector = Inspector;