From 9bcf6bfbd906d3abe36a4f0cf73dcdcf1ac94d0f Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 23 Jan 2026 14:14:39 +1300 Subject: [PATCH 1/3] fix: handle indexed handles and cursor jumping in prompt nodes - Update getHandleType() to recognize indexed handles (text-0, image-0) used by external providers like Replicate and fal.ai - Fix validateWorkflow() to accept indexed text handles for nanoBanana nodes - Add validation for generateVideo nodes requiring text input - Use local state pattern in PromptNode to prevent cursor jumping during typing Co-Authored-By: Claude Opus 4.5 --- src/components/WorkflowCanvas.tsx | 6 +++--- src/components/nodes/PromptNode.tsx | 32 +++++++++++++++++++++++++---- src/store/workflowStore.ts | 17 ++++++++++++++- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 85ec1458..35b6f223 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -65,10 +65,10 @@ const getHandleType = (handleId: string | null | undefined): "image" | "text" | // Standard handles if (handleId === "video") return "video"; if (handleId === "image" || handleId === "text") return handleId; - // Dynamic handles - check naming patterns + // Dynamic handles - check naming patterns (including indexed: text-0, image-0) if (handleId.includes("video")) return "video"; - if (handleId.includes("image") || handleId.includes("frame")) return "image"; - if (handleId === "prompt" || handleId === "negative_prompt" || handleId.includes("prompt")) return "text"; + if (handleId.startsWith("image-") || handleId.includes("image") || handleId.includes("frame")) return "image"; + if (handleId.startsWith("text-") || handleId === "prompt" || handleId === "negative_prompt" || handleId.includes("prompt")) return "text"; return null; }; diff --git a/src/components/nodes/PromptNode.tsx b/src/components/nodes/PromptNode.tsx index c4f8a1b3..18dd25e8 100644 --- a/src/components/nodes/PromptNode.tsx +++ b/src/components/nodes/PromptNode.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useState } from "react"; +import { useCallback, useState, useEffect } from "react"; import { createPortal } from "react-dom"; import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; @@ -19,13 +19,35 @@ export function PromptNode({ id, data, selected }: NodeProps) { const decrementModalCount = useWorkflowStore((state) => state.decrementModalCount); const [isModalOpenLocal, setIsModalOpenLocal] = useState(false); + // Local state for prompt to prevent cursor jumping during typing + const [localPrompt, setLocalPrompt] = useState(nodeData.prompt); + const [isEditing, setIsEditing] = useState(false); + + // Sync from props when not actively editing + useEffect(() => { + if (!isEditing) { + setLocalPrompt(nodeData.prompt); + } + }, [nodeData.prompt, isEditing]); + const handleChange = useCallback( (e: React.ChangeEvent) => { - updateNodeData(id, { prompt: e.target.value }); + setLocalPrompt(e.target.value); }, - [id, updateNodeData] + [] ); + const handleFocus = useCallback(() => { + setIsEditing(true); + }, []); + + const handleBlur = useCallback(() => { + setIsEditing(false); + if (localPrompt !== nodeData.prompt) { + updateNodeData(id, { prompt: localPrompt }); + } + }, [id, localPrompt, nodeData.prompt, updateNodeData]); + const handleOpenModal = useCallback(() => { setIsModalOpenLocal(true); incrementModalCount(); @@ -57,8 +79,10 @@ export function PromptNode({ id, data, selected }: NodeProps) { commentNavigation={commentNavigation ?? undefined} >