@ -486,6 +486,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
selected = { selected }
isExecuting = { isRunning }
hasError = { nodeData . status === "error" }
fullBleed
>
{ /* Input handles - ALWAYS use same IDs and positions for connection stability */ }
{ /* Image input at 35%, Text input at 65% - never changes regardless of model */ }
@ -547,113 +548,112 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
Image
< / div >
< div className = "flex-1 flex flex-col min-h-0 gap-2 " >
< div className = "relative w-full h-full min-h-0 " >
{ /* Preview area */ }
{ nodeData . outputImage ? (
< >
< div className = "relative w-full flex-1 min-h-0" >
< img
src = { nodeData . outputImage }
alt = "Generated"
className = "w-full h-full object-contain rounded"
/ >
{ /* Loading overlay for generation */ }
{ nodeData . status === "loading" && (
< div className = "absolute inset-0 bg-neutral-900/70 rounded flex items-center justify-center" >
< svg
className = "w-6 h-6 animate-spin text-white"
fill = "none"
viewBox = "0 0 24 24"
>
< circle
className = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
strokeWidth = "3"
/ >
< path
className = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/ >
< / svg >
< / div >
) }
{ /* Error overlay when generation failed */ }
{ nodeData . status === "error" && (
< div className = "absolute inset-0 bg-red-900/40 rounded flex flex-col items-center justify-center gap-1" >
< svg
className = "w-6 h-6 text-white"
fill = "none"
viewBox = "0 0 24 24"
< img
src = { nodeData . outputImage }
alt = "Generated"
className = "w-full h-full object-cover"
/ >
{ /* Loading overlay for generation */ }
{ nodeData . status === "loading" && (
< div className = "absolute inset-0 bg-neutral-900/70 flex items-center justify-center" >
< svg
className = "w-6 h-6 animate-spin text-white"
fill = "none"
viewBox = "0 0 24 24"
>
< circle
className = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
strokeWidth = { 2 }
>
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" / >
< / svg >
< span className = "text-white text-xs font-medium" > Generation failed < / span >
< span className = "text-white/70 text-[10px]" > See toast for details < / span >
< / div >
) }
{ /* Loading overlay for carousel navigation */ }
{ isLoadingCarouselImage && (
< div className = "absolute inset-0 bg-neutral-900/50 rounded flex items-center justify-center" >
< svg
className = "w-4 h-4 animate-spin text-white"
fill = "none"
viewBox = "0 0 24 24"
>
< circle
className = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
strokeWidth = "3"
/ >
< path
className = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/ >
< / svg >
< / div >
) }
< div className = "absolute top-1 right-1" >
< button
onClick = { handleClearImage }
className = "w-5 h-5 bg-neutral-900/80 hover:bg-red-600/80 rounded flex items-center justify-center text-neutral-400 hover:text-white transition-colors"
title = "Clear image"
strokeWidth = "3"
/ >
< path
className = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/ >
< / svg >
< / div >
) }
{ /* Error overlay when generation failed */ }
{ nodeData . status === "error" && (
< div className = "absolute inset-0 bg-red-900/40 flex flex-col items-center justify-center gap-1" >
< svg
className = "w-6 h-6 text-white"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
strokeWidth = { 2 }
>
< svg className = "w-3 h-3" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M6 18L18 6M6 6l12 12" / >
< / svg >
< / button >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" / >
< / svg >
< span className = "text-white text-xs font-medium" > Generation failed < / span >
< span className = "text-white/70 text-[10px]" > See toast for details < / span >
< / div >
) }
{ /* Loading overlay for carousel navigation */ }
{ isLoadingCarouselImage && (
< div className = "absolute inset-0 bg-neutral-900/50 flex items-center justify-center" >
< svg
className = "w-4 h-4 animate-spin text-white"
fill = "none"
viewBox = "0 0 24 24"
>
< circle
className = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
strokeWidth = "3"
/ >
< path
className = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/ >
< / svg >
< / div >
) }
{ /* Clear button */ }
< div className = "absolute top-1 right-1" >
< button
onClick = { handleClearImage }
className = "w-5 h-5 bg-neutral-900/80 hover:bg-red-600/80 rounded flex items-center justify-center text-neutral-400 hover:text-white transition-colors"
title = "Clear image"
>
< svg className = "w-3 h-3" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M6 18L18 6M6 6l12 12" / >
< / svg >
< / button >
< / div >
{ /* Carousel controls - only show if there are multiple images */ }
{ /* Carousel controls - overlaid on image bottom */ }
{ hasCarouselImages && (
< div className = "flex items-center justify-center gap-2 shrink-0" >
< div className = "absolute bottom-0 left-0 right-0 flex items-center justify-center gap-2 py-1.5 bg-neutral-900/60 backdrop-blur-sm " >
< button
onClick = { handleCarouselPrevious }
disabled = { isLoadingCarouselImage }
className = "w-5 h-5 rounded bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center text-neutral-400 hover:text-white transition-colors"
className = "w-5 h-5 rounded hover:bg-white/10 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center text-white/7 0 hover:text-white transition-colors"
title = "Previous image"
>
< svg className = "w-3 h-3" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M15 19l-7-7 7-7" / >
< / svg >
< / button >
< span className = "text-[10px] text-neutral-400 min-w-[32px] text-center" >
< span className = "text-[10px] text-white/7 0 min-w-[32px] text-center" >
{ ( nodeData . selectedHistoryIndex || 0 ) + 1 } / { ( nodeData . imageHistory || [ ] ) . length }
< / span >
< button
onClick = { handleCarouselNext }
disabled = { isLoadingCarouselImage }
className = "w-5 h-5 rounded bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center text-neutral-400 hover:text-white transition-colors"
className = "w-5 h-5 rounded hover:bg-white/10 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center text-white/7 0 hover:text-white transition-colors"
title = "Next image"
>
< svg className = "w-3 h-3" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
@ -664,7 +664,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
) }
< / >
) : (
< div className = "w-full flex-1 min-h-[112px] border border-dashed border-neutral-600 rounded flex flex-col items-center justify-center" >
< div className = "w-full h-full min-h-[112px] bg-neutral-900/40 flex flex-col items-center justify-center" >
{ nodeData . status === "loading" ? (
< svg
className = "w-4 h-4 animate-spin text-neutral-400"