diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 35b6f223..929bf343 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -13,6 +13,7 @@ import { useReactFlow, OnConnectEnd, Node, + OnSelectionChangeParams, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; @@ -80,7 +81,7 @@ const getNodeHandles = (nodeType: string): { inputs: string[]; outputs: string[] case "annotation": return { inputs: ["image"], outputs: ["image"] }; case "prompt": - return { inputs: [], outputs: ["text"] }; + return { inputs: ["text"], outputs: ["text"] }; case "nanoBanana": return { inputs: ["image", "text"], outputs: ["image"] }; case "generateVideo": @@ -613,6 +614,8 @@ export function WorkflowCanvas() { sourceHandleIdForNewNode = "text"; } } else if (nodeType === "prompt") { + // prompt can receive and output text + targetHandleId = "text"; sourceHandleIdForNewNode = "text"; } } @@ -993,6 +996,53 @@ export function WorkflowCanvas() { return () => window.removeEventListener("keydown", handleKeyDown); }, [handleKeyDown]); + + // Fix for React Flow selection bug where nodes with undefined bounds get incorrectly selected. + // Uses statistical outlier detection to identify and deselect nodes that are clearly + // outside the actual selection area. + const handleSelectionChange = useCallback(({ nodes: selectedNodes }: OnSelectionChangeParams) => { + if (selectedNodes.length <= 1) return; + + // Get positions of all selected nodes + const positions = selectedNodes.map(n => ({ + id: n.id, + x: n.position.x, + y: n.position.y, + })); + + // Calculate IQR-based bounds for outlier detection + const sortedX = [...positions].sort((a, b) => a.x - b.x); + const sortedY = [...positions].sort((a, b) => a.y - b.y); + + const q1X = sortedX[Math.floor(sortedX.length * 0.25)].x; + const q3X = sortedX[Math.floor(sortedX.length * 0.75)].x; + const q1Y = sortedY[Math.floor(sortedY.length * 0.25)].y; + const q3Y = sortedY[Math.floor(sortedY.length * 0.75)].y; + const iqrX = q3X - q1X; + const iqrY = q3Y - q1Y; + + // Outlier threshold: 3x IQR from quartiles + const minX = q1X - iqrX * 3; + const maxX = q3X + iqrX * 3; + const minY = q1Y - iqrY * 3; + const maxY = q3Y + iqrY * 3; + + // Find and deselect outliers + const outliers = positions.filter(p => + p.x < minX || p.x > maxX || p.y < minY || p.y > maxY + ); + + if (outliers.length > 0) { + onNodesChange( + outliers.map(o => ({ + type: 'select' as const, + id: o.id, + selected: false, + })) + ); + } + }, [onNodesChange]); + const handleDragOver = useCallback((event: DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = "copy"; @@ -1198,6 +1248,7 @@ export function WorkflowCanvas() { onConnect={handleConnect} onConnectEnd={handleConnectEnd} onNodeDragStop={handleNodeDragStop} + onSelectionChange={handleSelectionChange} nodeTypes={nodeTypes} edgeTypes={edgeTypes} isValidConnection={isValidConnection} diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 8ea6a2d1..7a959909 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -1000,9 +1000,14 @@ export const useWorkflowStore = create((set, get) => ({ break; } - case "prompt": - // Nothing to execute, data is already set + case "prompt": { + // Check for connected text input and update prompt if connected + const { text: connectedText } = getConnectedInputs(node.id); + if (connectedText !== null) { + updateNodeData(node.id, { prompt: connectedText }); + } break; + } case "nanoBanana": { const { images, text, dynamicInputs } = getConnectedInputs(node.id); @@ -2368,7 +2373,8 @@ export const useWorkflowStore = create((set, get) => ({ const workflow: WorkflowFile = { version: 1, name: name || `workflow-${new Date().toISOString().slice(0, 10)}`, - nodes, + // Strip selected property - selection is transient UI state and should not be persisted + nodes: nodes.map(({ selected, ...rest }) => rest), edges, edgeStyle, groups: Object.keys(groups).length > 0 ? groups : undefined, @@ -2452,7 +2458,16 @@ export const useWorkflowStore = create((set, get) => ({ const costData = workflow.id ? loadWorkflowCostData(workflow.id) : null; set({ - nodes: hydratedWorkflow.nodes, + // Clear selected state - selection should not be persisted across sessions + // Also validate position to ensure coordinates are finite numbers + nodes: hydratedWorkflow.nodes.map(node => ({ + ...node, + selected: false, + position: { + x: isFinite(node.position?.x) ? node.position.x : 0, + y: isFinite(node.position?.y) ? node.position.y : 0, + }, + })), edges: hydratedWorkflow.edges, edgeStyle: hydratedWorkflow.edgeStyle || "angular", groups: hydratedWorkflow.groups || {},