diff --git a/src/components/ConnectionDropMenu.tsx b/src/components/ConnectionDropMenu.tsx
index 4ee826c8..528b29a8 100644
--- a/src/components/ConnectionDropMenu.tsx
+++ b/src/components/ConnectionDropMenu.tsx
@@ -266,6 +266,15 @@ const IMAGE_SOURCE_OPTIONS: MenuOption[] = [
),
},
+ {
+ type: "conditionalSwitch",
+ label: "Conditional Switch",
+ icon: (
+
+ ),
+ },
];
const TEXT_SOURCE_OPTIONS: MenuOption[] = [
@@ -317,6 +326,24 @@ const TEXT_SOURCE_OPTIONS: MenuOption[] = [
),
},
+ {
+ type: "switch",
+ label: "Switch",
+ icon: (
+
+ ),
+ },
+ {
+ type: "conditionalSwitch",
+ label: "Conditional Switch",
+ icon: (
+
+ ),
+ },
];
// Video can only connect to videoStitch, generateVideo (video-to-video), or output nodes
diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx
index d2cae88c..d1982c6c 100644
--- a/src/components/WorkflowCanvas.tsx
+++ b/src/components/WorkflowCanvas.tsx
@@ -42,6 +42,7 @@ import {
VideoFrameGrabNode,
RouterNode,
SwitchNode,
+ ConditionalSwitchNode,
} from "./nodes";
// Lazy-load GLBViewerNode to avoid bundling three.js for users who don't use 3D nodes
@@ -84,6 +85,7 @@ const nodeTypes: NodeTypes = {
videoFrameGrab: VideoFrameGrabNode,
router: RouterNode,
switch: SwitchNode,
+ conditionalSwitch: ConditionalSwitchNode,
glbViewer: GLBViewerNode,
};
@@ -164,6 +166,9 @@ const getNodeHandles = (nodeType: string): { inputs: string[]; outputs: string[]
// Switch has one input handle (generic-input when disconnected, typed when connected)
// Output handles are dynamic based on switches array, all matching inputType
return { inputs: ["generic-input"], outputs: [] }; // Outputs handled dynamically in SwitchNode
+ case "conditionalSwitch":
+ // Conditional Switch has one text input and dynamic rule outputs + default
+ return { inputs: ["text"], outputs: [] }; // Outputs handled dynamically in ConditionalSwitchNode
case "glbViewer":
return { inputs: ["3d"], outputs: ["image"] };
default:
@@ -365,6 +370,14 @@ export function WorkflowCanvas() {
return true;
}
+ // Conditional Switch: text input only, text outputs only
+ if (targetNode?.type === "conditionalSwitch") {
+ return sourceType === "text";
+ }
+ if (sourceNode?.type === "conditionalSwitch") {
+ return targetType === "text";
+ }
+
// If we can't determine types, allow the connection
if (!sourceType || !targetType) return true;
@@ -663,6 +676,20 @@ export function WorkflowCanvas() {
return null;
}
+ // Conditional Switch: text input, dynamic rule outputs
+ if (node.type === "conditionalSwitch" && handleType === "text") {
+ if (needInput) {
+ return "text";
+ } else {
+ // Return first rule ID from node data
+ const condData = node.data as { rules?: Array<{ id: string }> };
+ if (condData.rules && condData.rules.length > 0) {
+ return condData.rules[0].id;
+ }
+ return "default";
+ }
+ }
+
// Fall back to static handles
const staticHandles = getNodeHandles(node.type || "");
const handleList = needInput ? staticHandles.inputs : staticHandles.outputs;
@@ -969,6 +996,17 @@ export function WorkflowCanvas() {
}
// Switch outputs use dynamic handle IDs (switch entry IDs)
sourceHandleIdForNewNode = null;
+ } else if (nodeType === "conditionalSwitch") {
+ // Conditional Switch: text input and dynamic rule outputs
+ targetHandleId = "text";
+ // Source handle is the first rule ID or "default"
+ const nodeDataCheck = nodes.find(n => n.id === newNodeId);
+ if (nodeDataCheck && nodeDataCheck.data) {
+ const condData = nodeDataCheck.data as { rules?: Array<{ id: string }> };
+ sourceHandleIdForNewNode = condData.rules && condData.rules.length > 0 ? condData.rules[0].id : "default";
+ } else {
+ sourceHandleIdForNewNode = "default";
+ }
} else if (handleType === "image") {
if (nodeType === "annotation" || nodeType === "output" || nodeType === "splitGrid" || nodeType === "outputGallery" || nodeType === "imageCompare") {
targetHandleId = "image";
@@ -1899,6 +1937,8 @@ export function WorkflowCanvas() {
return "#6b7280"; // neutral-500 (gray/slate utility theme)
case "switch":
return "#8b5cf6"; // violet-500 (distinct from Router)
+ case "conditionalSwitch":
+ return "#06b6d4"; // cyan-500 (distinct from Router gray and Switch violet)
case "glbViewer":
return "#0ea5e9"; // sky-500 (3D viewport)
default: