Ever wanted to build your own AI-powered tool? It's easier than you think! I'm sharing the complete code for a custom image generator that you can run and modify directly within Google's Gemini.
This tool provides a clean interface to enter prompts, select aspect ratios, and generate multiple images at once. It even includes a session history and a lightbox for viewing your creations.
Click below to expand and get the full code.
Click to expand and copy the full HTML code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Erudition Design Image Generator</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Lightbox styles */
.lightbox {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 2rem;
cursor: pointer;
backdrop-filter: blur(5px);
}
.lightbox img {
max-width: 90%;
max-height: 85%; /* Make space for download button */
object-fit: contain;
border-radius: 0.5rem;
box-shadow: 0 0 40px rgba(0,0,0,0.5);
cursor: default;
}
.lightbox-close {
position: absolute;
top: 20px;
right: 30px;
font-size: 3rem;
color: white;
cursor: pointer;
line-height: 1;
text-shadow: 0 0 5px black;
}
.generated-image-container {
position: relative;
cursor: pointer;
transition: transform 0.2s ease-in-out;
border-radius: 0.5rem; /* rounded-lg */
overflow: hidden; /* Ensures download button corners are rounded */
}
.generated-image-container:hover {
transform: scale(1.05);
}
.history-thumbnail {
cursor: pointer;
border-radius: 0.375rem; /* rounded-md */
border: 2px solid transparent;
transition: all 0.2s ease-in-out;
aspect-ratio: 1 / 1;
}
.history-thumbnail:hover {
transform: scale(1.1);
border-color: #3b82f6; /* blue-500 */
}
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<div class="min-h-screen flex items-center justify-center p-4">
<div class="w-full max-w-3xl mx-auto bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8">
<div class="text-center mb-8">
<img src="https://erudition.ca/images/ED%20logo%20-%20Copy.png" alt="Erudition Design Logo" class="mx-auto h-16 w-auto mb-4 bg-slate-700 p-1 rounded-md">
<p class="text-gray-600 dark:text-gray-300 mt-2">Generate images with your custom prompts and settings.</p>
</div>
<!-- Prompt Input -->
<div class="mb-6">
<label for="prompt-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Image Prompt</label>
<textarea id="prompt-input" rows="4" class="w-full p-3 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition" placeholder="Enter a detailed description..."></textarea>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- Aspect Ratio Selector -->
<div>
<label for="aspect-ratio-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Aspect Ratio</label>
<select id="aspect-ratio-select" class="w-full p-3 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
<option value="1:1">1:1 (Square)</option>
<option value="16:9" selected>16:9 (Widescreen)</option>
<option value="9:16">9:16 (Portrait)</option>
<option value="4:3">4:3 (Standard)</option>
<option value="3:4">3:4 (Tall)</option>
<option value="2:1">2:1 (Panoramic)</option>
</select>
<p id="ratio-warning" class="hidden text-xs text-yellow-500 dark:text-yellow-400 mt-2">Note: Panoramic ratios may be less stable and could occasionally result in an error.</p>
</div>
<!-- Number of Images Selector -->
<div>
<label for="image-count-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Number of Images</label>
<select id="image-count-select" class="w-full p-3 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
<option value="1">1 Image</option>
<option value="4">4 Images</option>
</select>
</div>
</div>
<!-- Generate Button -->
<div class="text-center mb-6">
<button id="generate-btn" class="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-8 rounded-lg shadow-lg transition-transform transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800">
Generate Image(s)
</button>
</div>
<!-- Image Display Area -->
<div id="image-container-wrapper" class="mt-6 w-full min-h-[24rem] bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center border-2 border-dashed border-gray-300 dark:border-gray-600 p-4">
<div id="placeholder" class="text-center text-gray-500 dark:text-gray-400">
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 4v.01M28 8L16 20m12-12v8m0 4v.01M20 28h8m-4-4v8m12-12v8m0 4v.01M28 8L16 20m12-12v8m0 4v.01M20 28h8m-4-4v8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<p class="mt-2 text-sm">Your generated images will appear here</p>
</div>
<div id="loader" class="loader hidden"></div>
<div id="image-grid" class="hidden w-full h-full grid grid-cols-1 sm:grid-cols-2 gap-4"></div>
</div>
<!-- Info/Error Message -->
<div id="message-area" class="hidden mt-4 text-center p-3 rounded-lg"></div>
<!-- Session History -->
<div id="history-section" class="hidden mt-8">
<h2 class="text-xl font-bold text-center mb-4 text-gray-800 dark:text-white">Session History</h2>
<div id="history-grid" class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-3">
<!-- Thumbnails will be injected here by JavaScript -->
</div>
</div>
</div>
</div>
<!-- Lightbox Structure -->
<div id="lightbox" class="lightbox hidden">
<span id="lightbox-close" class="lightbox-close">×</span>
<img id="lightbox-img" src="" alt="Enlarged image">
<!-- Download button for lightbox -->
<a id="lightbox-download-btn" href="#" download="erudition-design-image.png" class="absolute bottom-5 left-1/2 -translate-x-1/2 bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg shadow-lg transition-transform transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 flex items-center gap-2" onclick="event.stopPropagation();">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
Download
</a>
</div>
<script>
// --- DOM Element Selection ---
const generateBtn = document.getElementById('generate-btn');
const promptInput = document.getElementById('prompt-input');
const aspectRatioSelect = document.getElementById('aspect-ratio-select');
const imageCountSelect = document.getElementById('image-count-select');
const placeholder = document.getElementById('placeholder');
const loader = document.getElementById('loader');
const imageGrid = document.getElementById('image-grid');
const messageArea = document.getElementById('message-area');
const ratioWarning = document.getElementById('ratio-warning');
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');
const lightboxClose = document.getElementById('lightbox-close');
const lightboxDownloadBtn = document.getElementById('lightbox-download-btn');
const historySection = document.getElementById('history-section');
const historyGrid = document.getElementById('history-grid');
// --- Event Listeners ---
// Lightbox close functionality
lightbox.addEventListener('click', () => {
lightbox.classList.add('hidden');
});
lightboxClose.addEventListener('click', () => {
lightbox.classList.add('hidden');
});
// Show a warning for potentially unstable aspect ratios
aspectRatioSelect.addEventListener('change', () => {
if (aspectRatioSelect.value === '2:1') {
ratioWarning.classList.remove('hidden');
} else {
ratioWarning.classList.add('hidden');
}
});
// Main function to generate images on button click
generateBtn.addEventListener('click', async () => {
const prompt = promptInput.value.trim();
if (!prompt) {
showMessage('Please enter a prompt.', 'error');
return;
}
// --- UI State Management: Show Loading ---
setLoadingState(true);
let response; // To hold the fetch response for better error logging
try {
// --- API Payload Preparation ---
const aspectRatio = aspectRatioSelect.value;
const sampleCount = parseInt(imageCountSelect.value, 10);
const payload = {
instances: [{ prompt: prompt }],
parameters: {
"sampleCount": sampleCount,
"aspectRatio": aspectRatio
}
};
// The environment handles the API key automatically.
const apiKey = "";
const apiUrl = \`https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict?key=\${apiKey}\`;
// --- API Call ---
response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
// Throw an error to be caught by the catch block
throw new Error(\`HTTP error! status: \${response.status}\`);
}
const result = await response.json();
// --- Process and Display Results ---
if (result.predictions && result.predictions.length > 0) {
result.predictions.forEach(prediction => {
if (prediction.bytesBase64Encoded) {
const imageUrl = \`data:image/png;base64,\${prediction.bytesBase64Encoded}\`;
const fileName = \`erudition-design-image-\${Date.now()}.png\`;
// --- Create Image Container with Download Button ---
const container = document.createElement('div');
container.className = 'relative group generated-image-container';
const img = document.createElement('img');
img.src = imageUrl;
img.alt = 'Generated AI Image';
img.className = 'w-full h-full object-contain rounded-lg bg-gray-800';
const downloadLink = document.createElement('a');
downloadLink.href = imageUrl;
downloadLink.download = fileName;
downloadLink.className = 'absolute bottom-2 right-2 bg-blue-600 text-white p-2 rounded-full shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300 hover:bg-blue-700';
downloadLink.innerHTML = \`<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>\`;
downloadLink.title = 'Download Image';
// Prevent lightbox from opening when download button is clicked
downloadLink.addEventListener('click', (e) => e.stopPropagation());
container.appendChild(img);
container.appendChild(downloadLink);
// Open lightbox when the container (but not the download button) is clicked
container.addEventListener('click', () => {
lightboxImg.src = img.src;
lightboxDownloadBtn.href = img.src;
lightboxDownloadBtn.download = fileName;
lightbox.classList.remove('hidden');
});
imageGrid.appendChild(container);
// --- Create and append the history thumbnail ---
const thumbnail = document.createElement('img');
thumbnail.src = imageUrl;
thumbnail.alt = 'History thumbnail';
thumbnail.className = 'w-full h-full object-cover history-thumbnail';
thumbnail.addEventListener('click', () => {
lightboxImg.src = thumbnail.src;
lightboxDownloadBtn.href = thumbnail.src;
lightboxDownloadBtn.download = fileName;
lightbox.classList.remove('hidden');
});
// Prepend the thumbnail to show the newest first
historyGrid.prepend(thumbnail);
}
});
imageGrid.classList.remove('hidden');
// Show history section if it has content
if (historyGrid.children.length > 0) {
historySection.classList.remove('hidden');
}
} else {
throw new Error('Invalid response structure from API. No predictions found.');
}
} catch (error) {
// --- Error Handling ---
console.error('Error generating image:', error);
let errorMessage = 'An error occurred. Please try again.';
if (aspectRatioSelect.value === '2:1') {
errorMessage += ' The 2:1 ratio can sometimes fail. Please try again or select a different ratio.';
}
if (response) {
try {
const errorBody = await response.json();
console.error("Server error response:", JSON.stringify(errorBody, null, 2));
if (errorBody.error && errorBody.error.message) {
errorMessage = \`Server Error: \${errorBody.error.message}\`;
}
} catch (e) {
console.error("Could not parse the error response body as JSON.");
}
}
showMessage(errorMessage, 'error');
placeholder.classList.remove('hidden');
} finally {
// --- UI State Management: Hide Loading ---
setLoadingState(false);
}
});
// --- Helper Functions ---
/**
* Sets the UI to a loading or non-loading state.
* @param {boolean} isLoading - True to show loader, false to hide.
*/
function setLoadingState(isLoading) {
if (isLoading) {
placeholder.classList.add('hidden');
imageGrid.classList.add('hidden');
imageGrid.innerHTML = '';
messageArea.classList.add('hidden');
loader.classList.remove('hidden');
generateBtn.disabled = true;
generateBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
loader.classList.add('hidden');
generateBtn.disabled = false;
generateBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
/**
* Displays a message to the user.
* @param {string} text - The message to display.
* @param {string} type - 'error' or 'success' for styling.
*/
function showMessage(text, type = 'error') {
messageArea.textContent = text;
const baseClasses = 'mt-4 text-center p-3 rounded-lg';
if (type === 'error') {
messageArea.className = \`\${baseClasses} bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-300\`;
} else if (type === 'success') {
messageArea.className = \`\${baseClasses} bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-300\`;
}
messageArea.classList.remove('hidden');
if (type === 'success') {
setTimeout(() => {
messageArea.classList.add('hidden');
}, 3000);
}
}
</script>
</body>
</html>
How to Set It Up in Gemini with Canvas
- Open Gemini and Start a New Canvas: In the Gemini interface, look for the option to create a new "Canvas" or interactive environment.
- Paste the Code: Simply copy the entire HTML code block above and paste it directly into the Gemini Canvas.
- Run the Code: Execute the code within the Canvas. Gemini will render the HTML and create an interactive preview of the image generator.
Important Note: This tool is specifically designed to run within Gemini. It relies on Gemini's backend to securely handle the API key and process the image generation requests. Therefore, it will not work as a standalone webpage.
Easy to Modify and Make Your Own
The best part is that you can easily customize this tool. Here are a few ideas:
- Change the branding: Swap out the "Erudition Design" logo and text with your own.
- Adjust the options: Add or remove aspect ratios from the dropdown menu.
- Set default prompts: Modify the
placeholder
text in thetextarea
to suggest different creative ideas.
This is a fantastic way to get hands-on experience with AI and web development without the hassle of setting up a complex development environment. Let me know what you create!
#AI #WebDevelopment #Gemini #Google #GenerativeAI