nano banana

import React, { useState, useEffect, useCallback, useRef } from ‘react’; import { useDropzone } from ‘react-dropzone’; import { openDB, IDBPDatabase } from ‘idb’;// — TYPE DEFINITIONS — type Mode = ‘Image’ | ‘Video’; type AspectRatio = ‘1:1′ | ’16:9’ | ‘9:16’ | ‘4:3’ | ‘3:4’; type Creation = { id: number; mode: Mode; prompt: string; negativePrompt: string; baseImage: string | null; blendImage: string | null; aspectRatio: AspectRatio; generatedMedia: string; generatedText: string; timestamp: Date; };// — DATABASE HELPER — const DB_NAME = ‘NanoBananaDB’; const STORE_NAME = ‘creations’; const DB_VERSION = 1;async function initDB(): Promise { return openDB(DB_NAME, DB_VERSION, { upgrade(db) { if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: ‘id’, autoIncrement: true }); } }, }); }// — ICONS (as SVG components) — const BananaIcon = () => ( ); const DownloadIcon = () => ; const ReuseIcon = () => ; const GalleryIcon = () => ; const ExpandIcon = () => ; const TipsIcon = () => ; const TranslateIcon = () => ; const DeleteIcon = () => ; const ReloadIcon = () => ; const PlayIcon = () => ; const Spinner = () => ;// — MOCK API FUNCTIONS — // In a real app, these would make network requests. We simulate them with timeouts. const FAKE_API_DELAY = 2500; const FAKE_VIDEO_POLLING_INTERVAL = 3000; const FAKE_VIDEO_POLLING_STEPS = 4;const generateMediaAPI = async ( mode: Mode, prompt: string, baseImage: string | null, blendImage: string | null, aspectRatio: AspectRatio, setLoadingMessage?: (message: string) => void ): Promise<{ mediaUrl: string; text: string }> => { return new Promise((resolve) => { if (mode === ‘Video’) { let step = 0; const messages = [“Initializing video creation…”, “Querying generation cluster…”, “Polling for result…”, “Finalizing video…”]; const poll = setInterval(() => { if(setLoadingMessage) setLoadingMessage(messages[step]); step++; if (step >= FAKE_VIDEO_POLLING_STEPS) { clearInterval(poll); if(setLoadingMessage) setLoadingMessage(“Almost there…”); setTimeout(() => { resolve({ mediaUrl: ‘https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4’, text: `Generated video for: ${prompt}`, }); }, FAKE_API_DELAY / 2); } }, FAKE_VIDEO_POLLING_INTERVAL); } else { // Image setTimeout(() => { const query = encodeURIComponent(baseImage ? `edit of ${prompt}`: prompt); const [w, h] = aspectRatio.split(‘:’).map(Number); const width = w * 200; const height = h * 200; resolve({ mediaUrl: `https://placehold.co/${width}x${height}/1e293b/ffffff?text=AI+Image\\n${prompt.substring(0, 20)}…`, text: `Generated image for: ${prompt}`, }); }, FAKE_API_DELAY); } }); };const translateTextAPI = async (text: string): Promise => { return new Promise(resolve => { setTimeout(() => { resolve(`(Translated) ${text}`); }, 1000); }); };const buildPromptAPI = async (keywords: string): Promise => { return new Promise(resolve => { setTimeout(() => { resolve(`masterpiece, best quality, ${keywords}, cinematic lighting, ultra-detailed, 8k`); }, 1500); }); }// — COMPONENTS —// 1. ImageUploader Component const ImageUploader = ({ title, image, onImageChange }: { title: string, image: string | null, onImageChange: (file: string | null) => void }) => { const onDrop = useCallback((acceptedFiles: File[]) => { const file = acceptedFiles[0]; if (file) { const reader = new FileReader(); reader.onload = () => { onImageChange(reader.result as string); }; reader.readAsDataURL(file); } }, [onImageChange]);const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { ‘image/*’: [] } });return (
{image ? ( <> uploaded ) : (

{isDragActive ? ‘Drop the file here …’ : ‘Drag & drop or click to upload’}

)}
); };// 2. AspectRatioSelector Component const aspectRatios: { value: AspectRatio, icon: JSX.Element }[] = [ { value: ‘1:1’, icon:
}, { value: ’16:9′, icon:
}, { value: ‘9:16’, icon:
}, { value: ‘4:3’, icon:
}, { value: ‘3:4’, icon:
}, ];const AspectRatioSelector = ({ selected, onSelect }: { selected: AspectRatio, onSelect: (ar: AspectRatio) => void }) => { return (
{aspectRatios.map(({ value, icon }) => (
onSelect(value)} className=”flex flex-col items-center gap-2 cursor-pointer”>
{icon}
{value}
))}
); };// 3. MediaCanvas Component const MediaCanvas = ({ generatedMedia, generatedText, mode, onUseAsBase, onDownload }: { generatedMedia: string | null, generatedText: string, mode: Mode, onUseAsBase: () => void, onDownload: () => void }) => { const isVideo = mode === ‘Video’ && generatedMedia;return (
{!generatedMedia ? (

Nano Banana Studio

Your AI-powered creation space.

Describe your vision and let’s create something amazing!

) : (
{isVideo ? (
{generatedText &&

{generatedText}

}
)}
); };// 4. GalleryModal Component const GalleryModal = ({ isOpen, onClose, creations, onDelete, onReload }: { isOpen: boolean, onClose: () => void, creations: Creation[], onDelete: (id: number) => void, onReload: (creation: Creation) => void }) => { if (!isOpen) return null;return (
e.stopPropagation()}>

My Creations

{creations.length === 0 ? (
Your gallery is empty. Start creating!
) : (
{creations.sort((a, b) => b.timestamp.getTime() – a.timestamp.getTime()).map(creation => (
{creation.prompt} {creation.mode === ‘Video’ && (
)}

{creation.prompt}

))}
)}
); };// 5. PromptHelperModal Component const promptHelperContent = { en: { tabs: [‘Guide’, ‘Editing’, ‘Builder’, ‘Keywords’, ‘Negative’], guide: { title: ‘Prompting Guide’, content: “A good prompt is specific and descriptive. Think about Subject, Style, and Composition.”, example: “Example: `A majestic lion with a glowing mane, standing on a rocky cliff overlooking a stormy sea, digital painting, epic fantasy art, dramatic lighting.`” }, editing: { title: ‘Editing Guide’, content: “When editing, describe the change you want to see.”, example: “Example: `Add a futuristic city in the background`, or `Change the style to watercolor painting.`” }, builder: { title: ‘Prompt Builder’, description: ‘Describe the main elements of your image. We will generate a detailed prompt for you.’, labels: [‘Type (photo, painting, 3D render…)’, ‘Subject (a cat, a spaceship…)’, ‘Style (impressionist, cyberpunk…)’, ‘Details (wearing a hat, glowing…)’], button: ‘Build My Prompt’ }, keywords: { title: ‘Styles & Keywords’, categories: { ‘Photography’: [‘cinematic’, ‘long exposure’, ‘macro’, ‘golden hour’], ‘Art Styles’: [‘impressionism’, ‘surrealism’, ‘pop art’, ‘pixel art’], ‘Artists’: [‘van gogh style’, ‘salvador dali style’, ‘hokusai style’], ‘Lighting’: [‘dramatic lighting’, ‘soft light’, ‘neon glow’, ‘rim lighting’], ‘Effects’: [‘bokeh’, ‘lens flare’, ‘double exposure’, ‘glitch effect’], } }, negative: { title: ‘Negative Prompts’, content: “Use negative prompts to exclude things you don’t want.”, example: “Example: `ugly, blurry, deformed, watermark, text, extra fingers`” }, }, ‘pt-br’: { tabs: [‘Guia’, ‘Edição’, ‘Construtor’, ‘Palavras-chave’, ‘Negativas’], guide: { title: ‘Guia de Prompts’, content: “Um bom prompt é específico e descritivo. Pense em Sujeito, Estilo e Composição.”, example: “Exemplo: `Um leão majestoso com uma juba brilhante, em um penhasco rochoso com vista para um mar tempestuoso, pintura digital, arte de fantasia épica, iluminação dramática.`” }, editing: { title: ‘Guia de Edição’, content: “Ao editar, descreva a mudança que você quer ver.”, example: “Exemplo: `Adicione uma cidade futurista ao fundo`, ou `Mude o estilo para pintura em aquarela.`” }, builder: { title: ‘Construtor de Prompt’, description: ‘Descreva os elementos principais da sua imagem. Nós geraremos um prompt detalhado para você.’, labels: [‘Tipo (foto, pintura, render 3D…)’, ‘Sujeito (um gato, uma nave…)’, ‘Estilo (impressionista, cyberpunk…)’, ‘Detalhes (usando um chapéu, brilhando…)’], button: ‘Construir meu Prompt’ }, keywords: { title: ‘Estilos & Palavras-chave’, categories: { ‘Fotografia’: [‘cinematic’, ‘long exposure’, ‘macro’, ‘golden hour’], ‘Estilos de Arte’: [‘impressionism’, ‘surrealism’, ‘pop art’, ‘pixel art’], ‘Artistas’: [‘van gogh style’, ‘salvador dali style’, ‘hokusai style’], ‘Iluminação’: [‘dramatic lighting’, ‘soft light’, ‘neon glow’, ‘rim lighting’], ‘Efeitos’: [‘bokeh’, ‘lens flare’, ‘double exposure’, ‘glitch effect’], } }, negative: { title: ‘Prompts Negativos’, content: “Use prompts negativos para excluir coisas que você não quer.”, example: “Exemplo: `feio, borrado, deformado, marca d’água, texto, dedos extras`” }, } } const keywordTranslations: {[key: string]: string} = { ‘cinematic’: ‘cinemático’, ‘long exposure’: ‘longa exposição’, ‘macro’: ‘macro’, ‘golden hour’: ‘hora dourada’, ‘impressionism’: ‘impressionismo’, ‘surrealism’: ‘surrealismo’, ‘pop art’: ‘pop art’, ‘pixel art’: ‘pixel art’, ‘van gogh style’: ‘estilo van gogh’, ‘salvador dali style’: ‘estilo salvador dali’, ‘hokusai style’: ‘estilo hokusai’, ‘dramatic lighting’: ‘iluminação dramática’, ‘soft light’: ‘luz suave’, ‘neon glow’: ‘brilho neon’, ‘rim lighting’: ‘luz de contorno’, ‘bokeh’: ‘bokeh’, ‘lens flare’: ‘reflexo da lente’, ‘double exposure’: ‘dupla exposição’, ‘glitch effect’: ‘efeito glitch’, ‘ugly, blurry, deformed, watermark, text, extra fingers’: ‘feio, borrado, deformado, marca d\’água, texto, dedos extras’ }const PromptHelperModal = ({ isOpen, onClose, onApplyPrompt, onApplyNegativePrompt }: { isOpen: boolean, onClose: () => void, onApplyPrompt: (p: string) => void, onApplyNegativePrompt: (p: string) => void }) => { const [activeTab, setActiveTab] = useState(0); const [lang, setLang] = useState<'en' | 'pt-br'>(‘en’); const [builderInputs, setBuilderInputs] = useState([”, ”, ”, ”]); const [isBuilding, setIsBuilding] = useState(false);if (!isOpen) return null;const content = promptHelperContent[lang]; const handleBuildPrompt = async () => { setIsBuilding(true); const keywords = builderInputs.filter(Boolean).join(‘, ‘); const newPrompt = await buildPromptAPI(keywords); onApplyPrompt(newPrompt); setIsBuilding(false); onClose(); }const renderContent = () => { switch (activeTab) { case 0: // Guide return

{content.guide.title}

{content.guide.content}

{content.guide.example}

; case 1: // Editing return

{content.editing.title}

{content.editing.content}

{content.editing.example}

; case 2: // Builder return

{content.builder.title}

{content.builder.description}

{content.builder.labels.map((label, index) => ( { const newInputs = […builderInputs]; newInputs[index] = e.target.value; setBuilderInputs(newInputs); }} className=”w-full bg-slate-700 border border-slate-600 rounded-md p-2 text-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500″ /> ))}
; case 3: // Keywords return

{content.keywords.title}

{Object.entries(content.keywords.categories).map(([category, keywords]) => (

{lang === ‘pt-br’ ? keywordTranslations[category] || category : category}

{keywords.map(kw => )}
))}
; case 4: // Negative return

{content.negative.title}

{content.negative.content}

onApplyNegativePrompt(promptHelperContent.en.negative.example)}>{content.negative.example}

; default: return null; } }return (
e.stopPropagation()}>

Prompt Helper

{content.tabs.map((tab, index) => ( ))}
{renderContent()}
); };// 6. PromptEditorModal Component const PromptEditorModal = ({ isOpen, onClose, prompt, setPrompt }: { isOpen: boolean, onClose: () => void, prompt: string, setPrompt: (p: string) => void }) => { if (!isOpen) return null; return (
e.stopPropagation()}>

Prompt Editor

); }// — MAIN APP COMPONENT — const App = () => { // — State Management — const [mode, setMode] = useState(‘Image’); const [prompt, setPrompt] = useState(”); const [negativePrompt, setNegativePrompt] = useState(”); const [baseImage, setBaseImage] = useState(null); const [blendImage, setBlendImage] = useState(null); const [aspectRatio, setAspectRatio] = useState(‘1:1’); const [generatedMedia, setGeneratedMedia] = useState(null); const [generatedText, setGeneratedText] = useState(”); const [isLoading, setIsLoading] = useState(false); const [loadingMessage, setLoadingMessage] = useState(”); const [isGalleryOpen, setIsGalleryOpen] = useState(false); const [isPromptEditorOpen, setIsPromptEditorOpen] = useState(false); const [isPromptHelperOpen, setIsPromptHelperOpen] = useState(false); const [creations, setCreations] = useState([]); const dbRef = useRef(null);// — DB Initialization & Loading — useEffect(() => { const init = async () => { dbRef.current = await initDB(); loadCreations(); }; init(); }, []);const loadCreations = async () => { if (dbRef.current) { const allCreations = await dbRef.current.getAll(STORE_NAME); setCreations(allCreations.map(c => ({…c, timestamp: new Date(c.timestamp)}))); } }; // — State Handlers — const isEditMode = mode === ‘Image’ && !!baseImage;const clearAll = () => { setPrompt(”); setNegativePrompt(”); setBaseImage(null); setBlendImage(null); setAspectRatio(‘1:1’); setGeneratedMedia(null); setGeneratedText(”); }; const handleGenerate = async () => { if (isLoading || !prompt) return;setIsLoading(true); setGeneratedMedia(null); setGeneratedText(”); setLoadingMessage(mode === ‘Video’ ? ‘Initializing…’ : ‘Generating…’);const result = await generateMediaAPI(mode, prompt, baseImage, blendImage, aspectRatio, setLoadingMessage); setGeneratedMedia(result.mediaUrl); setGeneratedText(result.text); setIsLoading(false); setLoadingMessage(”);// Save to DB if (dbRef.current) { const newCreation: Omit = { mode, prompt, negativePrompt, baseImage, blendImage, aspectRatio, generatedMedia: result.mediaUrl, generatedText: result.text, timestamp: new Date() }; await dbRef.current.add(STORE_NAME, newCreation); loadCreations(); } };const handleUseAsBase = () => { if (generatedMedia && mode === ‘Image’) { setBaseImage(generatedMedia); setGeneratedMedia(null); setGeneratedText(”); } };const handleDownload = () => { if (generatedMedia && mode === ‘Image’) { const link = document.createElement(‘a’); link.href = generatedMedia; link.download = `nanobanana-${Date.now()}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } };const handleDeleteCreation = async (id: number) => { if (dbRef.current) { await dbRef.current.delete(STORE_NAME, id); loadCreations(); } };const handleReloadCreation = (creation: Creation) => { setMode(creation.mode); setPrompt(creation.prompt); setNegativePrompt(creation.negativePrompt); setBaseImage(creation.baseImage); setBlendImage(creation.blendImage); setAspectRatio(creation.aspectRatio); setGeneratedMedia(creation.generatedMedia); setGeneratedText(creation.generatedText); setIsGalleryOpen(false); };const handleTranslate = async (text: string, setText: (s:string)=>void) => { const translated = await translateTextAPI(text); setText(translated); } // — Dynamic UI Texts — const generateButtonText = () => { if (mode === ‘Video’) return ‘Generate Video’; if (isEditMode) return ‘Generate’; return ‘Create’; }; return (
{/* — Modals — */} setIsGalleryOpen(false)} creations={creations} onDelete={handleDeleteCreation} onReload={handleReloadCreation} /> setIsPromptEditorOpen(false)} prompt={prompt} setPrompt={setPrompt} /> setIsPromptHelperOpen(false)} onApplyPrompt={p => setPrompt(prev => `${prev} ${p}`.trim())} onApplyNegativePrompt={p => setNegativePrompt(prev => `${prev} ${p}`.trim())} />{/* — Header — */}

Nano Banana Studio

{/* — Main Content — */}
{/* Left: Control Panel */}
{/* Mode Toggle */}
{/* Prompts */}