Spaces:
Running
Running
| /** | |
| * @license | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| /* tslint:disable */ | |
| // FIX: Import `PersonGeneration` and `SafetyFilterLevel` enums from `@google/genai` to fix type errors. | |
| import { | |
| GoogleGenAI, | |
| PersonGeneration, | |
| SafetyFilterLevel, | |
| } from '@google/genai'; | |
| import JSZip from 'jszip'; | |
| import { | |
| Archive, | |
| Download, | |
| ImageIcon, | |
| LoaderCircle, | |
| SendHorizontal, | |
| SlidersHorizontal, | |
| Trash2, | |
| X, | |
| } from 'lucide-react'; | |
| import {useState} from 'react'; | |
| const ai = new GoogleGenAI({apiKey: process.env.API_KEY}); | |
| function parseError(error: string): React.ReactNode { | |
| if (error.includes('429') && error.includes('RESOURCE_EXHAUSTED')) { | |
| return ( | |
| <> | |
| You've exceeded your current API quota (Rate Limit). This is a usage | |
| limit on Google's servers. | |
| <br /> | |
| <br /> | |
| Please check your plan and billing details, or try again after some | |
| time. For more information, visit the{' '} | |
| <a | |
| href="https://ai.google.dev/gemini-api/docs/rate-limits" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-blue-600 underline hover:text-blue-800" | |
| > | |
| Gemini API rate limits documentation | |
| </a> | |
| . | |
| </> | |
| ); | |
| } | |
| const regex = /"message":\s*"(.*?)"/g; | |
| const match = regex.exec(error); | |
| if (match && match[1]) { | |
| return match[1]; | |
| } | |
| return error; | |
| } | |
| export default function Home() { | |
| const [prompt, setPrompt] = useState(''); | |
| const [generatedImages, setGeneratedImages] = useState<string[]>([]); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [isZipping, setIsZipping] = useState(false); | |
| const [showErrorModal, setShowErrorModal] = useState(false); | |
| const [errorMessage, setErrorMessage] = useState<React.ReactNode>(''); | |
| const [numberOfImages, setNumberOfImages] = useState(2); | |
| const [aspectRatio, setAspectRatio] = useState('1:1'); | |
| const [showSettings, setShowSettings] = useState(false); | |
| // New state for advanced settings | |
| const [model, setModel] = useState('imagen-4.0-fast-generate-001'); | |
| // FIX: Use the PersonGeneration enum for the personGeneration state to fix type errors. | |
| const [personGeneration, setPersonGeneration] = useState<PersonGeneration>( | |
| PersonGeneration.ALLOW_ALL, | |
| ); | |
| const handleClear = () => { | |
| setGeneratedImages([]); | |
| setPrompt(''); | |
| setNumberOfImages(2); | |
| setAspectRatio('1:1'); | |
| setModel('imagen-4.0-fast-generate-001'); | |
| // FIX: Use enum members to set state, resolving type errors. | |
| setPersonGeneration(PersonGeneration.ALLOW_ALL); | |
| }; | |
| const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | |
| e.preventDefault(); | |
| if (!prompt) { | |
| setErrorMessage('Please enter a prompt to generate an image.'); | |
| setShowErrorModal(true); | |
| return; | |
| } | |
| setIsLoading(true); | |
| setGeneratedImages([]); // Clear previous results | |
| try { | |
| const response = await ai.models.generateImages({ | |
| model, | |
| prompt, | |
| config: { | |
| numberOfImages: Number(numberOfImages), | |
| aspectRatio, | |
| personGeneration, | |
| }, | |
| }); | |
| const imageUrls = response.generatedImages.map( | |
| (img) => `data:image/png;base64,${img.image.imageBytes}`, | |
| ); | |
| setGeneratedImages(imageUrls); | |
| } catch (error) { | |
| console.error('Error generating images:', error); | |
| const rawMessage = (error as Error).message || 'An unexpected error occurred.'; | |
| setErrorMessage(parseError(rawMessage)); | |
| setShowErrorModal(true); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleDownload = (src: string, index: number) => { | |
| const link = document.createElement('a'); | |
| link.href = src; | |
| link.download = `imagen4-studio-${index + 1}.png`; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| }; | |
| const handleDownloadAll = async () => { | |
| if (generatedImages.length === 0) return; | |
| setIsZipping(true); | |
| try { | |
| const zip = new JSZip(); | |
| for (let i = 0; i < generatedImages.length; i++) { | |
| const src = generatedImages[i]; | |
| const base64Data = src.split(',')[1]; | |
| zip.file(`imagen4-studio-${i + 1}.png`, base64Data, {base64: true}); | |
| } | |
| const content = await zip.generateAsync({type: 'blob'}); | |
| const link = document.createElement('a'); | |
| link.href = URL.createObjectURL(content); | |
| link.download = 'imagen4-studio-images.zip'; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| URL.revokeObjectURL(link.href); | |
| } catch (error) { | |
| console.error('Error creating zip file:', error); | |
| setErrorMessage('Failed to create the zip file.'); | |
| setShowErrorModal(true); | |
| } finally { | |
| setIsZipping(false); | |
| } | |
| }; | |
| const closeErrorModal = () => { | |
| setShowErrorModal(false); | |
| }; | |
| const models = [ | |
| {id: 'imagen-4.0-fast-generate-001', name: 'Imagen 4 Fast'}, | |
| {id: 'imagen-4.0-generate-001', name: 'Imagen 4'}, | |
| {id: 'imagen-4.0-ultra-generate-001', name: 'Imagen 4 Ultra'}, | |
| ]; | |
| // FIX: Use enum members for option IDs to ensure type safety. | |
| const personGenerationOptions = [ | |
| {id: PersonGeneration.ALLOW_ALL, name: 'Allow All'}, | |
| {id: PersonGeneration.ALLOW_ADULT, name: 'Allow Adults'}, | |
| {id: PersonGeneration.DONT_ALLOW, name: "Don't Allow"}, | |
| ]; | |
| const aspectRatios = ['1:1', '16:9', '4:3', '3:4', '9:16']; | |
| const SettingButton = ({onClick, current, value, children}) => ( | |
| <button | |
| type="button" | |
| onClick={() => onClick(value)} | |
| className={`px-3 py-1.5 text-sm font-semibold rounded-md transition-colors ${ | |
| current === value | |
| ? 'bg-black text-white' | |
| : 'bg-gray-200 hover:bg-gray-300' | |
| }`}> | |
| {children} | |
| </button> | |
| ); | |
| return ( | |
| <> | |
| <div className="min-h-screen text-gray-900 flex flex-col justify-start items-center"> | |
| <main className="container mx-auto px-3 sm:px-6 py-5 sm:py-10 pb-32 max-w-5xl w-full"> | |
| <div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4 sm:mb-8 gap-2"> | |
| <div> | |
| <h1 className="text-2xl sm:text-3xl font-bold mb-0 leading-tight"> | |
| IMAGEN4 AIO | |
| </h1> | |
| <p className="text-sm sm:text-base text-gray-500 mt-1"> | |
| Powered by the{' '} | |
| <a | |
| className="underline" | |
| href="https://aistudio.google.com/app/apikey" | |
| target="_blank" | |
| rel="noopener noreferrer"> | |
| Google Gemini API | |
| </a> | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-2 self-start sm:self-auto"> | |
| <button | |
| type="button" | |
| onClick={() => setShowSettings(!showSettings)} | |
| className={`w-12 h-12 rounded-full flex items-center justify-center shadow-sm transition-all hover:scale-110 ${ | |
| showSettings ? 'bg-gray-200' : 'bg-white hover:bg-gray-50' | |
| }`} | |
| aria-label="Toggle Settings"> | |
| <SlidersHorizontal className="w-6 h-6 text-gray-700" /> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={handleDownloadAll} | |
| disabled={isLoading || isZipping || generatedImages.length === 0} | |
| className="w-12 h-12 rounded-full flex items-center justify-center bg-white shadow-sm transition-all hover:bg-gray-50 hover:scale-110 disabled:scale-100 disabled:cursor-not-allowed disabled:bg-gray-200" | |
| aria-label="Download all images as a zip"> | |
| {isZipping ? ( | |
| <LoaderCircle className="w-6 h-6 animate-spin text-gray-700" /> | |
| ) : ( | |
| <Archive className="w-6 h-6 text-gray-700" /> | |
| )} | |
| </button> | |
| <button | |
| type="button" | |
| onClick={handleClear} | |
| className="w-12 h-12 rounded-full flex items-center justify-center bg-white shadow-sm transition-all hover:bg-gray-50 hover:scale-110" | |
| aria-label="Clear Results"> | |
| <Trash2 className="w-6 h-6 text-gray-700" /> | |
| </button> | |
| </div> | |
| </div> | |
| <div | |
| className={`w-full mb-6 h-[60vh] bg-gray-200/50 rounded-lg flex justify-center p-4 border-2 border-dashed border-gray-300 overflow-y-auto ${ | |
| generatedImages.length > 0 ? 'items-start' : 'items-center' | |
| }`}> | |
| {isLoading ? ( | |
| <div className="text-center text-gray-600 self-center"> | |
| <LoaderCircle className="w-12 h-12 animate-spin mx-auto" /> | |
| <p className="mt-4 font-semibold">Generating images...</p> | |
| <p className="text-sm text-gray-500">This may take a moment</p> | |
| </div> | |
| ) : generatedImages.length > 0 ? ( | |
| <div | |
| className={`grid gap-4 w-full ${ | |
| generatedImages.length > 1 | |
| ? 'grid-cols-1 sm:grid-cols-2' | |
| : 'grid-cols-1' | |
| }`}> | |
| {generatedImages.map((src, index) => ( | |
| <div | |
| key={index} | |
| className="relative group flex items-center justify-center bg-black/5 rounded-md overflow-hidden"> | |
| <img | |
| src={src} | |
| alt={`Generated image ${index + 1}`} | |
| className="max-w-full max-h-full object-contain rounded-md" | |
| /> | |
| <button | |
| onClick={() => handleDownload(src, index)} | |
| className="absolute top-2 right-2 p-2 bg-black/50 text-white rounded-full opacity-0 group-hover:opacity-100 transition-all duration-300 hover:scale-110" | |
| aria-label="Download image"> | |
| <Download className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div className="text-center text-gray-500"> | |
| <ImageIcon className="w-12 h-12 mx-auto" /> | |
| <h3 className="font-semibold text-lg mt-4"> | |
| Your generated images will appear here | |
| </h3> | |
| <p>Enter a prompt below to get started</p> | |
| </div> | |
| )} | |
| </div> | |
| {/* Input form */} | |
| <form onSubmit={handleSubmit} className="w-full"> | |
| <div className="relative"> | |
| <input | |
| type="text" | |
| value={prompt} | |
| onChange={(e) => setPrompt(e.target.value)} | |
| placeholder="Describe the image you want to create..." | |
| className="w-full p-3 sm:p-4 pr-12 sm:pr-14 text-sm sm:text-base border-2 border-black bg-white text-gray-800 shadow-sm focus:ring-2 focus:ring-gray-200 focus:outline-none transition-all h-14" | |
| required | |
| /> | |
| <button | |
| type="submit" | |
| disabled={isLoading} | |
| className="absolute right-3 sm:right-4 top-1/2 -translate-y-1/2 p-1.5 sm:p-2 rounded-none bg-black text-white hover:cursor-pointer hover:bg-gray-800 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors" | |
| aria-label="Submit"> | |
| {isLoading ? ( | |
| <LoaderCircle | |
| className="w-5 sm:w-6 h-5 sm:h-6 animate-spin" | |
| aria-label="Loading" | |
| /> | |
| ) : ( | |
| <SendHorizontal className="w-5 sm:w-6 h-5 sm:h-6" /> | |
| )} | |
| </button> | |
| </div> | |
| </form> | |
| </main> | |
| {/* Settings Modal */} | |
| {showSettings && ( | |
| <div | |
| className="fixed inset-0 bg-black/50 flex items-center justify-center z-40 p-4" | |
| onClick={() => setShowSettings(false)}> | |
| <div | |
| className="bg-white rounded-lg shadow-2xl max-w-md w-full" | |
| onClick={(e) => e.stopPropagation()}> | |
| <div className="flex justify-between items-center p-4 border-b"> | |
| <h2 className="text-xl font-bold">Settings</h2> | |
| <button | |
| type="button" | |
| onClick={() => setShowSettings(false)} | |
| className="p-1 rounded-full hover:bg-gray-100" | |
| aria-label="Close settings"> | |
| <X className="w-6 h-6" /> | |
| </button> | |
| </div> | |
| <div className="p-6 space-y-6 overflow-y-auto max-h-[70vh]"> | |
| <div> | |
| <span className="block text-sm font-medium text-gray-700 mb-2"> | |
| Model | |
| </span> | |
| <div className="flex flex-wrap gap-2"> | |
| {models.map((m) => ( | |
| <SettingButton | |
| key={m.id} | |
| onClick={setModel} | |
| current={model} | |
| value={m.id}> | |
| {m.name} | |
| </SettingButton> | |
| ))} | |
| </div> | |
| </div> | |
| <div> | |
| <span className="block text-sm font-medium text-gray-700 mb-2"> | |
| Person Generation | |
| </span> | |
| <div className="flex flex-wrap gap-2"> | |
| {personGenerationOptions.map((opt) => ( | |
| <SettingButton | |
| key={opt.id} | |
| onClick={setPersonGeneration} | |
| current={personGeneration} | |
| value={opt.id}> | |
| {opt.name} | |
| </SettingButton> | |
| ))} | |
| </div> | |
| </div> | |
| <div> | |
| <label | |
| htmlFor="num-images" | |
| className="block text-sm font-medium text-gray-700 mb-2"> | |
| Number of Images | |
| </label> | |
| <input | |
| type="number" | |
| id="num-images" | |
| min="1" | |
| max="4" | |
| value={numberOfImages} | |
| onChange={(e) => { | |
| const val = Math.max( | |
| 1, | |
| Math.min(4, Number(e.target.value)), | |
| ); | |
| setNumberOfImages(val); | |
| }} | |
| className="w-24 p-2 border border-gray-300 rounded-md shadow-sm focus:ring-gray-500 focus:border-gray-500" | |
| /> | |
| </div> | |
| <div> | |
| <span className="block text-sm font-medium text-gray-700 mb-2"> | |
| Aspect Ratio | |
| </span> | |
| <div className="flex flex-wrap gap-2"> | |
| {aspectRatios.map((ratio) => ( | |
| <SettingButton | |
| key={ratio} | |
| onClick={setAspectRatio} | |
| current={aspectRatio} | |
| value={ratio}> | |
| {ratio} | |
| </SettingButton> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Error Modal */} | |
| {showErrorModal && ( | |
| <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> | |
| <div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6"> | |
| <div className="flex justify-between items-start mb-4"> | |
| <h3 className="text-xl font-bold text-gray-700"> | |
| Generation Failed | |
| </h3> | |
| <button | |
| onClick={closeErrorModal} | |
| className="text-gray-400 hover:text-gray-500"> | |
| <X className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| <div className="font-medium text-gray-600"> | |
| {errorMessage} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </> | |
| ); | |
| } | |