@ -13,6 +13,7 @@ import {
useReactFlow ,
useReactFlow ,
OnConnectEnd ,
OnConnectEnd ,
Node ,
Node ,
OnSelectionChangeParams ,
} from "@xyflow/react" ;
} from "@xyflow/react" ;
import "@xyflow/react/dist/style.css" ;
import "@xyflow/react/dist/style.css" ;
@ -80,7 +81,7 @@ const getNodeHandles = (nodeType: string): { inputs: string[]; outputs: string[]
case "annotation" :
case "annotation" :
return { inputs : [ "image" ] , outputs : [ "image" ] } ;
return { inputs : [ "image" ] , outputs : [ "image" ] } ;
case "prompt" :
case "prompt" :
return { inputs : [ ] , outputs : [ "text" ] } ;
return { inputs : [ "text" ] , outputs : [ "text" ] } ;
case "nanoBanana" :
case "nanoBanana" :
return { inputs : [ "image" , "text" ] , outputs : [ "image" ] } ;
return { inputs : [ "image" , "text" ] , outputs : [ "image" ] } ;
case "generateVideo" :
case "generateVideo" :
@ -613,6 +614,8 @@ export function WorkflowCanvas() {
sourceHandleIdForNewNode = "text" ;
sourceHandleIdForNewNode = "text" ;
}
}
} else if ( nodeType === "prompt" ) {
} else if ( nodeType === "prompt" ) {
// prompt can receive and output text
targetHandleId = "text" ;
sourceHandleIdForNewNode = "text" ;
sourceHandleIdForNewNode = "text" ;
}
}
}
}
@ -993,6 +996,53 @@ export function WorkflowCanvas() {
return ( ) = > window . removeEventListener ( "keydown" , handleKeyDown ) ;
return ( ) = > window . removeEventListener ( "keydown" , handleKeyDown ) ;
} , [ 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 < HTMLDivElement > ) = > {
const handleDragOver = useCallback ( ( event : DragEvent < HTMLDivElement > ) = > {
event . preventDefault ( ) ;
event . preventDefault ( ) ;
event . dataTransfer . dropEffect = "copy" ;
event . dataTransfer . dropEffect = "copy" ;
@ -1198,6 +1248,7 @@ export function WorkflowCanvas() {
onConnect = { handleConnect }
onConnect = { handleConnect }
onConnectEnd = { handleConnectEnd }
onConnectEnd = { handleConnectEnd }
onNodeDragStop = { handleNodeDragStop }
onNodeDragStop = { handleNodeDragStop }
onSelectionChange = { handleSelectionChange }
nodeTypes = { nodeTypes }
nodeTypes = { nodeTypes }
edgeTypes = { edgeTypes }
edgeTypes = { edgeTypes }
isValidConnection = { isValidConnection }
isValidConnection = { isValidConnection }