Browse Source

feat(43-02): integrate Router into connection menus, execution, and data flow

ConnectionDropMenu.tsx:
- Add Router entry to all 8 connection drop menu arrays
- Router appears as last item (utility node) in each menu
- Uses git-branch SVG icon for routing/branching concept

workflowStore.ts:
- Import executeRouter from execution module
- Add "router" case to executeWorkflow switch (after easeCurve)
- Add "router" case to regenerateNode switch (after easeCurve)

execution/index.ts:
- Export executeRouter from simpleNodeExecutors

connectedInputs.ts:
- Add optional visited parameter to getConnectedInputsPure for circular protection
- Add Router passthrough logic before getSourceOutput call
- Router nodes recursively fetch upstream data and route by handle type
- Prevents infinite loops with visited Set tracking
- Handle all 6 data types: image, text, video, audio, 3d, easeCurve

RouterNode.tsx (bugfix):
- Add missing id prop to BaseNode (from 43-01)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
562da7cd90
  1. 96
      src/components/ConnectionDropMenu.tsx
  2. 1
      src/components/nodes/RouterNode.tsx
  3. 1
      src/store/execution/index.ts
  4. 35
      src/store/utils/connectedInputs.ts
  5. 7
      src/store/workflowStore.ts

96
src/components/ConnectionDropMenu.tsx

@ -88,6 +88,18 @@ const IMAGE_TARGET_OPTIONS: MenuOption[] = [
</svg>
),
},
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
];
const TEXT_TARGET_OPTIONS: MenuOption[] = [
@ -154,6 +166,18 @@ const TEXT_TARGET_OPTIONS: MenuOption[] = [
</svg>
),
},
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
];
// Define which nodes can provide sources for handle types (when dragging to a target handle)
@ -203,6 +227,18 @@ const IMAGE_SOURCE_OPTIONS: MenuOption[] = [
</svg>
),
},
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
];
const TEXT_SOURCE_OPTIONS: MenuOption[] = [
@ -242,6 +278,18 @@ const TEXT_SOURCE_OPTIONS: MenuOption[] = [
</svg>
),
},
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
];
// Video can only connect to videoStitch, generateVideo (video-to-video), or output nodes
@ -300,6 +348,18 @@ const VIDEO_TARGET_OPTIONS: MenuOption[] = [
</svg>
),
},
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
];
// GenerateVideo and VideoStitch nodes produce video output
@ -340,6 +400,18 @@ const VIDEO_SOURCE_OPTIONS: MenuOption[] = [
</svg>
),
},
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
];
// Audio target options (nodes that accept audio input)
@ -371,6 +443,18 @@ const AUDIO_TARGET_OPTIONS: MenuOption[] = [
</svg>
),
},
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
];
// Audio source options (nodes that produce audio output)
@ -393,6 +477,18 @@ const AUDIO_SOURCE_OPTIONS: MenuOption[] = [
</svg>
),
},
{
type: "router",
label: "Router",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
<path d="M6 3v12" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
),
},
];
// 3D target options (nodes that accept 3D input)

1
src/components/nodes/RouterNode.tsx

@ -56,6 +56,7 @@ export const RouterNode = memo(({ id, data, selected }: NodeProps<WorkflowNode>)
return (
<BaseNode
id={id}
title="Router"
customTitle={nodeData.customTitle}
comment={nodeData.comment}

1
src/store/execution/index.ts

@ -17,6 +17,7 @@ export {
executeOutputGallery,
executeImageCompare,
executeGlbViewer,
executeRouter,
} from "./simpleNodeExecutors";
export { executeNanoBanana } from "./nanoBananaExecutor";

35
src/store/utils/connectedInputs.ts

@ -123,14 +123,19 @@ function getSourceOutput(
export function getConnectedInputsPure(
nodeId: string,
nodes: WorkflowNode[],
edges: WorkflowEdge[]
edges: WorkflowEdge[],
visited?: Set<string>
): ConnectedInputs {
const _visited = visited || new Set<string>();
if (_visited.has(nodeId)) return { images: [], videos: [], audio: [], model3d: null, text: null, dynamicInputs: {}, easeCurve: null };
_visited.add(nodeId);
const images: string[] = [];
const videos: string[] = [];
const audio: string[] = [];
let model3d: string | null = null;
let text: string | null = null;
const dynamicInputs: Record<string, string | string[]> = {};
let easeCurve: ConnectedInputs["easeCurve"] = null;
// Get the target node to check for inputSchema
const targetNode = nodes.find((n) => n.id === nodeId);
@ -163,6 +168,29 @@ export function getConnectedInputsPure(
const sourceNode = nodes.find((n) => n.id === edge.source);
if (!sourceNode) return;
// Router passthrough — traverse upstream to find actual data source
if (sourceNode.type === "router") {
const routerInputs = getConnectedInputsPure(sourceNode.id, nodes, edges, _visited);
// Determine which type this edge carries based on the source handle
const edgeType = edge.sourceHandle; // Will be "image", "text", "video", "audio", "3d", or "easeCurve"
if (edgeType === "image" || (!edgeType && isImageHandle(edge.sourceHandle))) {
images.push(...routerInputs.images);
} else if (edgeType === "text" || (!edgeType && isTextHandle(edge.sourceHandle))) {
if (routerInputs.text) text = routerInputs.text;
} else if (edgeType === "video") {
videos.push(...routerInputs.videos);
} else if (edgeType === "audio") {
audio.push(...routerInputs.audio);
} else if (edgeType === "3d") {
if (routerInputs.model3d) model3d = routerInputs.model3d;
} else if (edgeType === "easeCurve") {
// EaseCurve passthrough
if (routerInputs.easeCurve) easeCurve = routerInputs.easeCurve;
}
return; // Skip normal getSourceOutput processing for this edge
}
const handleId = edge.targetHandle;
const { type, value } = getSourceOutput(
sourceNode,
@ -201,8 +229,8 @@ export function getConnectedInputsPure(
}
});
// Extract easeCurve data from parent EaseCurve node
let easeCurve: ConnectedInputs["easeCurve"] = null;
// Extract easeCurve data from parent EaseCurve node (if not already set by router passthrough)
if (!easeCurve) {
const easeCurveEdge = edges.find(
(e) => e.target === nodeId && e.targetHandle === "easeCurve"
);
@ -216,6 +244,7 @@ export function getConnectedInputsPure(
};
}
}
}
return { images, videos, audio, model3d, text, dynamicInputs, easeCurve };
}

7
src/store/workflowStore.ts

@ -77,6 +77,7 @@ import {
executeVideoTrim,
executeVideoFrameGrab,
executeGlbViewer,
executeRouter,
} from "./execution";
import type { NodeExecutionContext } from "./execution";
export type { LevelGroup } from "./utils/executionUtils";
@ -1008,6 +1009,9 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
case "videoFrameGrab":
await executeVideoFrameGrab(executionCtx);
break;
case "router":
await executeRouter(executionCtx);
break;
}
}; // End of executeSingleNode helper
@ -1326,6 +1330,9 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
case "videoFrameGrab":
await executeVideoFrameGrab(executionCtx);
break;
case "router":
await executeRouter(executionCtx);
break;
}
};

Loading…
Cancel
Save