Browse Source

feat(45-02): register ConditionalSwitch in WorkflowCanvas and connection menus

- Import and add ConditionalSwitchNode to nodeTypes map
- Add getNodeHandles case with text input and dynamic outputs
- Add cyan-500 (#06b6d4) minimap color distinct from Router/Switch
- Add isValidConnection text-only validation
- Add handleMenuSelect with text handle and first rule/default source
- Add findCompatibleHandle for text input and rule outputs
- Add ConditionalSwitch to TEXT_TARGET_OPTIONS and TEXT_SOURCE_OPTIONS with filter icon
- Positioned after Switch entry in both menus
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
b6529a55d2
  1. 27
      src/components/ConnectionDropMenu.tsx
  2. 40
      src/components/WorkflowCanvas.tsx

27
src/components/ConnectionDropMenu.tsx

@ -266,6 +266,15 @@ const IMAGE_SOURCE_OPTIONS: MenuOption[] = [
</svg>
),
},
{
type: "conditionalSwitch",
label: "Conditional Switch",
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" />
</svg>
),
},
];
const TEXT_SOURCE_OPTIONS: MenuOption[] = [
@ -317,6 +326,24 @@ const TEXT_SOURCE_OPTIONS: MenuOption[] = [
</svg>
),
},
{
type: "switch",
label: "Switch",
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
),
},
{
type: "conditionalSwitch",
label: "Conditional Switch",
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" />
</svg>
),
},
];
// Video can only connect to videoStitch, generateVideo (video-to-video), or output nodes

40
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:

Loading…
Cancel
Save