@ -14,6 +14,7 @@ import { getImageDimensions, calculateNodeSizePreservingHeight } from "@/utils/n
import { ProviderBadge } from "./ProviderBadge" ;
import { useInlineParameters } from "@/hooks/useInlineParameters" ;
import { InlineParameterPanel } from "./InlineParameterPanel" ;
import { SettingsTabBar } from "./SettingsTabBar" ;
import { browseRegistry } from "@/utils/browseRegistry" ;
import { useAdaptiveImageSrc } from "@/hooks/useAdaptiveImageSrc" ;
@ -65,6 +66,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
const [ isLoadingModels , setIsLoadingModels ] = useState ( false ) ;
const [ modelsFetchError , setModelsFetchError ] = useState < string | null > ( null ) ;
const [ isBrowseDialogOpen , setIsBrowseDialogOpen ] = useState ( false ) ;
const [ settingsTab , setSettingsTab ] = useState < "primary" | "fallback" > ( "primary" ) ;
// Inline parameters infrastructure
const { inlineParametersEnabled } = useInlineParameters ( ) ;
@ -513,112 +515,137 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
onToggle = { handleToggleParams }
nodeId = { id }
>
{ /* Gemini-specific controls */ }
{ isGeminiProvider && currentModelId && ( ( ) = > {
const controls : React.ReactNode [ ] = [
< div key = "model" className = "flex items-center gap-2" >
< label className = "text-[11px] text-neutral-400 shrink-0" > Model < / label >
< select
value = { currentModelId }
onChange = { handleModelChange }
data - tutorial = "generate-model-selector"
className = "nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{ GEMINI_IMAGE_MODELS . map ( ( m ) = > (
< option key = { m . value } value = { m . value } >
{ m . label }
< / option >
) ) }
< / select >
< / div > ,
< div key = "aspect-ratio" className = "flex items-center gap-2" >
< label className = "text-[11px] text-neutral-400 shrink-0" > Aspect Ratio < / label >
< select
value = { nodeData . aspectRatio || "1:1" }
onChange = { handleAspectRatioChange }
className = "nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{ aspectRatios . map ( ( ratio ) = > (
< option key = { ratio } value = { ratio } >
{ ratio }
< / option >
) ) }
< / select >
< / div > ,
] ;
if ( supportsResolution ) {
controls . push (
< div key = "resolution" className = "flex items-center gap-2" >
< label className = "text-[11px] text-neutral-400 shrink-0" > Resolution < / label >
< select
value = { nodeData . resolution || "2K" }
onChange = { handleResolutionChange }
className = "nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{ resolutions . map ( ( res ) = > (
< option key = { res } value = { res } >
{ res }
< / option >
) ) }
< / select >
< / div >
) ;
}
if ( currentModelId === "nano-banana-pro" || currentModelId === "nano-banana-2" ) {
controls . push (
< label key = "google-search" className = "flex items-center gap-1.5 text-[11px] text-neutral-300 cursor-pointer" >
< input
type = "checkbox"
checked = { nodeData . useGoogleSearch || false }
onChange = { handleGoogleSearchToggle }
className = "nodrag nopan w-3 h-3 rounded bg-[#1a1a1a] text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/ >
Google Search
< / label >
) ;
}
if ( currentModelId === "nano-banana-2" ) {
controls . push (
< label key = "image-search" className = "flex items-center gap-1.5 text-[11px] text-neutral-300 cursor-pointer" >
< input
type = "checkbox"
checked = { nodeData . useImageSearch || false }
onChange = { handleImageSearchToggle }
className = "nodrag nopan w-3 h-3 rounded bg-[#1a1a1a] text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/ >
Image Search
< / label >
) ;
}
{ /* Tab bar for primary/fallback settings */ }
{ nodeData . fallbackModel && (
< SettingsTabBar
activeTab = { settingsTab }
onTabChange = { setSettingsTab }
primaryLabel = { nodeData . selectedModel ? . displayName || "Primary" }
fallbackLabel = { nodeData . fallbackModel . displayName }
/ >
) }
const display = useGeminiGrid && geminiColCount > 1
? reorderColumnFirst ( controls , geminiColCount )
: controls ;
{ /* Primary tab content */ }
{ settingsTab === "primary" && (
< >
{ /* Gemini-specific controls */ }
{ isGeminiProvider && currentModelId && ( ( ) = > {
const controls : React.ReactNode [ ] = [
< div key = "model" className = "flex items-center gap-2" >
< label className = "text-[11px] text-neutral-400 shrink-0" > Model < / label >
< select
value = { currentModelId }
onChange = { handleModelChange }
data - tutorial = "generate-model-selector"
className = "nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{ GEMINI_IMAGE_MODELS . map ( ( m ) = > (
< option key = { m . value } value = { m . value } >
{ m . label }
< / option >
) ) }
< / select >
< / div > ,
< div key = "aspect-ratio" className = "flex items-center gap-2" >
< label className = "text-[11px] text-neutral-400 shrink-0" > Aspect Ratio < / label >
< select
value = { nodeData . aspectRatio || "1:1" }
onChange = { handleAspectRatioChange }
className = "nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{ aspectRatios . map ( ( ratio ) = > (
< option key = { ratio } value = { ratio } >
{ ratio }
< / option >
) ) }
< / select >
< / div > ,
] ;
if ( supportsResolution ) {
controls . push (
< div key = "resolution" className = "flex items-center gap-2" >
< label className = "text-[11px] text-neutral-400 shrink-0" > Resolution < / label >
< select
value = { nodeData . resolution || "2K" }
onChange = { handleResolutionChange }
className = "nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{ resolutions . map ( ( res ) = > (
< option key = { res } value = { res } >
{ res }
< / option >
) ) }
< / select >
< / div >
) ;
}
return (
< div
ref = { geminiGridRef }
className = { useGeminiGrid
? "grid grid-cols-[repeat(auto-fill,minmax(min(180px,100%),1fr))] max-w-[420px] gap-x-6 gap-y-1.5"
: "space-y-1.5 max-w-[280px]"
if ( currentModelId === "nano-banana-pro" || currentModelId === "nano-banana-2" ) {
controls . push (
< label key = "google-search" className = "flex items-center gap-1.5 text-[11px] text-neutral-300 cursor-pointer" >
< input
type = "checkbox"
checked = { nodeData . useGoogleSearch || false }
onChange = { handleGoogleSearchToggle }
className = "nodrag nopan w-3 h-3 rounded bg-[#1a1a1a] text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/ >
Google Search
< / label >
) ;
}
>
{ display }
< / div >
) ;
} ) ( ) }
{ /* External provider parameters - reuse ModelParameters component */ }
{ ! isGeminiProvider && nodeData . selectedModel ? . modelId && (
if ( currentModelId === "nano-banana-2" ) {
controls . push (
< label key = "image-search" className = "flex items-center gap-1.5 text-[11px] text-neutral-300 cursor-pointer" >
< input
type = "checkbox"
checked = { nodeData . useImageSearch || false }
onChange = { handleImageSearchToggle }
className = "nodrag nopan w-3 h-3 rounded bg-[#1a1a1a] text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/ >
Image Search
< / label >
) ;
}
const display = useGeminiGrid && geminiColCount > 1
? reorderColumnFirst ( controls , geminiColCount )
: controls ;
return (
< div
ref = { geminiGridRef }
className = { useGeminiGrid
? "grid grid-cols-[repeat(auto-fill,minmax(min(180px,100%),1fr))] max-w-[420px] gap-x-6 gap-y-1.5"
: "space-y-1.5 max-w-[280px]"
}
>
{ display }
< / div >
) ;
} ) ( ) }
{ /* External provider parameters - reuse ModelParameters component */ }
{ ! isGeminiProvider && nodeData . selectedModel ? . modelId && (
< ModelParameters
modelId = { nodeData . selectedModel . modelId }
provider = { currentProvider }
parameters = { nodeData . parameters || { } }
onParametersChange = { handleParametersChange }
onInputsLoaded = { handleInputsLoaded }
/ >
) }
< / >
) }
{ /* Fallback tab content */ }
{ settingsTab === "fallback" && nodeData . fallbackModel && (
< ModelParameters
modelId = { nodeData . selectedModel . modelId }
provider = { currentProvider }
parameters = { nodeData . parameters || { } }
onParametersChange = { handleParametersChange }
onInputsLoaded = { handleInputsLoaded }
modelId = { nodeData . fallbackModel . modelId }
provider = { nodeData . fallbackModel . provider }
parameters = { nodeData . fallbackParameters || { } }
onParametersChange = { ( p ) = > updateNodeData ( id , { fallbackParameters : p } ) }
/ >
) }
< / InlineParameterPanel >