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 ? (
<>
>
) : (
{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 ? (
) : (

)}
{!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.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 */}
{mode === ‘Image’ && !isEditMode && (
)}
{/* Image Uploaders */}
{isEditMode &&
}{/* Aspect Ratio */}
{mode === ‘Image’ && !isEditMode &&
}{/* Action Buttons */}
{/* Right: Media Canvas */}
);
};export default App;