@bian-womp/spark-workbench 0.2.22 → 0.2.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
@@ -534,13 +534,19 @@ class LocalGraphRunner extends AbstractGraphRunner {
534
534
  const def = undefined; // UI will supply def/positions on download for local
535
535
  const inputs = this.getInputs(this.runtime
536
536
  ? {
537
- nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({ nodeId: id, typeId: "" })),
537
+ nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({
538
+ nodeId: id,
539
+ typeId: "",
540
+ })),
538
541
  edges: [],
539
542
  }
540
543
  : { nodes: [], edges: [] });
541
544
  const outputs = this.getOutputs(this.runtime
542
545
  ? {
543
- nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({ nodeId: id, typeId: "" })),
546
+ nodes: Array.from(this.runtime.getNodeIds()).map((id) => ({
547
+ nodeId: id,
548
+ typeId: "",
549
+ })),
544
550
  edges: [],
545
551
  }
546
552
  : { nodes: [], edges: [] });
@@ -1261,18 +1267,44 @@ function layoutNode(args) {
1261
1267
  }
1262
1268
 
1263
1269
  function toReactFlow(def, positions, registry, opts) {
1270
+ const EDGE_STYLE_MISSING = { stroke: "#f59e0b", strokeWidth: 2 }; // amber-500
1264
1271
  const EDGE_STYLE_ERROR = { stroke: "#ef4444", strokeWidth: 2 };
1265
1272
  const EDGE_STYLE_RUNNING = { stroke: "#3b82f6" };
1266
- const nodeHandleMap = {};
1267
- // Precompute which inputs are connected per node
1273
+ // Build a map of valid handles per node up-front
1274
+ const validHandleMap = {};
1275
+ for (const n of def.nodes) {
1276
+ const { inputs, outputs } = computeEffectiveHandles(n, registry);
1277
+ const inputOrder = Object.keys(inputs).filter((k) => !sparkGraph.isInputPrivate(inputs, k));
1278
+ const outputOrder = Object.keys(outputs);
1279
+ validHandleMap[n.nodeId] = {
1280
+ inputs: new Set(inputOrder),
1281
+ outputs: new Set(outputOrder),
1282
+ };
1283
+ }
1284
+ // Track which inputs are connected (for UI) and which handles are missing (for layout)
1268
1285
  const connectedInputs = {};
1286
+ const missingInputsByNode = {};
1287
+ const missingOutputsByNode = {};
1269
1288
  for (const e of def.edges) {
1270
- const nid = e.target.nodeId;
1271
- const hid = e.target.handle;
1272
- if (!connectedInputs[nid])
1273
- connectedInputs[nid] = new Set();
1274
- connectedInputs[nid].add(hid);
1275
- }
1289
+ const tgtId = e.target.nodeId;
1290
+ const tgtHandle = e.target.handle;
1291
+ if (!connectedInputs[tgtId])
1292
+ connectedInputs[tgtId] = new Set();
1293
+ connectedInputs[tgtId].add(tgtHandle);
1294
+ const tgtValid = !!validHandleMap[tgtId]?.inputs.has(tgtHandle);
1295
+ if (!tgtValid) {
1296
+ (missingInputsByNode[tgtId] || (missingInputsByNode[tgtId] = new Set())).add(tgtHandle);
1297
+ }
1298
+ const srcId = e.source.nodeId;
1299
+ const srcHandle = e.source.handle;
1300
+ const srcValid = !!validHandleMap[srcId]?.outputs.has(srcHandle);
1301
+ if (!srcValid) {
1302
+ (missingOutputsByNode[srcId] || (missingOutputsByNode[srcId] = new Set())).add(srcHandle);
1303
+ }
1304
+ }
1305
+ // This map is still used later for certain checks; align with valid handles
1306
+ const nodeHandleMap = {};
1307
+ Object.assign(nodeHandleMap, validHandleMap);
1276
1308
  const nodes = def.nodes.map((n) => {
1277
1309
  const { inputs: inputSource, outputs: outputSource } = computeEffectiveHandles(n, registry);
1278
1310
  const overrideSize = opts.getDefaultNodeSize?.(n.typeId);
@@ -1294,11 +1326,61 @@ function toReactFlow(def, positions, registry, opts) {
1294
1326
  inputs: new Set(inputHandles.map((h) => h.id)),
1295
1327
  outputs: new Set(outputHandles.map((h) => h.id)),
1296
1328
  };
1297
- // Shared sizing
1329
+ // Append placeholder entries for any missing handles (below valid ones)
1330
+ const baseLeftCount = geom.inputOrder.length;
1331
+ const baseRightCount = geom.outputOrder.length;
1332
+ const extraInputs = Array.from(missingInputsByNode[n.nodeId] || []);
1333
+ const extraOutputs = Array.from(missingOutputsByNode[n.nodeId] || []);
1334
+ const HEADER = NODE_HEADER_HEIGHT_PX;
1335
+ const ROW = NODE_ROW_HEIGHT_PX;
1336
+ const extraHandleLayoutLeft = extraInputs.map((id, i) => ({
1337
+ id,
1338
+ type: "target",
1339
+ position: react.Position.Left,
1340
+ y: HEADER + (baseLeftCount + i) * ROW + ROW / 2,
1341
+ missing: true,
1342
+ }));
1343
+ const extraHandleLayoutRight = extraOutputs.map((id, i) => ({
1344
+ id,
1345
+ type: "source",
1346
+ position: react.Position.Right,
1347
+ y: HEADER + (baseRightCount + i) * ROW + ROW / 2,
1348
+ missing: true,
1349
+ }));
1350
+ const handleLayout = [
1351
+ ...geom.handleLayout,
1352
+ ...extraHandleLayoutLeft,
1353
+ ...extraHandleLayoutRight,
1354
+ ];
1355
+ // Precompute handle bounds (including missing) so edges can render immediately
1356
+ const missingBoundsLeft = extraInputs.map((id, i) => ({
1357
+ id,
1358
+ type: "target",
1359
+ position: react.Position.Left,
1360
+ x: 0,
1361
+ y: HEADER + (baseLeftCount + i) * ROW,
1362
+ width: 1,
1363
+ height: ROW + 2,
1364
+ }));
1365
+ const missingBoundsRight = extraOutputs.map((id, i) => ({
1366
+ id,
1367
+ type: "source",
1368
+ position: react.Position.Right,
1369
+ x: geom.width - 1,
1370
+ y: HEADER + (baseRightCount + i) * ROW,
1371
+ width: 1,
1372
+ height: ROW + 2,
1373
+ }));
1374
+ const handles = [
1375
+ ...geom.handles,
1376
+ ...missingBoundsLeft,
1377
+ ...missingBoundsRight,
1378
+ ];
1379
+ // Adjust node height to accommodate missing handle rows
1380
+ const baseRows = Math.max(baseLeftCount, baseRightCount);
1381
+ const newRows = Math.max(baseLeftCount + extraInputs.length, baseRightCount + extraOutputs.length);
1298
1382
  const initialWidth = geom.width;
1299
- const initialHeight = geom.height;
1300
- // Precompute handle bounds so edges can render immediately without waiting for measurement
1301
- const handles = geom.handles;
1383
+ const initialHeight = geom.height + Math.max(0, newRows - baseRows) * ROW;
1302
1384
  return {
1303
1385
  id: n.nodeId,
1304
1386
  data: {
@@ -1310,7 +1392,7 @@ function toReactFlow(def, positions, registry, opts) {
1310
1392
  h.id,
1311
1393
  !!connectedInputs[n.nodeId]?.has(h.id),
1312
1394
  ])),
1313
- handleLayout: geom.handleLayout,
1395
+ handleLayout,
1314
1396
  showValues: opts.showValues,
1315
1397
  renderWidth: initialWidth,
1316
1398
  renderHeight: initialHeight,
@@ -1337,24 +1419,21 @@ function toReactFlow(def, positions, registry, opts) {
1337
1419
  height: initialHeight,
1338
1420
  };
1339
1421
  });
1340
- const edges = def.edges
1341
- .filter((e) => {
1342
- const src = nodeHandleMap[e.source.nodeId];
1343
- const dst = nodeHandleMap[e.target.nodeId];
1344
- if (!src || !dst)
1345
- return false;
1346
- return (src.outputs.has(e.source.handle) && dst.inputs.has(e.target.handle));
1347
- })
1348
- .map((e) => {
1422
+ const edges = def.edges.map((e) => {
1349
1423
  const st = opts.edgeStatus?.[e.id];
1350
1424
  const isRunning = !!st?.activeRuns;
1351
1425
  const hasError = !!st?.lastError;
1352
1426
  const isInvalidEdge = !!opts.edgeValidation?.[e.id];
1353
- const style = hasError || isInvalidEdge
1354
- ? EDGE_STYLE_ERROR
1355
- : isRunning
1356
- ? EDGE_STYLE_RUNNING
1357
- : undefined;
1427
+ const sourceMissing = !validHandleMap[e.source.nodeId]?.outputs.has(e.source.handle);
1428
+ const targetMissing = !validHandleMap[e.target.nodeId]?.inputs.has(e.target.handle);
1429
+ const isMissing = sourceMissing || targetMissing;
1430
+ const style = isMissing
1431
+ ? EDGE_STYLE_MISSING
1432
+ : hasError || isInvalidEdge
1433
+ ? EDGE_STYLE_ERROR
1434
+ : isRunning
1435
+ ? EDGE_STYLE_RUNNING
1436
+ : undefined;
1358
1437
  return {
1359
1438
  id: e.id,
1360
1439
  source: e.source.nodeId,
@@ -1364,7 +1443,7 @@ function toReactFlow(def, positions, registry, opts) {
1364
1443
  selected: opts.selectedEdgeIds
1365
1444
  ? opts.selectedEdgeIds.has(e.id)
1366
1445
  : undefined,
1367
- animated: isRunning,
1446
+ animated: isRunning && !isMissing,
1368
1447
  style,
1369
1448
  label: e.typeId || undefined,
1370
1449
  };
@@ -2161,6 +2240,17 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2161
2240
  }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) }), debug && (jsxRuntime.jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsxRuntime.jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
2162
2241
  }
2163
2242
 
2243
+ function NodeHandleItem({ kind, id, type, position, y, isConnectable, className, labelClassName, renderLabel, }) {
2244
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: id, type: type, position: position, isConnectable: isConnectable, className: className, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + (kind === "input" ? " left-2" : " right-2"), style: {
2245
+ top: (y ?? 0) - 8,
2246
+ ...(kind === "input"
2247
+ ? { right: "50%" }
2248
+ : { left: "50%", textAlign: "right" }),
2249
+ whiteSpace: "nowrap",
2250
+ overflow: "hidden",
2251
+ textOverflow: "ellipsis",
2252
+ }, children: renderLabel({ kind, id }) }))] }));
2253
+ }
2164
2254
  function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray-600", outputClassName = "!w-2 !h-2 !bg-gray-600", getClassName, renderLabel, labelClassName = "absolute text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", }) {
2165
2255
  const layout = data.handleLayout ?? [];
2166
2256
  const byId = React.useMemo(() => {
@@ -2171,40 +2261,49 @@ function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray
2171
2261
  position: h.position,
2172
2262
  y: h.y,
2173
2263
  type: h.type,
2264
+ missing: h.missing,
2174
2265
  });
2175
2266
  // Back-compat: also store by id-only if not already set
2176
2267
  if (!m.has(h.id))
2177
- m.set(h.id, { position: h.position, y: h.y, type: h.type });
2268
+ m.set(h.id, {
2269
+ position: h.position,
2270
+ y: h.y,
2271
+ type: h.type,
2272
+ missing: h.missing,
2273
+ });
2178
2274
  }
2179
2275
  return m;
2180
2276
  }, [layout]);
2277
+ const inputIds = React.useMemo(() => new Set((data.inputHandles ?? []).map((h) => h.id)), [data.inputHandles]);
2278
+ const outputIds = React.useMemo(() => new Set((data.outputHandles ?? []).map((h) => h.id)), [data.outputHandles]);
2279
+ const missingInputs = React.useMemo(() => (layout || []).filter((h) => h.type === "target" && (!inputIds.has(h.id) || h.missing)), [layout, inputIds]);
2280
+ const missingOutputs = React.useMemo(() => (layout || []).filter((h) => h.type === "source" && (!outputIds.has(h.id) || h.missing)), [layout, outputIds]);
2181
2281
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(data.inputHandles ?? []).map((h) => {
2182
2282
  const placed = byId.get(`target:${h.id}`) ?? byId.get(h.id);
2183
2283
  const position = placed?.position ?? react.Position.Left;
2184
2284
  const y = placed?.y;
2185
2285
  const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ??
2186
2286
  inputClassName;
2187
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: h.id, type: "target", position: position, isConnectable: isConnectable, className: cls, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + " left-2", style: {
2188
- top: (y ?? 0) - 8,
2189
- right: "50%",
2190
- whiteSpace: "nowrap",
2191
- overflow: "hidden",
2192
- textOverflow: "ellipsis",
2193
- }, children: renderLabel({ kind: "input", id: h.id }) }))] }, h.id));
2287
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "input", id: h.id, type: "target", position: position, y: y, isConnectable: isConnectable, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, h.id));
2288
+ }), missingInputs.map((h) => {
2289
+ const key = `missing-input:${h.id}`;
2290
+ const position = h.position ?? react.Position.Left;
2291
+ const y = h.y;
2292
+ const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500";
2293
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "input", id: h.id, type: "target", position: position, y: y, isConnectable: false, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, key));
2194
2294
  }), (data.outputHandles ?? []).map((h) => {
2195
2295
  const placed = byId.get(`source:${h.id}`) ?? byId.get(h.id);
2196
2296
  const position = placed?.position ?? react.Position.Right;
2197
2297
  const y = placed?.y;
2198
2298
  const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ??
2199
2299
  outputClassName;
2200
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: h.id, type: "source", position: position, isConnectable: isConnectable, className: `${cls} wb-nodrag wb-nowheel`, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + " right-2", style: {
2201
- top: (y ?? 0) - 8,
2202
- left: "50%",
2203
- textAlign: "right",
2204
- whiteSpace: "nowrap",
2205
- overflow: "hidden",
2206
- textOverflow: "ellipsis",
2207
- }, children: renderLabel({ kind: "output", id: h.id }) }))] }, h.id));
2300
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: isConnectable, className: `${cls} wb-nodrag wb-nowheel`, labelClassName: labelClassName, renderLabel: renderLabel }, h.id));
2301
+ }), missingOutputs.map((h) => {
2302
+ const key = `missing-output:${h.id}`;
2303
+ const position = h.position ?? react.Position.Right;
2304
+ const y = h.y;
2305
+ const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500 !rounded-none wb-nodrag wb-nowheel";
2306
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: false, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, key));
2208
2307
  })] }));
2209
2308
  }
2210
2309
 
@@ -2297,7 +2396,7 @@ function DefaultNodeContent({ data, isConnectable, }) {
2297
2396
  const entries = kind === "input" ? inputEntries : outputEntries;
2298
2397
  const entry = entries.find((e) => e.id === handleId);
2299
2398
  if (!entry)
2300
- return handleId;
2399
+ return prettyHandle(handleId);
2301
2400
  const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === handleId);
2302
2401
  const hasAny = vIssues.length > 0;
2303
2402
  const hasErr = vIssues.some((v) => v.level === "error");