diff --git a/.env.example b/.env.example index 8ceace67..4c0f8a3b 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,3 @@ REPLICATE_API_KEY=your_replicate_api_key_here # Get your API key from: https://fal.ai/dashboard/keys FAL_API_KEY=your_fal_api_key_here -# Community Workflows API URL (Optional - defaults to node-banana-pro hosted service) -# Set this if you're self-hosting the community workflows -COMMUNITY_WORKFLOWS_API_URL=https://your-node-banana-pro-instance.com/api/public/community-workflows diff --git a/src/app/api/community-workflows/[id]/route.ts b/src/app/api/community-workflows/[id]/route.ts index 91f2901b..dce25974 100644 --- a/src/app/api/community-workflows/[id]/route.ts +++ b/src/app/api/community-workflows/[id]/route.ts @@ -1,8 +1,6 @@ import { NextResponse } from "next/server"; -// Default to node-banana-pro hosted service const COMMUNITY_WORKFLOWS_API_URL = - process.env.COMMUNITY_WORKFLOWS_API_URL || "https://nodebananapro.com/api/public/community-workflows"; // Allowed hostnames for presigned URL fetches (SSRF protection) diff --git a/src/app/api/community-workflows/route.ts b/src/app/api/community-workflows/route.ts index 2a62fc18..ee954f48 100644 --- a/src/app/api/community-workflows/route.ts +++ b/src/app/api/community-workflows/route.ts @@ -1,8 +1,6 @@ import { NextResponse } from "next/server"; -// Default to node-banana-pro hosted service const COMMUNITY_WORKFLOWS_API_URL = - process.env.COMMUNITY_WORKFLOWS_API_URL || "https://nodebananapro.com/api/public/community-workflows"; /** 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/__tests__/PromptNode.test.tsx b/src/components/__tests__/PromptNode.test.tsx index cf180c12..a3b3702d 100644 --- a/src/components/__tests__/PromptNode.test.tsx +++ b/src/components/__tests__/PromptNode.test.tsx @@ -80,7 +80,7 @@ describe("PromptNode", () => { expect(textarea).toBeInTheDocument(); }); - it("should call updateNodeData when typing in textarea", () => { + it("should call updateNodeData when typing in textarea and blurring", () => { render( @@ -88,7 +88,9 @@ describe("PromptNode", () => { ); const textarea = screen.getByPlaceholderText("Describe what to generate..."); + fireEvent.focus(textarea); fireEvent.change(textarea, { target: { value: "New prompt text" } }); + fireEvent.blur(textarea); expect(mockUpdateNodeData).toHaveBeenCalledWith("test-prompt-1", { prompt: "New prompt text", 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} >