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: