diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 3b48e875..ec8ec4bc 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -319,9 +319,42 @@ export function WorkflowCanvas() { const { clientX, clientY } = event as MouseEvent; const fromHandleId = connectionState.fromHandle?.id || null; - const fromHandleType = (fromHandleId === "image" || fromHandleId === "text") ? fromHandleId : null; + const fromHandleType = getHandleType(fromHandleId); // Use getHandleType for dynamic handles const isFromSource = connectionState.fromHandle?.type === "source"; + // Helper to find a compatible handle on a node by type + const findCompatibleHandle = ( + node: Node, + handleType: "image" | "text", + needInput: boolean + ): string | null => { + // Check for dynamic inputSchema first + const nodeData = node.data as { inputSchema?: Array<{ name: string; type: string }> }; + if (nodeData.inputSchema && nodeData.inputSchema.length > 0) { + if (needInput) { + // Find first input handle matching the type + const match = nodeData.inputSchema.find(i => i.type === handleType); + if (match) return match.name; + } + // Output is still "image" for generate nodes + return handleType === "image" ? "image" : null; + } + + // Fall back to static handles + const staticHandles = getNodeHandles(node.type || ""); + const handleList = needInput ? staticHandles.inputs : staticHandles.outputs; + + // First try exact match + if (handleList.includes(handleType)) return handleType; + + // Then check each handle's type + for (const h of handleList) { + if (getHandleType(h) === handleType) return h; + } + + return null; + }; + // Check if we dropped on a node by looking for node elements under the cursor const elementsUnderCursor = document.elementsFromPoint(clientX, clientY); const nodeElement = elementsUnderCursor.find((el) => { @@ -337,22 +370,12 @@ export function WorkflowCanvas() { const targetNode = nodes.find((n) => n.id === targetNodeId); if (targetNode) { - const targetHandles = getNodeHandles(targetNode.type || ""); - // Find a compatible handle on the target node - let compatibleHandle: string | null = null; - - if (isFromSource) { - // Dragging from output, need an input on target that matches type - if (targetHandles.inputs.includes(fromHandleType)) { - compatibleHandle = fromHandleType; - } - } else { - // Dragging from input, need an output on target that matches type - if (targetHandles.outputs.includes(fromHandleType)) { - compatibleHandle = fromHandleType; - } - } + const compatibleHandle = findCompatibleHandle( + targetNode, + fromHandleType, + isFromSource // need input if dragging from output + ); if (compatibleHandle) { // Create the connection @@ -391,7 +414,7 @@ export function WorkflowCanvas() { sourceHandleId: fromHandleId, }); }, - [screenToFlowPosition, nodes, getNodeHandles, handleConnect] + [screenToFlowPosition, nodes, handleConnect] ); // Handle the splitGrid action - uses automated grid detection @@ -530,16 +553,21 @@ export function WorkflowCanvas() { let sourceHandleIdForNewNode: string | null = null; // Map handle type to the correct handle ID based on node type + // Note: New nodes start with default handles (image, text) before a model is selected if (handleType === "image") { if (nodeType === "annotation" || nodeType === "output" || nodeType === "splitGrid") { targetHandleId = "image"; - } else if (nodeType === "nanoBanana") { + // annotation also has an image output + if (nodeType === "annotation") { + sourceHandleIdForNewNode = "image"; + } + } else if (nodeType === "nanoBanana" || nodeType === "generateVideo") { targetHandleId = "image"; } else if (nodeType === "imageInput") { sourceHandleIdForNewNode = "image"; } } else if (handleType === "text") { - if (nodeType === "nanoBanana" || nodeType === "llmGenerate") { + if (nodeType === "nanoBanana" || nodeType === "generateVideo" || nodeType === "llmGenerate") { targetHandleId = "text"; // llmGenerate also has a text output if (nodeType === "llmGenerate") {