Browse Source

Merge pull request #37 from shrimbly/fix/phase-30-small-fixes

fix: phase 30 small fixes
handoff-20260429-1057
Willie 6 months ago
committed by GitHub
parent
commit
e19705e9c7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      .env.example
  2. 2
      src/app/api/community-workflows/[id]/route.ts
  3. 2
      src/app/api/community-workflows/route.ts
  4. 6
      src/components/WorkflowCanvas.tsx
  5. 4
      src/components/__tests__/PromptNode.test.tsx
  6. 32
      src/components/nodes/PromptNode.tsx
  7. 17
      src/store/workflowStore.ts

3
.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

2
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)

2
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";
/**

6
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;
};

4
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(
<TestWrapper>
<PromptNode {...defaultProps} />
@ -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",

32
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<PromptNodeType>) {
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<HTMLTextAreaElement>) => {
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<PromptNodeType>) {
commentNavigation={commentNavigation ?? undefined}
>
<textarea
value={nodeData.prompt}
value={localPrompt}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="Describe what to generate..."
className="nodrag nopan nowheel w-full flex-1 min-h-[70px] p-2 text-xs leading-relaxed text-neutral-100 border border-neutral-700 rounded bg-neutral-900/50 resize-none focus:outline-none focus:ring-1 focus:ring-neutral-600 focus:border-neutral-600 placeholder:text-neutral-500"
/>

17
src/store/workflowStore.ts

@ -819,7 +819,8 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
.filter((n) => n.type === "nanoBanana")
.forEach((node) => {
const textConnected = edges.some(
(e) => e.target === node.id && e.targetHandle === "text"
(e) => e.target === node.id &&
(e.targetHandle === "text" || e.targetHandle?.startsWith("text-"))
);
if (!textConnected) {
@ -827,6 +828,20 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
}
});
// Check generateVideo nodes have required text input
nodes
.filter((n) => n.type === "generateVideo")
.forEach((node) => {
const textConnected = edges.some(
(e) => e.target === node.id &&
(e.targetHandle === "text" || e.targetHandle?.startsWith("text-"))
);
if (!textConnected) {
errors.push(`Video node "${node.id}" missing text input`);
}
});
// Check annotation nodes have image input (either connected or manually loaded)
nodes
.filter((n) => n.type === "annotation")

Loading…
Cancel
Save