Meta Description" name="description" />
import React, { useState, useRef, useEffect } from 'react';
import { Upload, X, Image as ImageIcon, Sparkles, AlertCircle, Download, Banana, RefreshCw } from 'lucide-react';
const apiKey = ""; // API Key injected by environment
export default function App() {
const [childPhoto, setChildPhoto] = useState(null);
const [adultPhoto, setAdultPhoto] = useState(null);
const [generatedImage, setGeneratedImage] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [status, setStatus] = useState('');
// File handling helpers
const handleFileChange = (e, setPhoto) => {
const file = e.target.files[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
setError("File size too large. Please upload images under 5MB.");
return;
}
const reader = new FileReader();
reader.onloadend = () => {
setPhoto({
file,
preview: reader.result,
base64: reader.result.split(',')[1],
mimeType: file.type
});
setError(null);
};
reader.readAsDataURL(file);
}
};
// API Call with Exponential Backoff
const generateImage = async () => {
if (!childPhoto || !adultPhoto) {
setError("Please upload both photos to continue.");
return;
}
setLoading(true);
setError(null);
setGeneratedImage(null);
setStatus('Initializing Nano Banana Model...');
const prompt = `
Generate a realistic, high-quality image based on the two input images provided.
The first image is a child. The second image is an adult.
Create a heartwarming scene where the adult (from the second image) is holding hands with the child (from the first image).
Key Requirements:
1. The interaction must look natural and emotional.
2. The person from the recent photo (adult) should be interacting with their younger self (child).
3. Use soft, studio-quality lighting.
4. The background must be a smooth, clean white/off-white background.
5. The style should be photorealistic.
`;
// Construct Payload
const payload = {
contents: [{
parts: [
{ text: prompt },
{
inlineData: {
mimeType: childPhoto.mimeType,
data: childPhoto.base64
}
},
{
inlineData: {
mimeType: adultPhoto.mimeType,
data: adultPhoto.base64
}
}
]
}],
generationConfig: {
responseModalities: ["TEXT", "IMAGE"]
}
};
const makeRequest = async (retryCount = 0) => {
try {
setStatus(retryCount > 0 ? `Optimizing pixels (Attempt ${retryCount + 1})...` : 'Processing temporal fusion...');
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent?key=${apiKey}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
}
);
if (!response.ok) {
// Handle specific API errors
if (response.status === 503) throw new Error("Model overloaded");
const errData = await response.json();
throw new Error(errData.error?.message || `API Error: ${response.status}`);
}
const result = await response.json();
// Extract Image
const imagePart = result.candidates?.[0]?.content?.parts?.find(p => p.inlineData);
if (imagePart) {
const base64Image = `data:image/png;base64,${imagePart.inlineData.data}`;
setGeneratedImage(base64Image);
setStatus('Completed!');
} else {
// Sometimes the model might refuse or return only text
const textPart = result.candidates?.[0]?.content?.parts?.find(p => p.text);
if (textPart) {
throw new Error("The model returned text instead of an image. Please try a different photo.");
}
throw new Error("Failed to generate image. Please try again.");
}
} catch (err) {
if (retryCount < 4) {
const delay = Math.pow(2, retryCount) * 1000;
await new Promise(r => setTimeout(r, delay));
return makeRequest(retryCount + 1);
} else {
throw err;
}
}
};
try {
await makeRequest();
} catch (err) {
setError(err.message || "An unexpected error occurred.");
} finally {
setLoading(false);
}
};
const reset = () => {
setChildPhoto(null);
setAdultPhoto(null);
setGeneratedImage(null);
setError(null);
setStatus('');
};
return (
<div className="min-h-screen bg-gray-50 text-gray-800 font-sans selection:bg-yellow-200">
{/* Header */}
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="max-w-5xl mx-auto px-4 h-16 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="bg-yellow-400 p-2 rounded-lg text-white">
<Banana size={20} fill="currentColor" className="text-yellow-900" />
</div>
<h1 className="text-xl font-bold tracking-tight text-gray-900">AiEdit</h1>
<span className="px-2 py-0.5 bg-gray-100 text-gray-500 text-xs rounded-full font-medium border border-gray-200">
Nano Banana Model v1.0
</span>
</div>
<button
onClick={reset}
className="text-sm font-medium text-gray-500 hover:text-gray-900 flex items-center gap-2"
>
<RefreshCw size={14} /> Reset
</button>
</div>
</header>
<main className="max-w-5xl mx-auto px-4 py-12">
<div className="text-center mb-12">
<h2 className="text-4xl font-extrabold text-gray-900 mb-4 tracking-tight">
Meet your inner child.
</h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Upload a childhood photo and a recent photo. Our Nano Banana AI will bridge the gap of time, creating a memory where you stand hand-in-hand with your younger self.
</p>
</div>
{/* Error Message */}
{error && (
<div className="mb-8 p-4 bg-red-50 border border-red-200 rounded-xl flex items-start gap-3 text-red-700 max-w-2xl mx-auto animate-in fade-in slide-in-from-top-4">
<AlertCircle className="shrink-0 mt-0.5" size={20} />
<div>
<p className="font-semibold">Generation Failed</p>
<p className="text-sm opacity-90">{error}</p>
</div>
<button onClick={() => setError(null)} className="ml-auto hover:bg-red-100 p-1 rounded">
<X size={16} />
</button>
</div>
)}
{/* Main Interface Grid */}
<div className="grid md:grid-cols-2 gap-8 items-start">
{/* Left Column: Inputs */}
<div className="space-y-6">
{/* Child Photo Input */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 transition-all hover:shadow-md">
<div className="flex justify-between items-center mb-4">
<label className="text-sm font-bold text-gray-900 uppercase tracking-wide flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-yellow-400"></span>
1. Child Photo
</label>
{childPhoto && (
<button onClick={() => setChildPhoto(null)} className="text-gray-400 hover:text-red-500">
<X size={16} />
</button>
)}
</div>
{!childPhoto ? (
<label className="block w-full cursor-pointer group">
<input type="file" className="hidden" accept="image/*" onChange={(e) => handleFileChange(e, setChildPhoto)} />
<div className="h-48 border-2 border-dashed border-gray-300 rounded-xl flex flex-col items-center justify-center gap-3 group-hover:border-yellow-400 group-hover:bg-yellow-50 transition-colors">
<div className="p-3 bg-gray-100 rounded-full group-hover:bg-white transition-colors">
<Upload className="text-gray-400 group-hover:text-yellow-500" size={24} />
</div>
<p className="text-sm text-gray-500 font-medium group-hover:text-yellow-700">Upload old photo</p>
</div>
</label>
) : (
<div className="relative h-48 rounded-xl overflow-hidden bg-gray-100 ring-1 ring-gray-200">
<img src={childPhoto.preview} alt="Child" className="w-full h-full object-cover" />
</div>
)}
</div>
{/* Adult Photo Input */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 transition-all hover:shadow-md">
<div className="flex justify-between items-center mb-4">
<label className="text-sm font-bold text-gray-900 uppercase tracking-wide flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-blue-500"></span>
2. Recent Photo
</label>
{adultPhoto && (
<button onClick={() => setAdultPhoto(null)} className="text-gray-400 hover:text-red-500">
<X size={16} />
</button>
)}
</div>
{!adultPhoto ? (
<label className="block w-full cursor-pointer group">
<input type="file" className="hidden" accept="image/*" onChange={(e) => handleFileChange(e, setAdultPhoto)} />
<div className="h-48 border-2 border-dashed border-gray-300 rounded-xl flex flex-col items-center justify-center gap-3 group-hover:border-blue-400 group-hover:bg-blue-50 transition-colors">
<div className="p-3 bg-gray-100 rounded-full group-hover:bg-white transition-colors">
<Upload className="text-gray-400 group-hover:text-blue-500" size={24} />
</div>
<p className="text-sm text-gray-500 font-medium group-hover:text-blue-700">Upload recent photo</p>
</div>
</label>
) : (
<div className="relative h-48 rounded-xl overflow-hidden bg-gray-100 ring-1 ring-gray-200">
<img src={adultPhoto.preview} alt="Adult" className="w-full h-full object-cover" />
</div>
)}
</div>
<button
onClick={generateImage}
disabled={loading || !childPhoto || !adultPhoto}
className={`w-full py-4 px-6 rounded-xl font-bold text-lg shadow-lg shadow-yellow-200/50 flex items-center justify-center gap-3 transition-all transform active:scale-95
${loading || !childPhoto || !adultPhoto
? 'bg-gray-200 text-gray-400 cursor-not-allowed shadow-none'
: 'bg-gradient-to-r from-yellow-400 to-yellow-500 text-white hover:from-yellow-500 hover:to-yellow-600'
}`}
>
{loading ? (
<>
<div className="w-5 h-5 border-3 border-white/30 border-t-white rounded-full animate-spin" />
<span>Processing...</span>
</>
) : (
<>
<Sparkles size={20} />
<span>Generate Time Bridge</span>
</>
)}
</button>
{loading && (
<p className="text-center text-sm text-gray-500 animate-pulse font-medium">{status}</p>
)}
</div>
{/* Right Column: Result */}
<div className="bg-white p-2 rounded-3xl shadow-xl shadow-gray-200/50 border border-gray-100 h-full min-h-[500px] flex flex-col">
<div className="flex-1 bg-gray-50 rounded-2xl border-2 border-dashed border-gray-200 flex flex-col items-center justify-center relative overflow-hidden">
{!generatedImage && !loading && (
<div className="text-center p-8 max-w-sm">
<div className="w-20 h-20 bg-white rounded-2xl shadow-sm mx-auto mb-6 flex items-center justify-center">
<ImageIcon className="text-gray-300" size={40} />
</div>
<h3 className="text-gray-900 font-semibold mb-2">Ready to Imagine</h3>
<p className="text-gray-500 text-sm">Upload your photos on the left and hit generate to see the magic happen here.</p>
</div>
)}
{loading && !generatedImage && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-white/80 backdrop-blur-sm z-10">
<div className="relative w-24 h-24 mb-6">
<div className="absolute inset-0 border-4 border-yellow-200 rounded-full animate-ping opacity-25"></div>
<div className="absolute inset-2 border-4 border-yellow-400 border-t-transparent rounded-full animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<Banana className="text-yellow-500 animate-bounce" size={32} />
</div>
</div>
<p className="text-gray-600 font-medium">Consulting Nano Banana...</p>
</div>
)}
{generatedImage && (
<div className="relative w-full h-full group">
<img
src={generatedImage}
alt="Generated Result"
className="w-full h-full object-contain bg-white"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors flex items-end justify-center pb-6 opacity-0 group-hover:opacity-100">
<a
href={generatedImage}
download="AiEdit_TimeBridge.png"
className="bg-white text-gray-900 px-6 py-3 rounded-full font-bold shadow-lg flex items-center gap-2 transform translate-y-4 group-hover:translate-y-0 transition-all hover:bg-yellow-50"
>
<Download size={18} />
Download Memory
</a>
</div>
</div>
)}
</div>
{/* Footer inside the card */}
<div className="p-6 text-center">
<p className="text-xs text-gray-400 uppercase tracking-widest font-semibold">AiEdit Result Preview</p>
</div>
</div>
</div>
</main>
</div>
);
}
4
1
15KB
16KB
84.0ms
204.0ms
116.0ms