diff --git a/src/components/nodes/SwitchNode.tsx b/src/components/nodes/SwitchNode.tsx new file mode 100644 index 00000000..e015f428 --- /dev/null +++ b/src/components/nodes/SwitchNode.tsx @@ -0,0 +1,277 @@ +"use client"; + +import { memo, useMemo, useEffect, useState, useCallback } from "react"; +import { Handle, Position, useUpdateNodeInternals, useReactFlow, NodeProps } from "@xyflow/react"; +import { BaseNode } from "./BaseNode"; +import { useWorkflowStore } from "@/store/workflowStore"; +import type { WorkflowNode, SwitchNodeData, HandleType } from "@/types"; + +const HANDLE_COLORS: Record = { + image: "#10b981", // emerald — matches globals.css + text: "#3b82f6", // blue — matches globals.css + video: "#ffffff", // white — default handle style + audio: "rgb(167, 139, 250)", // violet — matches GenerateAudioNode/OutputNode + "3d": "#f97316", // orange — matches globals.css + easeCurve: "#ffffff", // white — default handle style +}; + +export const SwitchNode = memo(({ id, data, selected }: NodeProps) => { + const nodeData = data as SwitchNodeData; + const edges = useWorkflowStore((state) => state.edges); + const updateNodeData = useWorkflowStore((state) => state.updateNodeData); + const updateNodeInternals = useUpdateNodeInternals(); + const { setNodes, setEdges } = useReactFlow(); + const [editingId, setEditingId] = useState(null); + + // Derive inputType from incoming edge connection + const derivedInputType = useMemo(() => { + const inputEdge = edges.find((e) => e.target === id); + return inputEdge?.targetHandle as HandleType | undefined; + }, [edges, id]); + + // Update stored inputType when derived type changes + useEffect(() => { + const newType = derivedInputType || null; + if (newType !== nodeData.inputType) { + updateNodeData(id, { inputType: newType }); + } + }, [derivedInputType, id, nodeData.inputType, updateNodeData]); + + // Calculate handle positioning + const handleSpacing = 32; + const baseOffset = 38; // Clear the header bar + + // Dynamic height based on switch count and whether we have input + const switchCount = nodeData.switches.length; + const showOutputs = nodeData.inputType !== null; + const lastHandleTop = baseOffset + (showOutputs ? switchCount * handleSpacing : handleSpacing); + const minHeight = lastHandleTop + 40; // Extra space for add button + + // Resize node and notify React Flow when switch count or inputType changes + useEffect(() => { + setNodes((nodes) => + nodes.map((node) => { + if (node.id === id) { + const currentHeight = (node.style?.height as number) || 0; + if (currentHeight < minHeight) { + return { ...node, style: { ...node.style, height: minHeight } }; + } + } + return node; + }) + ); + updateNodeInternals(id); + }, [switchCount, showOutputs, id, minHeight, setNodes, updateNodeInternals]); + + // Handle toggle change + const handleToggle = useCallback( + (switchId: string) => { + const updatedSwitches = nodeData.switches.map((sw) => + sw.id === switchId ? { ...sw, enabled: !sw.enabled } : sw + ); + updateNodeData(id, { switches: updatedSwitches }); + }, + [id, nodeData.switches, updateNodeData] + ); + + // Handle name edit + const handleNameEdit = useCallback( + (switchId: string, newName: string) => { + const updatedSwitches = nodeData.switches.map((sw) => + sw.id === switchId ? { ...sw, name: newName } : sw + ); + updateNodeData(id, { switches: updatedSwitches }); + setEditingId(null); + }, + [id, nodeData.switches, updateNodeData] + ); + + // Handle delete switch + const handleDelete = useCallback( + (switchId: string) => { + const updatedSwitches = nodeData.switches.filter((sw) => sw.id !== switchId); + updateNodeData(id, { switches: updatedSwitches }); + + // Remove edges connected to this handle + setEdges((edges) => edges.filter((e) => !(e.source === id && e.sourceHandle === switchId))); + }, + [id, nodeData.switches, updateNodeData, setEdges] + ); + + // Handle add switch + const handleAddSwitch = useCallback(() => { + const newSwitch = { + id: Math.random().toString(36).slice(2, 9), + name: `Output ${nodeData.switches.length + 1}`, + enabled: true, + }; + updateNodeData(id, { switches: [...nodeData.switches, newSwitch] }); + }, [id, nodeData.switches, updateNodeData]); + + return ( + updateNodeData(id, { customTitle })} + onCommentChange={(comment) => updateNodeData(id, { comment })} + selected={selected} + minWidth={220} + minHeight={minHeight} + className="bg-violet-950/50 border-violet-600" + > + {/* Input handle (left) */} + {nodeData.inputType ? ( + + ) : ( + + )} + + {/* Output handles (right) - only when input connected */} + {showOutputs && + nodeData.switches.map((sw, index) => ( + + ))} + + {/* Body content */} +
+ {!showOutputs ? ( +
+ Connect input to enable outputs +
+ ) : ( + <> + {nodeData.switches.map((sw, index) => ( +
+ {/* Toggle */} +
+ + ); +}); + +SwitchNode.displayName = "SwitchNode";