fix: veo handle connection and coordinate alignment issues#92
fix: veo handle connection and coordinate alignment issues#92charliepvc wants to merge 6 commits intoshrimbly:masterfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a BaseNode Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant GenerateNode as GenerateImage/Video/AudioNode
participant BaseNode
participant ReactFlow as FlowInternals
participant Store as connectedInputs
User->>GenerateNode: select model / connect input handles / provide data
GenerateNode->>GenerateNode: detect model (isVeo?) and build/migrate input schema
GenerateNode->>BaseNode: pass `handles` ReactNode (input/output handles)
GenerateNode->>ReactFlow: call updateNodeInternals (on schema/structure change)
User->>BaseNode: create/modify connection on a handle
BaseNode->>Store: emit connection/update events
Store->>Store: resolve connected inputs (primary text -> text, others -> dynamicInputs)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/nodes/GenerateImageNode.tsx`:
- Around line 476-485: The handles rendered in GenerateImageNode lost pointer
interaction because BaseNode now wraps handles in a pointer-events-none overlay;
update the Handle elements inside GenerateImageNode (the Handle with id="image"
type="target", id="text" type="target", and id="image" type="source") to opt
back into pointer events by setting their style (or class) to include
pointerEvents: "auto" (or the equivalent CSS class used in GenerateVideoNode) so
the Image and Prompt ports become connectable again; mirror the same approach
used in GenerateVideoNode to restore pointer-events for these specific Handle
components.
In `@src/components/nodes/GenerateVideoNode.tsx`:
- Around line 487-504: The code currently pushes placeholder handles with id
"image" and "text" even when the model doesn't support them; update the logic in
GenerateVideoNode.tsx (the block that builds the handles array using
hasImageInput, hasTextInput, textInputs) to skip adding those canonical "image"
or "text" handles entirely when hasImageInput/hasTextInput is false (do not add
isPlaceholder entries), and apply the same fix to the similar block around lines
518-535 so unsupported ports are not registered for canvas validation or
connection.
- Around line 553-564: The fallback target handles (Handle id="image" and
id="text") and the video source handle (Handle id="video") are non-interactive
because they live inside BaseNode's pointer-events-none overlay; update each
Handle's style prop to include pointerEvents: "auto" (e.g., merge into the
existing style object for the image/text handles and for the video handle) so
those handles become connectable/draggable even when the overlay disables
pointer events.
In `@src/store/utils/connectedInputs.ts`:
- Around line 290-300: The code incorrectly detects the primary text input by
checking fixed ids ("text", "text-0", "prompt"), causing custom-named text
handles (e.g., lyrics) to be ignored; update the logic in the block handling
type === "text" (and the isPrimaryText decision around handleId, text, and
dynamicInputs) to derive the primary text handle from the node schema instead:
call getConnectedInputs(nodeId) to obtain the node's text field (e.g., { images,
text }) and treat that schema-derived name as the primary text, falling back to
handleToSchemaName(handleId) to map normalized ids into dynamicInputs when not
primary so that custom handle names populate either the root text variable or
dynamicInputs appropriately.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 98e42a33-b645-42c6-8e31-458fa4f75b53
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
package.jsonserver.jssrc/app/page.tsxsrc/components/nodes/BaseNode.tsxsrc/components/nodes/GenerateImageNode.tsxsrc/components/nodes/GenerateVideoNode.tsxsrc/store/utils/connectedInputs.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/nodes/GenerateVideoNode.tsx`:
- Around line 35-46: The isI2V check in buildVeoInputSchema is too narrow (only
checks id.includes("image-to-video")), causing some Veo image-to-video variants
(like those containing "i2v" or "i2v" with spaces/caps) to be treated as
text-only; update the heuristic in buildVeoInputSchema to detect I2V models more
broadly (e.g., check for multiple tokens such as "image-to-video", "i2v", "image
to video", and case-insensitive variants) before building the schema so the
image port is included; adjust the isI2V assignment and keep imageParamName
logic intact so image handling still chooses between "imageUrls" and "image".
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7787e6ce-7db2-4746-bea3-f1d8185cbab2
📒 Files selected for processing (3)
src/components/nodes/GenerateImageNode.tsxsrc/components/nodes/GenerateVideoNode.tsxsrc/store/utils/connectedInputs.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/store/utils/connectedInputs.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/components/nodes/GenerateVideoNode.tsx (1)
595-598: Consider using a video-specific color for the output label.The video output handle label uses
var(--handle-color-image). If there's a--handle-color-videoCSS variable defined for consistency with other video-related styling, consider using it here.💅 Proposed styling consistency fix
- <div className="absolute text-[10px] font-medium whitespace-nowrap pointer-events-none" style={{ left: `calc(100% + 8px)`, top: "calc(50% - 18px)", color: "var(--handle-color-image)" }}>Video</div> + <div className="absolute text-[10px] font-medium whitespace-nowrap pointer-events-none" style={{ left: `calc(100% + 8px)`, top: "calc(50% - 18px)", color: "var(--handle-color-video, var(--handle-color-image))" }}>Video</div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/nodes/GenerateVideoNode.tsx` around lines 595 - 598, The output label for the video handle in GenerateVideoNode.tsx is using the image color variable (--handle-color-image); update the label styling to use the video-specific CSS variable by replacing the color reference on the div that labels the Handle with --handle-color-video (or use a fallback like var(--handle-color-video, var(--handle-color-image))) so the label matches other video styling; target the div next to the Handle with id="video" in GenerateVideoNode.tsx when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/store/utils/connectedInputs.ts`:
- Around line 318-335: The routing fails because GenerateAudioNode.tsx sets
handle id to schema names (input.name) so handleToSchemaName lookup in
connectedInputs.ts (used by isPrimaryText and the text assignment logic) returns
undefined; update GenerateAudioNode.tsx to use normalized handle IDs ("text" for
primary and "text-1", "text-2", ...) for subsequent text inputs instead of
input.name, ensuring the handleId values match the expectations in
connectedInputs.ts (which checks for "text", "text-0", "prompt" and maps
handleToSchemaName), and keep the mapping from these normalized ids to schema
names so handleToSchemaName still resolves correctly.
---
Nitpick comments:
In `@src/components/nodes/GenerateVideoNode.tsx`:
- Around line 595-598: The output label for the video handle in
GenerateVideoNode.tsx is using the image color variable (--handle-color-image);
update the label styling to use the video-specific CSS variable by replacing the
color reference on the div that labels the Handle with --handle-color-video (or
use a fallback like var(--handle-color-video, var(--handle-color-image))) so the
label matches other video styling; target the div next to the Handle with
id="video" in GenerateVideoNode.tsx when making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 57687a34-78d3-4f42-84c8-947834716eb2
📒 Files selected for processing (3)
src/components/nodes/GenerateImageNode.tsxsrc/components/nodes/GenerateVideoNode.tsxsrc/store/utils/connectedInputs.ts
| // Multi-handle text input handling: | ||
| // By default, the first text input (prompt) should populate the main 'text' variable. | ||
| // Secondary inputs (e.g. text-1/negative_prompt) are handled via dynamicInputs. | ||
| const stringValue = typeof value === "string" ? value : String(value); | ||
|
|
||
| // Normalize handleId: if it's the primary handle for prompt, set the root 'text' | ||
| let isPrimaryText = !handleId || handleId === "text" || handleId === "text-0" || handleId === "prompt"; | ||
| if (inputSchema && inputSchema.length > 0) { | ||
| const textInputs = inputSchema.filter((i) => i.type === "text"); | ||
| if (textInputs.length > 0 && handleId) { | ||
| const schemaName = handleToSchemaName[handleId]; | ||
| isPrimaryText = schemaName === textInputs[0].name; | ||
| } | ||
| } | ||
|
|
||
| if (isPrimaryText) { | ||
| text = stringValue; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if any nodes use schema names directly as handle IDs instead of normalized IDs
rg "id=\{input\.name\}" src/components/nodes/ -A 2 -B 2
rg "id=\{.*\.name\}" src/components/nodes/ | grep -i "handle"Repository: shrimbly/node-banana
Length of output: 421
Fix GenerateAudioNode handle IDs to use normalized patterns for proper text routing.
The current multi-handle text routing logic in connectedInputs.ts relies on normalized handle IDs ("text", "text-0", etc.) to correctly identify primary text inputs. However, GenerateAudioNode.tsx uses id={input.name}, setting handle IDs to schema names directly (e.g., "lyrics"). This causes the schema lookup in lines 325-331 to fail—handleToSchemaName["lyrics"] is undefined—resulting in isPrimaryText = false and preventing text data from populating the main text field.
Change handle IDs in GenerateAudioNode.tsx to use normalized patterns: id="text" for the primary text input and id="text-1", id="text-2", etc. for secondary text inputs, per coding guidelines.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/store/utils/connectedInputs.ts` around lines 318 - 335, The routing fails
because GenerateAudioNode.tsx sets handle id to schema names (input.name) so
handleToSchemaName lookup in connectedInputs.ts (used by isPrimaryText and the
text assignment logic) returns undefined; update GenerateAudioNode.tsx to use
normalized handle IDs ("text" for primary and "text-1", "text-2", ...) for
subsequent text inputs instead of input.name, ensuring the handleId values match
the expectations in connectedInputs.ts (which checks for "text", "text-0",
"prompt" and maps handleToSchemaName), and keep the mapping from these
normalized ids to schema names so handleToSchemaName still resolves correctly.
…JSDoc documentation
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/nodes/GenerateAudioNode.tsx (1)
213-240:⚠️ Potential issue | 🟠 MajorPreserve
audioas its own handle family.
handleType = isImage ? "image" : "text"means anyinput.type === "audio"is rendered as atextport/id here. That will fail handle-type validation and misroute connected inputs for audio-to-audio connections, breaking the connection logic inWorkflowCanvas.tsxthat explicitly checkshandleId === "audio"or starts with"audio".💡 Suggested change
- let textCount = 0; - let imageCount = 0; + let textCount = 0; + let imageCount = 0; + let audioCount = 0; return nodeData.inputSchema.map((input, index) => { - const isImage = input.type === "image"; - const handleType = isImage ? "image" : "text"; + const handleType = + input.type === "image" + ? "image" + : input.type === "audio" + ? "audio" + : "text"; let handleId = ""; - if (isImage) { + if (handleType === "image") { handleId = imageCount === 0 ? "image" : `image-${imageCount}`; imageCount++; + } else if (handleType === "audio") { + handleId = audioCount === 0 ? "audio" : `audio-${audioCount}`; + audioCount++; } else { handleId = textCount === 0 ? "text" : `text-${textCount}`; textCount++; } ... style={{ - background: handleType === "image" ? "rgb(34, 197, 94)" : "rgb(251, 191, 36)", + background: + handleType === "image" + ? "rgb(34, 197, 94)" + : handleType === "audio" + ? "rgb(167, 139, 250)" + : "rgb(251, 191, 36)", top: `${50 + (index - nodeData.inputSchema!.length / 2 + 0.5) * 20}px`, }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/nodes/GenerateAudioNode.tsx` around lines 213 - 240, The current logic in GenerateAudioNode.tsx sets handleType = isImage ? "image" : "text", which collapses any input.type === "audio" into the "text" family and breaks audio handle validation; update the mapping so audio is preserved as its own family (e.g., set handleType = input.type when input.type is "image"|"audio" otherwise "text"), add a separate audioCount and audio handleId generation (similar to textCount/imageCount) so handles get ids like "audio" or "audio-N", and ensure the generated data-handletype and id values (handleType and handleId) match the checks in WorkflowCanvas.tsx that look for "audio" or ids starting with "audio".
🧹 Nitpick comments (1)
src/components/nodes/GenerateVideoNode.tsx (1)
32-52: Share Veo detection instead of re-deriving it in the node.This helper is already broader than the Kie-side Veo classifier / image-param mapping, so the UI schema can drift from the request builder again. Moving the Veo classifier and image-param-name selection into shared code would keep the rendered handles and backend routing in lockstep.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/nodes/GenerateVideoNode.tsx` around lines 32 - 52, Extract the Veo detection and image-param mapping out of GenerateVideoNode.tsx into a shared utility and use it from buildVeoInputSchema instead of re-deriving; specifically move the logic in isVeoModel and the imageParamName selection (the id checks for "veo-", "veo3/", "/veo" and the image-param selection using id.startsWith("veo3") and the isI2V checks) into a single exported helper (e.g., getVeoMetadata or isVeoModel + getVeoImageParamName) and import that helper into GenerateVideoNode.tsx so buildVeoInputSchema calls the shared function to determine Veo status and image parameter name, keeping UI schema and backend routing consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/nodes/GenerateVideoNode.tsx`:
- Around line 296-299: The effect currently only updates React Flow internals on
inputSchema changes, but handle positions also shift when
handleParametersExpandChange() toggles parameters and when the preview
auto-resizes the node; mirror the pattern used in RouterNode by calling
updateNodeInternals(id) immediately after any setNodes(...) that changes node
height. Concretely, locate the handlers handleParametersExpandChange and the
preview auto-resize callback and add a call to updateNodeInternals(id) right
after the setNodes(...) call that modifies this node (or ensure those handlers
trigger a setNodes + updateNodeInternals pair), so React Flow's hitboxes are
recomputed whenever the node size changes.
---
Outside diff comments:
In `@src/components/nodes/GenerateAudioNode.tsx`:
- Around line 213-240: The current logic in GenerateAudioNode.tsx sets
handleType = isImage ? "image" : "text", which collapses any input.type ===
"audio" into the "text" family and breaks audio handle validation; update the
mapping so audio is preserved as its own family (e.g., set handleType =
input.type when input.type is "image"|"audio" otherwise "text"), add a separate
audioCount and audio handleId generation (similar to textCount/imageCount) so
handles get ids like "audio" or "audio-N", and ensure the generated
data-handletype and id values (handleType and handleId) match the checks in
WorkflowCanvas.tsx that look for "audio" or ids starting with "audio".
---
Nitpick comments:
In `@src/components/nodes/GenerateVideoNode.tsx`:
- Around line 32-52: Extract the Veo detection and image-param mapping out of
GenerateVideoNode.tsx into a shared utility and use it from buildVeoInputSchema
instead of re-deriving; specifically move the logic in isVeoModel and the
imageParamName selection (the id checks for "veo-", "veo3/", "/veo" and the
image-param selection using id.startsWith("veo3") and the isI2V checks) into a
single exported helper (e.g., getVeoMetadata or isVeoModel +
getVeoImageParamName) and import that helper into GenerateVideoNode.tsx so
buildVeoInputSchema calls the shared function to determine Veo status and image
parameter name, keeping UI schema and backend routing consistent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 649f4b70-39ea-4523-8153-a8e45917d7ea
📒 Files selected for processing (2)
src/components/nodes/GenerateAudioNode.tsxsrc/components/nodes/GenerateVideoNode.tsx
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/nodes/GenerateAudioNode.tsx (1)
210-248:⚠️ Potential issue | 🟠 MajorAdd React Flow internals refresh when the input schema changes, matching the pattern in
GenerateVideoNode.This node dynamically generates handles based on
nodeData.inputSchemawith calculated positions (top: ${50 + (index - nodeData.inputSchema!.length / 2 + 0.5) * 20}px), but it never triggersuseUpdateNodeInternals()when the schema changes. Without this, React Flow's handle hitboxes remain stale after schema updates, causing connections to snap to incorrect coordinates—the same issue this PR fixed inGenerateVideoNode.Add
useUpdateNodeInternalsto the imports and auseEffectthat depends onnodeData.inputSchema:const updateNodeInternals = useUpdateNodeInternals(); useEffect(() => { updateNodeInternals(id); }, [id, nodeData.inputSchema, updateNodeInternals]);Also applies to: 397-414
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/nodes/GenerateAudioNode.tsx` around lines 210 - 248, The GenerateAudioNode builds dynamic handles from nodeData.inputSchema but doesn't refresh React Flow internals, so add useUpdateNodeInternals to the imports and create an updateNodeInternals effect: call const updateNodeInternals = useUpdateNodeInternals() and add a useEffect that calls updateNodeInternals(id) whenever id, nodeData.inputSchema, or updateNodeInternals change; ensure the symbol names match exactly (GenerateAudioNode, nodeData.inputSchema, updateNodeInternals, id) so handle hitboxes are recalculated after schema updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/nodes/GenerateVideoNode.tsx`:
- Around line 503-519: The mounted schema-backed handles in
GenerateVideoNode.tsx use descriptive IDs ("image", "text", "audio") while
WorkflowCanvas.tsx is still probing for "image-0"/"text-0"/"audio-0", causing
auto-created edges to target non-existent handles; update WorkflowCanvas.tsx
(the auto-connection probe logic around the image/text/audio checks) to look for
the descriptive IDs ("image", "text", "audio") (or accept both forms during
migration) instead of "image-0"/"text-0"/"audio-0" so the canvas auto-connection
matches the handle IDs created by GenerateVideoNode.tsx. Ensure the probe logic
references the exact handle IDs "image"/"text"/"audio" (and remove or
de-prioritize the old "-0" variants).
In `@src/types/nodes.ts`:
- Around line 155-157: ModelInputDef now includes "audio" but
getConnectedInputs() still only maps image/text; update getConnectedInputs() to
wire audio edges into handleToSchemaName/dynamicInputs like the others by
detecting audio node outputs (use audioInput.data.audioFile and
generateAudio.data.outputAudio as the source fields) and add the corresponding
handle name to the handleToSchemaName entries so schema-backed audio inputs
receive the connected file.
---
Outside diff comments:
In `@src/components/nodes/GenerateAudioNode.tsx`:
- Around line 210-248: The GenerateAudioNode builds dynamic handles from
nodeData.inputSchema but doesn't refresh React Flow internals, so add
useUpdateNodeInternals to the imports and create an updateNodeInternals effect:
call const updateNodeInternals = useUpdateNodeInternals() and add a useEffect
that calls updateNodeInternals(id) whenever id, nodeData.inputSchema, or
updateNodeInternals change; ensure the symbol names match exactly
(GenerateAudioNode, nodeData.inputSchema, updateNodeInternals, id) so handle
hitboxes are recalculated after schema updates.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d6c906b9-f439-45c8-97a9-c1507299e279
📒 Files selected for processing (4)
src/components/nodes/GenerateAudioNode.tsxsrc/components/nodes/GenerateVideoNode.tsxsrc/types/nodes.tssrc/utils/veoUtils.ts
…schema resolution
Google Veo Nodes Handle Connection Issue - Status Report
What was the problem: Users were unable to connect Image or Prompt nodes to the appropriate input handles on Google Veo generation nodes (e.g., Veo 3.1 I2V, Veo 3.1 Fast). When dragging a connection cable, it would either refuse to snap to the handle, or aggressively snap to incorrect, empty coordinates hovering inside the node.
Why did it happen (The Root Causes): The issue was actually a combination of three overlapping layout and state-management problems within the React Flow implementation:
Stale Node Internals (useUpdateNodeInternals omission): When a user selected a specific Veo model from the dropdown, the node dynamically updated its input schema to generate new handles. However, React Flow caches the invisible physical "hitboxes" used for cable snapping when a node is first mounted. Because the useUpdateNodeInternals hook was never called during the schema swap, the visual handles updated, but the magnetic hitboxes remained stuck at the default 35% and 65% positions. The cables were trying to connect to "ghost" handles that no longer existed visually.
Coordinate Misalignment (The Padding Offset): The physical rendering of the handles was misaligned. The Handle components were being rendered inside a container that had internal padding/margins (from the node header). As a result, calculating top: 20% placed the handle at 20% of the inner content area, while React Flow's connection engine calculated 20% of the entire absolute node wrapper. This created a permanent visual offset between the dot and its hitbox.
Invisible Legacy Handle Collisions: To maintain backward compatibility with older saved projects, the code was actively generating duplicate, invisible handles (opacity: 0, isConnectable={false}) with standard IDs alongside the new dynamic handles. React Flow's connection engine was incorrectly prioritizing these invisible blocked handles, intercepting the user's mouse and killing the connection attempt.
see the problem

What was done (The Applied Fixes):
see now

premise, I'm not an experienced developer, I'm getting help from antigravity...
Summary by CodeRabbit
New Features
Improvements
Chores