/** * @license * SPDX-License-Identifier: Apache-2.0 */ /* tslint:disable */ import {ContentUnion, GoogleGenAI, Modality} from '@google/genai'; import {LoaderCircle, SendHorizontal, Trash2, X, Eraser} from 'lucide-react'; // Added Eraser import {useEffect, useRef, useState} from 'react'; function Home() { const [apiKey, setApiKey] = useState(null); const [ai, setAi] = useState(null); const [keyError, setKeyError] = useState(null); useEffect(() => { async function fetchKey() { try { const response = await fetch('/api/get-api-key'); // Assuming Nginx proxies this if (!response.ok) { throw new Error(`Failed to fetch API key: ${response.statusText}`); } const data = await response.json(); if (data.apiKey) { setApiKey(data.apiKey); setAi(new GoogleGenAI({ apiKey: data.apiKey })); } else { throw new Error(data.error || "API Key not returned from backend"); } } catch (error: any) { console.error("Error fetching API key:", error); setKeyError(error.message); // Show error to user } } fetchKey(); }, []); function parseError(error: string) { try { const regex = /{"error":(.*)}/gm; const m = regex.exec(error); if (m && m[1]) { const e = m[1]; const err = JSON.parse(e); return err.message || error; } } catch (e) { /* ignore */ } return error; } export default function Home() { const canvasRef = useRef(null); const backgroundImageRef = useRef(null); // To store the uploaded image object const fileInputRef = useRef(null); const [uploadedImageFile, setUploadedImageFile] = useState(null); // previewUrl is mostly for the initial load into Image object, canvas is the main display // const [previewUrl, setPreviewUrl] = useState(null); const [isDrawing, setIsDrawing] = useState(false); const [penColor, setPenColor] = useState('#000000'); const colorInputRef = useRef(null); const [prompt, setPrompt] = useState(''); const [generatedImage, setGeneratedImage] = useState(null); const [isLoading, setIsLoading] = useState(false); const [showErrorModal, setShowErrorModal] = useState(false); const [errorMessage, setErrorMessage] = useState(''); // Initialize canvas with white background or uploaded image const initializeCanvas = (clearWhite: boolean = true) => { if (!canvasRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d')!; ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height); if (!clearWhite && backgroundImageRef.current) { ctx.drawImage( backgroundImageRef.current, 0, 0, canvas.width, canvas.height, ); } }; // Effect for API key check useEffect(() => { if (!API_KEY) { setErrorMessage("Gemini API Key is missing. Please configure it to use the application."); setShowErrorModal(true); } initializeCanvas(true); // Initialize canvas on mount }, []); // Load uploaded image onto canvas useEffect(() => { if (uploadedImageFile) { const reader = new FileReader(); reader.onload = (e) => { const img = new window.Image(); img.onload = () => { backgroundImageRef.current = img; initializeCanvas(false); // Clear canvas and draw new image setGeneratedImage(null); // Clear previous AI generated image }; img.onerror = () => { setErrorMessage("Failed to load the uploaded image."); setShowErrorModal(true); backgroundImageRef.current = null; initializeCanvas(true); // Clear to white on error } img.src = e.target?.result as string; }; reader.readAsDataURL(uploadedImageFile); } else { // No file uploaded, or file cleared backgroundImageRef.current = null; initializeCanvas(true); // Clear to white } }, [uploadedImageFile]); // Drawing functions const getCoordinates = (e: React.MouseEvent | React.TouchEvent) => { const canvas = canvasRef.current!; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; let clientX, clientY; if (e.nativeEvent instanceof MouseEvent) { clientX = e.nativeEvent.offsetX * scaleX; clientY = e.nativeEvent.offsetY * scaleY; } else if (e.nativeEvent instanceof TouchEvent && e.nativeEvent.touches?.[0]) { clientX = (e.nativeEvent.touches[0].clientX - rect.left) * scaleX; clientY = (e.nativeEvent.touches[0].clientY - rect.top) * scaleY; } else { // Fallback for other PointerEvents or if offsetX/Y are not directly available clientX = (('clientX' in e.nativeEvent ? e.nativeEvent.clientX : (e.nativeEvent as any).layerX) - rect.left) * scaleX; clientY = (('clientY' in e.nativeEvent ? e.nativeEvent.clientY : (e.nativeEvent as any).layerY) - rect.top) * scaleY; } return { x: clientX, y: clientY }; }; const startDrawing = (e: React.MouseEvent | React.TouchEvent) => { if (!canvasRef.current) return; const { x, y } = getCoordinates(e); const ctx = canvasRef.current.getContext('2d')!; if (e.type === 'touchstart') e.preventDefault(); ctx.beginPath(); ctx.moveTo(x, y); setIsDrawing(true); }; const draw = (e: React.MouseEvent | React.TouchEvent) => { if (!isDrawing || !canvasRef.current) return; if (e.type === 'touchmove') e.preventDefault(); const { x, y } = getCoordinates(e); const ctx = canvasRef.current.getContext('2d')!; ctx.lineWidth = 5; ctx.lineCap = 'round'; ctx.strokeStyle = penColor; ctx.lineTo(x, y); ctx.stroke(); }; const stopDrawing = () => { if (isDrawing) { const ctx = canvasRef.current?.getContext('2d'); if (ctx) ctx.closePath(); // Close the path, though stroke() already renders setIsDrawing(false); } }; const handleImageUpload = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { setUploadedImageFile(file); // This will trigger the useEffect to draw it } }; // Clears only the drawings, keeps the uploaded image const clearDrawingLayer = () => { initializeCanvas(false); // Redraws background image, effectively clearing drawings }; // Clears everything: uploaded image, drawings, generated image const resetAll = () => { setUploadedImageFile(null); // This will trigger useEffect to clear canvas to white backgroundImageRef.current = null; setGeneratedImage(null); setPrompt(''); if (fileInputRef.current) { fileInputRef.current.value = ""; } initializeCanvas(true); // Ensure it's white }; const handleColorChange = (e: React.ChangeEvent) => { setPenColor(e.target.value); }; const openColorPicker = () => { colorInputRef.current?.click(); }; const handleKeyDownForColorPicker = (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { openColorPicker(); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!ai) { setErrorMessage("Gemini API client is not initialized. Check API Key."); setShowErrorModal(true); return; } if (!canvasRef.current || !backgroundImageRef.current) { // Ensure an image was uploaded setErrorMessage('Please upload an image first.'); setShowErrorModal(true); return; } if (!prompt.trim()) { setErrorMessage('Please enter a prompt.'); setShowErrorModal(true); return; } setIsLoading(true); setGeneratedImage(null); try { // Get the current canvas content (uploaded image + drawings) as base64 const canvas = canvasRef.current; const drawingAndImageData = canvas.toDataURL('image/png').split(',')[1]; const imageMimeType = 'image/png'; // We are sending PNG from canvas console.log('Request payload:', { prompt, imageMimeType, imageData: drawingAndImageData ? `${drawingAndImageData.substring(0, 50)}... (truncated)` : null, }); let contents: ContentUnion[] = [ { role: 'USER', parts: [ { inlineData: { data: drawingAndImageData, mimeType: imageMimeType } }, { text: `${prompt}. Modify the image based on the prompt and any drawings.` }, // Added context for drawings ], }, ]; const response = await ai.models.generateContent({ model: 'gemini-2.0-flash-preview-image-generation', contents, config: { responseModalities: [Modality.TEXT, Modality.IMAGE], }, }); const apiResponseData = { success: true, message: '', imageData: null, error: undefined }; for (const part of response.candidates[0].content.parts) { if (part.text) apiResponseData.message = part.text; else if (part.inlineData) apiResponseData.imageData = part.inlineData.data; } console.log('API Response:', { ...apiResponseData, imageData: apiResponseData.imageData ? `${apiResponseData.imageData.substring(0, 50)}... (truncated)` : null, }); if (apiResponseData.success && apiResponseData.imageData) { const imageUrl = `data:image/png;base64,${apiResponseData.imageData}`; setGeneratedImage(imageUrl); } else { const errorMsg = apiResponseData.error || apiResponseData.message || 'Failed to generate image from API response.'; setErrorMessage(errorMsg); setShowErrorModal(true); } } catch (error: any) { console.error('Error submitting image and prompt:', error); setErrorMessage(parseError(error.message || 'An unexpected error occurred.')); setShowErrorModal(true); } finally { setIsLoading(false); } }; const closeErrorModal = () => setShowErrorModal(false); // Add touch event prevention for drawing useEffect(() => { const canvas = canvasRef.current; const preventTouchDefault = (e: TouchEvent) => { if (isDrawing && canvas && canvas.contains(e.target as Node)) { e.preventDefault(); } }; if (canvas) { canvas.addEventListener('touchstart', preventTouchDefault, { passive: false }); canvas.addEventListener('touchmove', preventTouchDefault, { passive: false }); } return () => { if (canvas) { canvas.removeEventListener('touchstart', preventTouchDefault); canvas.removeEventListener('touchmove', preventTouchDefault); } }; }, [isDrawing]); return ( <>

Gemini Image Editor

Upload an image, draw on it, and prompt Gemini to modify!

Built with{' '} Gemini API

{/* Image upload and canvas area */}
{/* AI Generated Image Display */} {generatedImage && (

AI Generated Image:

Generated by AI
)}
setPrompt(e.target.value)} placeholder="Describe changes or what to generate from the image & drawings..." 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 font-mono" required disabled={!API_KEY || !backgroundImageRef.current} />
{showErrorModal && (

Error

{parseError(errorMessage)}

)}
); }