diff --git a/src/components/nodes/NanoBananaNode.tsx b/src/components/nodes/GenerateImageNode.tsx similarity index 63% rename from src/components/nodes/NanoBananaNode.tsx rename to src/components/nodes/GenerateImageNode.tsx index d3f4eb73..5df8c045 100644 --- a/src/components/nodes/NanoBananaNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -1,10 +1,11 @@ "use client"; -import { useCallback, useState } from "react"; +import { useCallback, useState, useEffect, useMemo } from "react"; import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { useWorkflowStore, saveNanoBananaDefaults } from "@/store/workflowStore"; -import { NanoBananaNodeData, AspectRatio, Resolution, ModelType } from "@/types"; +import { NanoBananaNodeData, AspectRatio, Resolution, ModelType, ProviderType, SelectedModel } from "@/types"; +import { ProviderModel, ModelCapability } from "@/lib/providers/types"; // All 10 aspect ratios supported by both models const ASPECT_RATIOS: AspectRatio[] = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"]; @@ -12,18 +13,115 @@ const ASPECT_RATIOS: AspectRatio[] = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", // Resolutions only for Nano Banana Pro (gemini-3-pro-image-preview) const RESOLUTIONS: Resolution[] = ["1K", "2K", "4K"]; -const MODELS: { value: ModelType; label: string }[] = [ +// Hardcoded Gemini image models (always available) +const GEMINI_IMAGE_MODELS: { value: ModelType; label: string }[] = [ { value: "nano-banana", label: "Nano Banana" }, { value: "nano-banana-pro", label: "Nano Banana Pro" }, ]; +// Image generation capabilities +const IMAGE_CAPABILITIES: ModelCapability[] = ["text-to-image", "image-to-image"]; + type NanoBananaNodeType = Node; -export function NanoBananaNode({ id, data, selected }: NodeProps) { +export function GenerateImageNode({ id, data, selected }: NodeProps) { const nodeData = data; const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const generationsPath = useWorkflowStore((state) => state.generationsPath); + const providerSettings = useWorkflowStore((state) => state.providerSettings); const [isLoadingCarouselImage, setIsLoadingCarouselImage] = useState(false); + const [externalModels, setExternalModels] = useState([]); + const [isLoadingModels, setIsLoadingModels] = useState(false); + + // Get the current selected provider (default to gemini) + const currentProvider: ProviderType = nodeData.selectedModel?.provider || "gemini"; + + // Get enabled providers + const enabledProviders = useMemo(() => { + const providers: { id: ProviderType; name: string }[] = []; + // Gemini is always available + providers.push({ id: "gemini", name: "Gemini" }); + // Add other enabled providers + if (providerSettings.providers.replicate?.enabled && providerSettings.providers.replicate?.apiKey) { + providers.push({ id: "replicate", name: "Replicate" }); + } + if (providerSettings.providers.fal?.enabled && providerSettings.providers.fal?.apiKey) { + providers.push({ id: "fal", name: "fal.ai" }); + } + return providers; + }, [providerSettings]); + + // Fetch models from external providers when provider changes + useEffect(() => { + const fetchModels = async () => { + if (currentProvider === "gemini") { + setExternalModels([]); + return; + } + + setIsLoadingModels(true); + try { + const capabilities = IMAGE_CAPABILITIES.join(","); + const response = await fetch(`/api/models?provider=${currentProvider}&capabilities=${capabilities}`); + if (response.ok) { + const models = await response.json(); + setExternalModels(models); + } else { + setExternalModels([]); + } + } catch (error) { + console.error("Failed to fetch models:", error); + setExternalModels([]); + } finally { + setIsLoadingModels(false); + } + }; + + fetchModels(); + }, [currentProvider]); + + // Handle provider change + const handleProviderChange = useCallback( + (e: React.ChangeEvent) => { + const provider = e.target.value as ProviderType; + + if (provider === "gemini") { + // Reset to Gemini default + const newSelectedModel: SelectedModel = { + provider: "gemini", + modelId: nodeData.model || "nano-banana-pro", + displayName: GEMINI_IMAGE_MODELS.find(m => m.value === (nodeData.model || "nano-banana-pro"))?.label || "Nano Banana Pro", + }; + updateNodeData(id, { selectedModel: newSelectedModel }); + } else { + // Set placeholder for external provider + const newSelectedModel: SelectedModel = { + provider, + modelId: "", + displayName: "Select model...", + }; + updateNodeData(id, { selectedModel: newSelectedModel }); + } + }, + [id, nodeData.model, updateNodeData] + ); + + // Handle model change for external providers + const handleExternalModelChange = useCallback( + (e: React.ChangeEvent) => { + const modelId = e.target.value; + const model = externalModels.find(m => m.id === modelId); + if (model) { + const newSelectedModel: SelectedModel = { + provider: currentProvider, + modelId: model.id, + displayName: model.name, + }; + updateNodeData(id, { selectedModel: newSelectedModel }); + } + }, + [id, currentProvider, externalModels, updateNodeData] + ); const handleAspectRatioChange = useCallback( (e: React.ChangeEvent) => { @@ -48,6 +146,14 @@ export function NanoBananaNode({ id, data, selected }: NodeProps m.value === model)?.label || model, + }; + updateNodeData(id, { selectedModel: newSelectedModel }); }, [id, updateNodeData] ); @@ -141,7 +247,8 @@ export function NanoBananaNode({ id, data, selected }: NodeProps 1; return ( @@ -316,46 +423,87 @@ export function NanoBananaNode({ id, data, selected }: NodeProps )} + {/* Provider selector - only show if multiple providers are enabled */} + {enabledProviders.length > 1 && ( + + )} + {/* Model selector */} - - - {/* Aspect ratio and resolution row */} -
+ {isGeminiProvider ? ( - {isNanoBananaPro && ( + ) : ( + + )} + + {/* Aspect ratio and resolution row - only for Gemini */} + {isGeminiProvider && ( +
- )} -
+ {isNanoBananaPro && ( + + )} +
+ )} {/* Google Search toggle - only for Nano Banana Pro */} {isNanoBananaPro && ( @@ -373,3 +521,6 @@ export function NanoBananaNode({ id, data, selected }: NodeProps ); } + +// Backward-compatible alias +export { GenerateImageNode as NanoBananaNode }; diff --git a/src/components/nodes/index.ts b/src/components/nodes/index.ts index 5a3a2b05..f9c1dad2 100644 --- a/src/components/nodes/index.ts +++ b/src/components/nodes/index.ts @@ -1,7 +1,7 @@ export { ImageInputNode } from "./ImageInputNode"; export { AnnotationNode } from "./AnnotationNode"; export { PromptNode } from "./PromptNode"; -export { NanoBananaNode } from "./NanoBananaNode"; +export { GenerateImageNode, NanoBananaNode } from "./GenerateImageNode"; export { LLMGenerateNode } from "./LLMGenerateNode"; export { SplitGridNode } from "./SplitGridNode"; export { OutputNode } from "./OutputNode";