Erudition Logo
<< Back to EdUp Articles

July 9, 2025

Create Your Own AI Image Generator with Gemini and a Few Lines of Code!

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...

AI Image Generator thumbnail

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.

How to Set It Up in Gemini with Canvas

  1. Open Gemini and Start a New Canvas: In the Gemini interface, look for the option to create a new "Canvas" or interactive environment.
  2. Paste the Code: Simply copy the entire HTML code block from the expandable section and paste it directly into the Gemini Canvas.
  3. 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

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 the textarea to suggest different creative ideas.
Download the Tool
<!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: 85%;
            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;
        }
        .lightbox-nav {
            position: absolute;
            top: 50%;
            transform: translateY(-50%);
            font-size: 4rem;
            color: white;
            cursor: pointer;
            text-shadow: 0 0 10px black;
            padding: 1rem;
            user-select: none;
            transition: background-color 0.2s;
            border-radius: 50%;
        }
        .lightbox-nav:hover {
            background-color: rgba(255, 255, 255, 0.1);
        }
        .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-container {
            position: relative;
            aspect-ratio: 1 / 1;
            border-radius: 0.375rem; /* rounded-md */
            overflow: hidden;
            transition: all 0.2s ease-in-out;
        }
        .history-thumbnail-container:hover {
             transform: scale(1.05);
        }
        .history-thumbnail {
            width: 100%;
            height: 100%;
            object-fit: cover;
            cursor: pointer;
        }
        .history-thumbnail-checkbox {
            position: absolute;
            top: 0.5rem;
            right: 0.5rem;
            width: 1.5rem;
            height: 1.5rem;
            cursor: pointer;
            opacity: 0;
            transition: opacity 0.2s ease-in-out;
        }
        .history-thumbnail-container:hover .history-thumbnail-checkbox,
        .history-thumbnail-checkbox:checked {
            opacity: 1;
        }
        .thumbnail-selected {
            box-shadow: 0 0 0 3px #3b82f6; /* blue-500 */
            transform: scale(1.05);
        }
        
        .lightbox-button-container {
            position: absolute;
            bottom: 1.25rem; /* bottom-5 */
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            gap: 1rem; /* gap-4 */
        }
    </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" onerror="this.onerror=null;this.src='https://placehold.co/200x64/1e293b/ffffff?text=Erudition+Design';">
                <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">
                 <div class="flex justify-between items-center mb-2">
                    <label for="prompt-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Image Prompt</label>
                    <button id="improve-prompt-btn" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">Improve Prompt</button>
                </div>
                <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">
                 <div class="flex justify-between items-center mb-4">
                    <h2 class="text-xl font-bold text-gray-800 dark:text-white">Generation History</h2>
                    <div id="history-actions" class="flex gap-2">
                        <button id="delete-selected-btn" class="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded-lg shadow transition-opacity disabled:opacity-50 disabled:cursor-not-allowed" disabled>Delete Selected</button>
                        <button id="delete-all-btn" class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-lg shadow">Delete All</button>
                    </div>
                </div>
                <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>
        <span id="lightbox-prev" class="lightbox-nav left-4 hidden"><</span>
        <img id="lightbox-img" src="" alt="Enlarged image">
        <span id="lightbox-next" class="lightbox-nav right-4 hidden">></span>
        <div class="lightbox-button-container">
            <a id="lightbox-download-btn" href="#" download="erudition-design-image.png" class="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>
            <button id="lightbox-regenerate-btn" class="hidden bg-green-600 hover:bg-green-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-green-300 dark:focus:ring-green-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="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 110 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
                </svg>
                Regenerate
            </button>
        </div>
    </div>

    <!-- Confirmation Modal -->
    <div id="confirm-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
        <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800">
            <div class="mt-3 text-center">
                <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white" id="confirm-modal-title">Delete History</h3>
                <div class="mt-2 px-7 py-3">
                    <p class="text-sm text-gray-500 dark:text-gray-300" id="confirm-modal-text">Are you sure you want to delete all items? This action cannot be undone.</p>
                </div>
                <div class="items-center px-4 py-3 flex justify-center gap-4">
                    <button id="confirm-modal-cancel" class="px-3 py-1.5 bg-gray-200 text-gray-800 dark:bg-gray-600 dark:text-gray-200 rounded-md hover:bg-gray-300">Cancel</button>
                    <button id="confirm-modal-confirm" class="px-3 py-1.5 bg-red-600 text-white rounded-md hover:bg-red-700">Delete</button>
                </div>
            </div>
        </div>
    </div>
    
    <!-- Prompt Improvement Modal -->
    <div id="improve-prompt-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
        <div class="relative top-20 mx-auto p-5 border w-full max-w-lg shadow-lg rounded-md bg-white dark:bg-gray-800">
            <div class="mt-3">
                <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white text-center">Improve Your Prompt</h3>
                <div id="suggestions-container" class="mt-4 px-4 py-3 max-h-64 overflow-y-auto">
                    <!-- Suggestions will be injected here -->
                </div>
                <div class="items-center px-4 py-3 flex justify-end gap-3">
                    <button id="improve-prompt-cancel" class="px-4 py-2 bg-gray-200 text-gray-800 dark:bg-gray-600 dark:text-gray-200 rounded-md hover:bg-gray-300">Cancel</button>
                    <button id="rewrite-prompt-btn" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Rewrite Prompt</button>
                </div>
            </div>
        </div>
    </div>


    <!-- Firebase SDK -->
    <script type="module">
        // --- Firebase SDK Imports ---
        import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
        import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
        import { getFirestore, collection, addDoc, query, onSnapshot, serverTimestamp, doc, deleteDoc } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";

        // --- 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 lightboxRegenerateBtn = document.getElementById('lightbox-regenerate-btn');
        const lightboxPrev = document.getElementById('lightbox-prev');
        const lightboxNext = document.getElementById('lightbox-next');
        const historySection = document.getElementById('history-section');
        const historyGrid = document.getElementById('history-grid');
        const deleteSelectedBtn = document.getElementById('delete-selected-btn');
        const deleteAllBtn = document.getElementById('delete-all-btn');
        const confirmModal = document.getElementById('confirm-modal');
        const confirmModalConfirm = document.getElementById('confirm-modal-confirm');
        const confirmModalCancel = document.getElementById('confirm-modal-cancel');
        const improvePromptBtn = document.getElementById('improve-prompt-btn');
        const improvePromptModal = document.getElementById('improve-prompt-modal');
        const improvePromptCancel = document.getElementById('improve-prompt-cancel');
        const rewritePromptBtn = document.getElementById('rewrite-prompt-btn');
        const suggestionsContainer = document.getElementById('suggestions-container');


        // --- Firebase & App State ---
        let db, auth, userId, historyUnsubscribe;
        let activeHistoryItems = [];
        let currentHistoryIndex = -1;
        let selectedHistoryIds = new Set();


        // --- Firebase Configuration & Initialization ---
        // These variables are provided by the environment.
        const appId = typeof __app_id !== 'undefined' ? __app_id : 'erudition-image-generator';
        const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : null;

        /**
         * Initializes Firebase, authenticates the user, and loads their history.
         */
        async function main() {
            if (!firebaseConfig) {
                console.error("Firebase config is not available.");
                showMessage("Application is not configured correctly. History will not be available.", "error");
                setLoadingState(false);
                generateBtn.disabled = true; // Disable core functionality if DB is not available
                return;
            }

            try {
                const app = initializeApp(firebaseConfig);
                db = getFirestore(app);
                auth = getAuth(app);
                
                onAuthStateChanged(auth, async (user) => {
                    if (user) {
                        userId = user.uid;
                        console.log("User authenticated with UID:", userId);
                        // User is signed in, now we can load their history.
                        loadHistory();
                    } else {
                        // User is signed out. Clear any existing history.
                        console.log("User is not authenticated.");
                        if (historyUnsubscribe) historyUnsubscribe();
                        historyGrid.innerHTML = '';
                        historySection.classList.add('hidden');
                    }
                });

                // Authenticate the user
                if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
                    await signInWithCustomToken(auth, __initial_auth_token);
                    console.log("Signed in with custom token.");
                } else {
                    await signInAnonymously(auth);
                    console.log("Signed in anonymously.");
                }

            } catch (error) {
                console.error("Firebase Initialization Error:", error);
                showMessage("Could not connect to the database. History will be disabled.", "error");
            }
        }


        // --- Event Listeners ---
        lightbox.addEventListener('click', () => lightbox.classList.add('hidden'));
        lightboxClose.addEventListener('click', () => lightbox.classList.add('hidden'));
        lightboxPrev.addEventListener('click', (e) => {
            e.stopPropagation();
            navigateLightbox('prev');
        });
        lightboxNext.addEventListener('click', (e) => {
            e.stopPropagation();
            navigateLightbox('next');
        });
        lightboxRegenerateBtn.addEventListener('click', regenerateImageFromHistory);


        aspectRatioSelect.addEventListener('change', () => {
            ratioWarning.classList.toggle('hidden', aspectRatioSelect.value !== '2:1');
        });

        generateBtn.addEventListener('click', handleImageGeneration);
        deleteSelectedBtn.addEventListener('click', handleDeleteSelected);
        deleteAllBtn.addEventListener('click', () => showConfirmModal('all'));
        confirmModalCancel.addEventListener('click', () => confirmModal.classList.add('hidden'));
        improvePromptBtn.addEventListener('click', handleImprovePrompt);
        improvePromptCancel.addEventListener('click', () => improvePromptModal.classList.add('hidden'));
        rewritePromptBtn.addEventListener('click', applyPromptSuggestions);


        // --- Core Functions ---
        
        /**
         * A robust fetch function with retry logic and exponential backoff.
         * @param {string} url - The URL to fetch.
         * @param {object} options - The options for the fetch request.
         * @param {number} retries - The number of times to retry on failure.
         * @returns {Promise<Response>}
         */
        async function fetchWithRetry(url, options, retries = 3) {
            let lastError;
            for (let i = 0; i < retries; i++) {
                try {
                    const response = await fetch(url, options);
                    if (!response.ok) {
                        // Throw the response to be handled by the catch block, which will trigger a retry
                        throw response;
                    }
                    return response;
                } catch (error) {
                    lastError = error;
                    if (i < retries - 1) {
                        console.warn(\`Fetch attempt \${i + 1} failed. Retrying in \${Math.pow(2, i)}s...\`);
                        await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
                    }
                }
            }
            // If all retries fail, throw the last error
            throw lastError;
        }

        /**
         * Handles the standard text-to-image generation process.
         */
        async function handleImageGeneration() {
            const prompt = promptInput.value.trim();
            if (!prompt) {
                showMessage('Please enter a prompt.', 'error');
                return;
            }
            if (!userId) {
                showMessage('User not authenticated. Cannot generate images.', 'error');
                return;
            }

            setLoadingState(true);
            
            try {
                const aspectRatio = aspectRatioSelect.value;
                const sampleCount = parseInt(imageCountSelect.value, 10);
                
                const payload = {
                    instances: [{ prompt: prompt }],
                    parameters: { 
                        "sampleCount": sampleCount,
                        "aspectRatio": aspectRatio
                    }
                };
                
                console.log("Sending Imagen API Payload:", JSON.stringify(payload, null, 2));

                const apiKey = ""; // Provided by the environment
                const apiUrl = \`https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict?key=\${apiKey}\`;

                const response = await fetchWithRetry(apiUrl, {
                    method: 'POST',
                    headers: { 
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(payload)
                });

                const result = await response.json();

                if (result.predictions && result.predictions.length > 0) {
                    imageGrid.innerHTML = ''; 
                    for (const prediction of result.predictions) {
                        if (prediction.bytesBase64Encoded) {
                            const imageUrl = \`data:image/png;base64,\${prediction.bytesBase64Encoded}\`;
                            displayGeneratedImage(imageUrl, prompt, aspectRatio);
                            await saveToHistory(imageUrl, prompt, aspectRatio);
                        }
                    }
                    imageGrid.classList.remove('hidden');
                } else {
                    showMessage('Your image could not be generated. The prompt may have been rejected for safety reasons. Please try a different prompt.', 'error');
                }
            } catch (error) {
                handleApiError(error);
                placeholder.classList.remove('hidden');
            } finally {
                setLoadingState(false);
            }
        }
        
        /**
         * Creates a smaller, compressed JPEG data URL from a given image URL.
         * @param {string} imageUrl - The original base64 data URL of the image.
         * @param {number} maxWidth - The maximum width of the thumbnail.
         * @param {number} quality - The JPEG quality (0.0 to 1.0).
         * @returns {Promise<string>} A promise that resolves with the thumbnail data URL.
         */
        function createThumbnailDataUrl(imageUrl, maxWidth, quality = 0.85) {
            return new Promise((resolve, reject) => {
                const img = new Image();
                img.onload = () => {
                    const canvas = document.createElement('canvas');
                    const ctx = canvas.getContext('2d');
                    let width = img.width;
                    let height = img.height;
                    if (width > maxWidth) {
                        height = (maxWidth / width) * height;
                        width = maxWidth;
                    }
                    canvas.width = width;
                    canvas.height = height;
                    ctx.drawImage(img, 0, 0, width, height);
                    resolve(canvas.toDataURL('image/jpeg', quality));
                };
                img.onerror = (err) => {
                    console.error("Failed to load image for thumbnail creation.", err);
                    reject(new Error("Failed to load image for thumbnail creation."));
                };
                img.src = imageUrl;
            });
        }

        /**
         * Saves a generated image's metadata to Firestore.
         * @param {string} imageUrl - The full-resolution base64 data URL of the image.
         * @param {string} prompt - The prompt used to generate the image.
         * @param {string} aspectRatio - The aspect ratio of the image.
         */
        async function saveToHistory(imageUrl, prompt, aspectRatio) {
            if (!db || !userId) {
                console.error("Firestore not initialized or user not logged in. Cannot save history.");
                return;
            }
            try {
                const thumbnailUrl = await createThumbnailDataUrl(imageUrl, 1024, 0.85);
                const historyCollectionRef = collection(db, \`artifacts/\${appId}/users/\${userId}/history\`);
                await addDoc(historyCollectionRef, {
                    imageUrl: thumbnailUrl,
                    prompt: prompt,
                    aspectRatio: aspectRatio,
                    createdAt: serverTimestamp() 
                });
                console.log("History item saved to Firestore.");
            } catch (error) {
                console.error("Error saving to Firestore:", error);
                showMessage("Could not save image to history. The image may be too large.", "error");
            }
        }

        /**
         * Sets up a real-time listener to load and display the user's image history.
         */
        function loadHistory() {
            if (!db || !userId) return;
            const historyCollectionRef = collection(db, \`artifacts/\${appId}/users/\${userId}/history\`);
            const q = query(historyCollectionRef);
            if (historyUnsubscribe) historyUnsubscribe();
            historyUnsubscribe = onSnapshot(q, (querySnapshot) => {
                const historyItems = [];
                querySnapshot.forEach((doc) => {
                    historyItems.push({ id: doc.id, ...doc.data() });
                });
                historyItems.sort((a, b) => (b.createdAt?.toMillis() || 0) - (a.createdAt?.toMillis() || 0));
                activeHistoryItems = historyItems;
                historySection.classList.toggle('hidden', activeHistoryItems.length === 0);
                historyGrid.innerHTML = '';
                activeHistoryItems.forEach((item, index) => {
                    if (item.imageUrl) {
                        appendHistoryThumbnail(item, index);
                    }
                });
                selectedHistoryIds.clear();
                updateDeleteButtonsState();
            }, (error) => {
                console.error("Error fetching history:", error);
                showMessage("Could not load generation history.", "error");
            });
        }

        /**
         * Regenerates an image using the settings from a history item.
         */
        function regenerateImageFromHistory() {
            if (currentHistoryIndex === -1) return;
            const item = activeHistoryItems[currentHistoryIndex];
            if (!item) return;

            // Populate the main UI with the history item's settings
            promptInput.value = item.prompt;
            aspectRatioSelect.value = item.aspectRatio;
            
            // Close the lightbox
            lightbox.classList.add('hidden');

            // Scroll to the top to see the generation happen
            window.scrollTo({ top: 0, behavior: 'smooth' });

            // Trigger the standard text-to-image generation process
            handleImageGeneration();
        }

        /**
         * Deletes the selected history items from Firestore.
         */
        function handleDeleteSelected() {
            if (selectedHistoryIds.size === 0) return;
            showConfirmModal('selected');
        }

        /**
         * Deletes a list of history items from Firestore.
         * @param {Set<string>} idsToDelete - A set of Firestore document IDs to delete.
         */
        async function deleteHistoryItems(idsToDelete) {
            if (!db || !userId) {
                showMessage("Database connection lost. Cannot delete.", "error");
                return;
            }
            const count = idsToDelete.size;
            if (count === 0) return;

            const deletePromises = [];
            for (const id of idsToDelete) {
                const docRef = doc(db, \`artifacts/\${appId}/users/\${userId}/history\`, id);
                deletePromises.push(deleteDoc(docRef));
            }
            try {
                await Promise.all(deletePromises);
                showMessage(\`\${count} item(s) deleted successfully.\`, 'success');
            } catch (error) {
                console.error("Error deleting history items:", error);
                showMessage("An error occurred while deleting items.", "error");
            }
        }

        /**
         * Shows the confirmation modal for deletion.
         * @param {'selected'|'all'} type - The type of deletion being confirmed.
         */
        function showConfirmModal(type) {
            const title = document.getElementById('confirm-modal-title');
            const text = document.getElementById('confirm-modal-text');
            if (type === 'all') {
                title.textContent = 'Delete All History';
                text.textContent = \`Are you sure you want to delete all \${activeHistoryItems.length} items? This action cannot be undone.\`;
                confirmModalConfirm.onclick = async () => {
                    const allIds = new Set(activeHistoryItems.map(item => item.id));
                    await deleteHistoryItems(allIds);
                    confirmModal.classList.add('hidden');
                };
            } else {
                title.textContent = 'Delete Selected Items';
                text.textContent = \`Are you sure you want to delete the \${selectedHistoryIds.size} selected item(s)? This action cannot be undone.\`;
                confirmModalConfirm.onclick = async () => {
                    await deleteHistoryItems(new Set(selectedHistoryIds)); // Pass a copy
                    confirmModal.classList.add('hidden');
                };
            }
            confirmModal.classList.remove('hidden');
        }

        /**
         * Handles the "Improve Prompt" button click.
         */
        async function handleImprovePrompt() {
            const currentPrompt = promptInput.value.trim();
            if (!currentPrompt) {
                showMessage("Please enter a prompt to improve.", "error");
                return;
            }

            suggestionsContainer.innerHTML = '<div class="loader mx-auto"></div>';
            improvePromptModal.classList.remove('hidden');

            try {
                const apiKey = "";
                const apiUrl = \`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=\${apiKey}\`;
                
                const requestBody = {
                    contents: [{
                        parts: [{
                            text: \`Analyze this image prompt and suggest 4-5 enhancements. Focus on adding descriptive details, specifying an art style or medium, and improving composition. Return ONLY a JSON object with a single key "suggestions" which is an array of strings. Do not include any other text or markdown. Prompt: "\${currentPrompt}"\`
                        }]
                    }],
                     generationConfig: {
                        responseMimeType: "application/json",
                     }
                };

                const response = await fetchWithRetry(apiUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(requestBody)
                });
                
                const result = await response.json();
                const suggestionsText = result.candidates[0].content.parts[0].text;
                const suggestions = JSON.parse(suggestionsText).suggestions;


                suggestionsContainer.innerHTML = '';
                if (suggestions && suggestions.length > 0) {
                    suggestions.forEach((suggestion, index) => {
                        const div = document.createElement('div');
                        div.className = 'flex items-center mb-2';
                        const checkbox = document.createElement('input');
                        checkbox.type = 'checkbox';
                        checkbox.id = \`suggestion-\${index}\`;
                        checkbox.value = suggestion;
                        checkbox.className = 'h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500';
                        const label = document.createElement('label');
                        label.htmlFor = \`suggestion-\${index}\`;
                        label.textContent = suggestion;
                        label.className = 'ml-3 block text-sm text-gray-700 dark:text-gray-300';
                        div.appendChild(checkbox);
                        div.appendChild(label);
                        suggestionsContainer.appendChild(div);
                    });
                } else {
                    suggestionsContainer.textContent = "Could not generate suggestions. Please try again.";
                }

            } catch (error) {
                handleApiError(error);
                suggestionsContainer.textContent = "Failed to get suggestions. Check the console for details.";
            }
        }
        
        /**
         * Applies the selected prompt suggestions to the main prompt input.
         */
        function applyPromptSuggestions() {
            let newPrompt = promptInput.value.trim();
            const checkboxes = suggestionsContainer.querySelectorAll('input[type="checkbox"]:checked');
            
            checkboxes.forEach(checkbox => {
                newPrompt += \`, \${checkbox.value}\`;
            });

            promptInput.value = newPrompt;
            improvePromptModal.classList.add('hidden');
        }


        // --- UI Helper Functions ---

        /**
         * Creates and displays a single generated image in the main grid.
         * @param {string} imageUrl - The base64 data URL of the image.
         */
        function displayGeneratedImage(imageUrl, prompt, aspectRatio) {
            const fileName = \`erudition-\${prompt.slice(0, 20).replace(/\\s/g, '_')}-\${Date.now()}.png\`;
            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: ' + prompt;
            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';
            downloadLink.addEventListener('click', (e) => e.stopPropagation());
            container.appendChild(img);
            container.appendChild(downloadLink);
            container.addEventListener('click', () => openLightbox(imageUrl, fileName, -1));
            imageGrid.appendChild(container);
        }

        /**
         * Creates and appends a thumbnail to the history grid.
         * @param {object} item - The history item from Firestore.
         * @param {number} index - The index of the item in the history array.
         */
        function appendHistoryThumbnail(item, index) {
            const container = document.createElement('div');
            container.className = 'history-thumbnail-container';
            const thumbnail = document.createElement('img');
            thumbnail.src = item.imageUrl;
            thumbnail.alt = 'History thumbnail: ' + item.prompt;
            thumbnail.className = 'history-thumbnail';
            thumbnail.addEventListener('click', () => {
                const imageUrl = item.imageUrl;
                const fileName = \`erudition-design-image-\${item.id}.png\`;
                openLightbox(imageUrl, fileName, index);
            });
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.className = 'history-thumbnail-checkbox';
            checkbox.addEventListener('change', () => {
                if (checkbox.checked) {
                    selectedHistoryIds.add(item.id);
                    container.classList.add('thumbnail-selected');
                } else {
                    selectedHistoryIds.delete(item.id);
                    container.classList.remove('thumbnail-selected');
                }
                updateDeleteButtonsState();
            });
            container.appendChild(thumbnail);
            container.appendChild(checkbox);
            historyGrid.appendChild(container);
        }
        
        /**
         * Updates the enabled/disabled state of the delete buttons.
         */
        function updateDeleteButtonsState() {
            deleteSelectedBtn.disabled = selectedHistoryIds.size === 0;
        }

        /**
         * Opens the lightbox with a specific image.
         * @param {string} src - The image source URL.
         * @param {string} downloadName - The filename for the download link.
         * @param {number} index - The index in the history array. -1 if not from history.
         */
        function openLightbox(src, downloadName, index) {
            currentHistoryIndex = index;
            lightboxImg.src = src;
            lightboxDownloadBtn.href = src;
            lightboxDownloadBtn.download = downloadName;
            lightbox.classList.remove('hidden');
            updateNavVisibility();
            lightboxRegenerateBtn.classList.toggle('hidden', index === -1);
        }

        /**
         * Shows or hides the lightbox navigation arrows based on the current index.
         */
        function updateNavVisibility() {
            if (currentHistoryIndex === -1 || activeHistoryItems.length <= 1) {
                lightboxPrev.classList.add('hidden');
                lightboxNext.classList.add('hidden');
            } else {
                lightboxPrev.classList.toggle('hidden', currentHistoryIndex === 0);
                lightboxNext.classList.toggle('hidden', currentHistoryIndex === activeHistoryItems.length - 1);
            }
        }

        /**
         * Navigates the lightbox to the previous or next image.
         * @param {'prev'|'next'} direction - The direction to navigate.
         */
        function navigateLightbox(direction) {
            if (direction === 'next' && currentHistoryIndex < activeHistoryItems.length - 1) {
                currentHistoryIndex++;
            } else if (direction === 'prev' && currentHistoryIndex > 0) {
                currentHistoryIndex--;
            }
            const item = activeHistoryItems[currentHistoryIndex];
            if (item) {
                const imageUrl = item.imageUrl;
                const fileName = \`erudition-design-image-\${item.id}.png\`;
                lightboxImg.src = imageUrl;
                lightboxDownloadBtn.href = imageUrl;
                lightboxDownloadBtn.download = fileName;
                updateNavVisibility();
            }
        }

        /**
         * 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');
        }

        /**
         * Parses and displays API or network errors.
         * @param {Error|Response} error - The error object.
         */
        async function handleApiError(error) {
            let errorMessage = 'An unexpected error occurred. Please try again.';
            console.error('An error occurred during image generation. Raw error object:', error);
            if (error instanceof Response) {
                const status = error.status;
                try {
                    const errorBody = await error.json();
                    console.error("Server error response:", errorBody);
                    if (errorBody.error && errorBody.error.message) {
                        errorMessage = \`Server Error (\${status}): \${errorBody.error.message}\`;
                    } else {
                        errorMessage = \`An API error occurred (Status: \${status}).\`;
                    }
                } catch (e) {
                    const textResponse = await error.text();
                    console.error("Raw text response from server:", textResponse);
                    if (textResponse) {
                        errorMessage = \`An API error occurred (Status: \${status}). See console for details.\`;
                    } else {
                        errorMessage = \`An API error occurred (Status: \${status}). The server returned an empty response. This could indicate a network issue or a problem with the request format.\`;
                    }
                }
            } else if (error instanceof Error) {
                console.error(\`Client-side Error Details:\`, { name: error.name, message: error.message, stack: error.stack });
                errorMessage = \`A client-side error occurred: \${error.message}\`;
            } else {
                try {
                    errorMessage = \`An unexpected error occurred: \${JSON.stringify(error)}\`;
                } catch (e) {
                    errorMessage = \`An unexpected and non-serializable error occurred.\`;
                }
            }
            showMessage(errorMessage, 'error');
        }

        // --- Start the application ---
        window.onload = main;

    </script>
</body>
</html>
                           
                   
               
               

Start Building, Start Learning

               

You don't need to be a professional developer to build this. Using the provided HTML, CSS, and JavaScript as a template, you can create your own version. Experiment with the instructions you send to Gemini. Add new prompt styles, ask for different educational breakdowns, or even have it suggest five key questions you should ask yourself before writing a prompt.

               

The act of building the tool itself is a lesson in prompt engineering. By crafting the instructions for Gemini to generate and explain prompts, you are practicing the very skills you aim to learn. It's a meta-learning experience that is one of the most effective ways to master the art and science of communicating with AI.