This is mind blowing!
Here is the entire Source Code you can run in Google's AI Studio and do this yourself:
import React, { useState, useCallback } from 'react';
// --- Constants ---
const DEFAULT_PROMPT = `
Create a realistic Instagram post composition.
The main subject (from the uploaded image) should be prominently displayed as the primary image within a clean, modern Instagram post frame.
The post frame should include:
- A user handle (e.g., "user_name")
- A small circular profile picture icon next to the handle (matching the main subject)
- Standard Instagram-like icons for "Like" (heart), "Comment" (speech bubble), and "Share" (paper plane) below the main image.
- A placeholder for "X likes" (e.g., "Liked by other_user and 1.3M others") and a short caption (e.g., "Working remote from anywhere! Loving this new setup #digitalnomad #remotework #workfromanywhere #travel")
Separately, generate a full-body, high-quality image of the person from the uploaded photo, sitting casually on a modern stool and actively using a silver laptop.
Overlay this generated full-body person as a transparent cutout, positioned on the bottom-right side, *outside* and partially overlapping the main Instagram post frame.
The overall background for the entire composition (behind the Instagram frame) should be a softly blurred, modern, well-lit office or cafe interior, giving a professional yet relaxed vibe. Ensure all elements are well-integrated and look natural.
`;
// --- Helper Components ---
/**
* A simple loading spinner component.
*/
const Spinner = () => (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
);
/**
* A component to render the image upload area.
*/
const ImageUploader = ({ onImageUpload, originalImage }) => {
const [isDragging, setIsDragging] = useState(false);
const handleFileChange = (e) => {
if (file) {
onImageUpload(file);
}
};
const handleDragOver = (e) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (e) => {
e.preventDefault();
setIsDragging(false);
};
const handleDrop = (e) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files?.[0];
if (file) {
onImageUpload(file);
}
};
return (
<div className="w-full">
<label
htmlFor="file-upload"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`flex flex-col items-center justify-center w-full h-64 border-2 border-dashed rounded-lg cursor-pointer transition-colors
${isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300 bg-gray-50 hover:bg-gray-100'}
${originalImage ? 'border-green-500 bg-green-50' : ''}
`}
>
{originalImage ? (
<div className="text-center">
<img src={originalImage} alt="Uploaded preview" className="max-h-48 rounded-md shadow-sm mx-auto" />
<p className="mt-4 text-sm font-medium text-green-700">Image loaded!</p>
<p className="text-xs text-gray-500">Drop a new image or click to replace</p>
</div>
) : (
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<svg className="w-8 h-8 mb-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16"> <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
</svg>
<p className="mb-2 text-sm text-gray-500"><span className="font-semibold">Click to upload</span> or drag and drop</p>
<p className="text-xs text-gray-500">PNG, JPG, or WEBP</p>
</div>
)}
</label>
<input id="file-upload" type="file" className="hidden" accept="image/png, image/jpeg, image/webp" onChange={handleFileChange} />
</div>
);
};
/**
* A component to display the generated image result and download option.
*/
const GenerationResult = ({ generatedImage, isLoading, error }) => {
// --- UPDATE: More robust download handler ---
const handleDownload = async () => {
if (generatedImage) {
try {
// The generatedImage is a data URL: "data:image/png;base64,..."
// Fetch it to convert it into a blob
const response = await fetch(generatedImage);
const blob = await response.blob();
// Create an object URL from the blob
const objectUrl = URL.createObjectURL(blob);
// Use the same anchor link trick, but with the object URL
const link = document.createElement('a');
link.href = objectUrl;
document.body.appendChild(link);
// Clean up: remove the link and revoke the object URL
document.body.removeChild(link);
URL.revokeObjectURL(objectUrl);
} catch (error) {
console.error("Download failed:", error);
// We can't use alert(), so we'll just log the error.
// A more advanced implementation might show a small error message in the UI.
}
}
};
// --- END UPDATE ---
return (
<div className="w-full h-full min-h-[400px] bg-gray-900 rounded-lg shadow-inner flex flex-col items-center justify-center p-4">
{isLoading && <Spinner />}
{error && !isLoading && (
<div className="text-center text-red-400">
<p className="font-semibold">Generation Failed</p>
<p className="text-sm mt-2">{error}</p>
</div>
)}
{generatedImage && !isLoading && (
<>
<img
key={generatedImage} // Force re-render on new image
src={generatedImage}
alt="Generated composition"
className="max-w-full max-h-full object-contain rounded-md"
/>
{/* --- Download Button (calls new async handler) --- */}
<button
onClick={handleDownload}
className="mt-4 flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<svg className="w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
Download Image
</button>
</>
)}
{!generatedImage && !isLoading && !error && (
<div className="text-center text-gray-400">
<svg className="w-12 h-12 mx-auto text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm16.5-1.5V6" />
</svg>
<p className="mt-4 font-medium">Your generated composition will appear here</p>
<p className="text-sm text-gray-500 mt-1">Upload an image and write a prompt to begin</p>
</div>
)}
</div>
);
};
// --- Main App Component ---
export default function App() {
const [imageFile, setImageFile] = useState(null);
const [originalImage, setOriginalImage] = useState(null); // For display
const [prompt, setPrompt] = useState(DEFAULT_PROMPT);
const [generatedImage, setGeneratedImage] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
/**
* Utility function to convert a File object to a base64 string,
* removing the data URL prefix.
*/
const fileToGenerativePart = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// Result is "data:image/jpeg;base64,..."
// We only want the part after the comma
const base64Data = reader.result.split(',')[1];
resolve({
inlineData: {
data: base64Data,
mimeType: file.type,
},
});
};
reader.onerror = (err) => reject(err);
reader.readAsDataURL(file);
});
};
/**
* Handles the file upload, setting both the file object
* and a data URL for preview.
*/
const handleImageUpload = (file) => {
if (!file.type.startsWith("image/")) {
setError("Please upload a valid image file (PNG, JPG, WEBP).");
return;
}
setError(null);
setImageFile(file);
setGeneratedImage(null); // Clear previous generation
// Create a URL for previewing the original image
const reader = new FileReader();
reader.onload = (e) => {
};
reader.readAsDataURL(file);
};
/**
* Implements exponential backoff for API retries.
*/
const fetchWithBackoff = async (url, options, retries = 3, delay = 1000) => {
try {
const response = await fetch(url, options);
if (!response.ok) {
if (response.status === 429 && retries > 0) {
// Throttled, retry with backoff
await new Promise(res => setTimeout(res, delay));
return fetchWithBackoff(url, options, retries - 1, delay * 2);
}
// Try to get error message from response body
let errorBody = "No error details from API.";
try {
const body = await response.json();
errorBody = body?.error?.message || JSON.stringify(body);
} catch(e) {
// Could not parse JSON
errorBody = await response.text();
}
throw new Error(`API Error: ${response.status} ${response.statusText}. Details: ${errorBody}`);
}
return response.json();
} catch (err) {
if (retries > 0 && !(err instanceof DOMException && err.name === 'AbortError')) { // Network or other error, retry
await new Promise(res => setTimeout(res, delay));
return fetchWithBackoff(url, options, retries - 1, delay * 2);
}
throw err; // After retries, throw the original error
}
};
/**
* Handles the "Generate" button click.
*/
const handleGenerate = useCallback(async () => {
if (!imageFile || !prompt) {
setError("Please upload an image and provide a prompt.");
return;
}
setIsLoading(true);
setError(null);
setGeneratedImage(null);
try {
// 1. Get the base64 data part for the API
const imagePart = await fileToGenerativePart(imageFile);
// 2. Construct the API payload
const payload = {
contents: [
{
role: "user",
parts: [
{ text: prompt },
imagePart
]
}
],
generationConfig: {
responseModalities: ['TEXT', 'IMAGE']
},
};
// 3. Make the API call with backoff
const apiKey = ""; // API key is injected by the environment
const result = await fetchWithBackoff(API_URL + apiKey, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
// 4. Process the response
const base64Data = result?.candidates?.[0]?.content?.parts?.find(p => p.inlineData)?.inlineData?.data;
if (base64Data) {
const imageUrl = `data:image/png;base64,${base64Data}`;
setGeneratedImage(imageUrl);
} else {
if (result?.candidates?.[0]?.finishReason === 'SAFETY') {
throw new Error("Generation failed due to safety filters. Try a different prompt or image.");
}
console.error("Unexpected API response:", result);
throw new Error("No image data found in API response. The model may not have been able to process this request.");
}
} catch (err) {
console.error("Generation failed:", err);
setError(err.message || "An unknown error occurred during generation.");
} finally {
setIsLoading(false);
}
}, [imageFile, prompt]);
return (
<div className="flex flex-col lg:flex-row min-h-screen bg-gray-100 font-sans">
{/* --- Control Panel (Left Side) --- */}
<div className="w-full lg:w-1/3 xl:w-1/4 p-6 bg-white shadow-lg overflow-y-auto">
<h1 className="text-2xl font-bold text-gray-800 mb-1">Image Composer AI</h1>
<p className="text-sm text-gray-500 mb-6">Create new compositions from your images.</p>
<div className="space-y-6">
{/* --- Step 1: Upload --- */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">1. Upload Your Base Image</label>
<ImageUploader onImageUpload={handleImageUpload} originalImage={originalImage} />
</div>
{/* --- Step 2: Prompt --- */}
<div>
<label htmlFor="prompt" className="block text-sm font-medium text-gray-700 mb-2">2. Describe the Composition</label>
<textarea
id="prompt"
rows="6"
className="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
value={prompt}
onChange={(e) => setPrompt(e.target.value)} placeholder="e.g., Place this person in a forest..."
/>
</div>
{/* --- Step 3: Generate --- */}
<div>
<button
onClick={handleGenerate}
disabled={isLoading || !imageFile}
className="w-full flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-lg shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition-all"
>
{isLoading ? (
<>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<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"></path>
</svg>
Generating...
</>
) : (
"Generate Composition"
)}
</button>
{error && !isLoading && (
<p className="text-sm text-red-600 mt-3 text-center">{error}</p>
)}
</div>
</div>
</div>
{/* --- Main Content Area (Right Side) --- */}
<div className="w-full lg:w-2/3 xl:w-3/4 p-6 lg:p-10 flex-1">
<h2 className="text-xl font-semibold text-gray-700 mb-4">Composition Result</h2>
<GenerationResult
generatedImage={generatedImage}
isLoading={isLoading}
error={error}
/>
</div>
</div>
);
}