/** * @license * SPDX-License-Identifier: Apache-2.0 */ /* tslint:disable */ import { GoogleGenAI, GenerateContentResponse, } from '@google/genai'; import { ArrowUp, Brush, Cpu, Eraser, Info, LoaderCircle, Moon, Palette, Redo2, Sun, Trash2, Undo2, X, } from 'lucide-react'; import {useEffect, useRef, useState} from 'react'; export default function Home() { const canvasRef = useRef(null); const backgroundImageRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); const [prompt, setPrompt] = useState(''); const [generatedImage, setGeneratedImage] = useState(null); const [isLoading, setIsLoading] = useState(false); const [showErrorModal, setShowErrorModal] = useState(false); const [showInfoModal, setShowInfoModal] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [errorTitle, setErrorTitle] = useState('Failed to generate'); // New state for API Key management to match the desired flow const [apiKey, setApiKey] = useState(''); const [showApiKeyModal, setShowApiKeyModal] = useState(false); const [tempApiKey, setTempApiKey] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); // Brush settings const [brushSize, setBrushSize] = useState(5); const [showBrushMenu, setShowBrushMenu] = useState(false); const [brushColor, setBrushColor] = useState('#000000'); const [showColorMenu, setShowColorMenu] = useState(false); const [isErasing, setIsErasing] = useState(false); // Model settings const [selectedModel, setSelectedModel] = useState('gemini-2.5-flash-image'); const [showModelMenu, setShowModelMenu] = useState(false); // Theme settings const [isDarkMode, setIsDarkMode] = useState(false); // State for canvas history const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); // Toggle Dark Mode useEffect(() => { if (isDarkMode) { document.documentElement.setAttribute('data-theme', 'dark'); } else { document.documentElement.removeAttribute('data-theme'); } }, [isDarkMode]); const toggleDarkMode = () => { setIsDarkMode(!isDarkMode); }; // When switching to canvas mode, initialize it and its history useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL(); setHistory([dataUrl]); setHistoryIndex(0); }, []); // Load background image when generatedImage changes useEffect(() => { if (generatedImage && canvasRef.current) { const img = new window.Image(); img.onload = () => { backgroundImageRef.current = img; drawImageToCanvas(); setTimeout(saveCanvasState, 50); }; img.src = generatedImage; } }, [generatedImage]); const initializeCanvas = () => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height); }; const drawImageToCanvas = () => { if (!canvasRef.current || !backgroundImageRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage( backgroundImageRef.current, 0, 0, canvas.width, canvas.height, ); }; // Canvas history functions const saveCanvasState = () => { if (!canvasRef.current) return; const canvas = canvasRef.current; const dataUrl = canvas.toDataURL(); const newHistory = history.slice(0, historyIndex + 1); newHistory.push(dataUrl); setHistory(newHistory); setHistoryIndex(newHistory.length - 1); }; const restoreCanvasState = (index: number) => { if (!canvasRef.current || !history[index]) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; const dataUrl = history[index]; const img = new window.Image(); img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); }; img.src = dataUrl; }; const handleUndo = () => { if (historyIndex > 0) { const newIndex = historyIndex - 1; setHistoryIndex(newIndex); restoreCanvasState(newIndex); } }; const handleRedo = () => { if (historyIndex < history.length - 1) { const newIndex = historyIndex + 1; setHistoryIndex(newIndex); restoreCanvasState(newIndex); } }; const getCoordinates = (e: React.MouseEvent | React.TouchEvent) => { const canvas = canvasRef.current; if (!canvas) return { x: 0, y: 0 }; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; let clientX, clientY; if ('touches' in e.nativeEvent) { clientX = e.nativeEvent.touches[0].clientX; clientY = e.nativeEvent.touches[0].clientY; } else { clientX = e.nativeEvent.clientX; clientY = e.nativeEvent.clientY; } return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY, }; }; const startDrawing = (e: React.MouseEvent | React.TouchEvent) => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const {x, y} = getCoordinates(e); if ('touches' in e.nativeEvent) { e.preventDefault(); } ctx.beginPath(); ctx.moveTo(x, y); setIsDrawing(true); }; const draw = (e: React.MouseEvent | React.TouchEvent) => { if (!isDrawing) return; if ('touches' in e.nativeEvent) { e.preventDefault(); } const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const {x, y} = getCoordinates(e); ctx.lineWidth = brushSize; ctx.lineCap = 'round'; ctx.strokeStyle = isErasing ? '#FFFFFF' : brushColor; ctx.lineTo(x, y); ctx.stroke(); }; const stopDrawing = () => { if (!isDrawing) return; setIsDrawing(false); saveCanvasState(); }; const handleClear = () => { if (canvasRef.current) { initializeCanvas(); const dataUrl = canvasRef.current.toDataURL(); setHistory([dataUrl]); setHistoryIndex(0); } setGeneratedImage(null); backgroundImageRef.current = null; setPrompt(''); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!apiKey) { setShowApiKeyModal(true); return; } if (isSubmitting) return; setIsSubmitting(true); await performGeneration(); setIsSubmitting(false); }; const handleApiKeySubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!tempApiKey) { setErrorMessage("Please enter an API key."); setErrorTitle("API Key Required"); setShowErrorModal(true); return; } setApiKey(tempApiKey); setShowApiKeyModal(false); setTimeout(() => { if (isSubmitting) return; setIsSubmitting(true); performGeneration(tempApiKey).finally(() => setIsSubmitting(false)); }, 0); }; const performGeneration = async (currentApiKey?: string) => { const keyToUse = currentApiKey || apiKey; if (!keyToUse) { setShowApiKeyModal(true); return; } setIsLoading(true); setErrorMessage(''); setErrorTitle('Failed to generate'); try { const ai = new GoogleGenAI({apiKey: keyToUse}); if (!canvasRef.current) throw new Error("Canvas is not available."); const canvas = canvasRef.current; const imageB64 = canvas.toDataURL('image/png').split(',')[1]; const parts = [ {inlineData: {data: imageB64, mimeType: 'image/png'}}, {text: prompt}, ]; const response: GenerateContentResponse = await ai.models.generateContent({ model: selectedModel, contents: [{parts}], config: { imageConfig: { aspectRatio: '16:9', }, }, }); let newImageData: string | null = null; for (const part of response.candidates[0].content.parts) { if (part.inlineData) { newImageData = part.inlineData.data; break; } } if (newImageData) { const imageUrl = `data:image/png;base64,${newImageData}`; setGeneratedImage(imageUrl); } else { setErrorMessage( 'Failed to generate image from the response. Please try again.', ); setShowErrorModal(true); } } catch (error: any) { console.error('Error submitting:', error); let message = error.message || 'An unexpected error occurred. Check the console for details.'; if ( error.status === 403 || error.code === 403 || message.includes('403') || message.includes('PERMISSION_DENIED') ) { setErrorTitle('Permission Denied'); message = selectedModel.includes('pro') ? 'The Gemini 3 Pro Image model may require a paid API key from a Google Cloud Project with billing enabled. Please switch to "Gemini 2.5 Flash" or provide a valid key.' : 'The API key provided does not have permission to access the Gemini API. Please ensure your key is valid and has the necessary permissions.'; } setErrorMessage(message); setShowErrorModal(true); } finally { setIsLoading(false); } }; const closeErrorModal = () => setShowErrorModal(false); const closeInfoModal = () => setShowInfoModal(false); const toggleBrushMenu = () => { setShowBrushMenu(!showBrushMenu); setShowColorMenu(false); setShowModelMenu(false); }; const toggleColorMenu = () => { setShowColorMenu(!showColorMenu); setShowBrushMenu(false); setShowModelMenu(false); }; const toggleModelMenu = () => { setShowModelMenu(!showModelMenu); setShowBrushMenu(false); setShowColorMenu(false); }; const toggleEraser = () => { setIsErasing(!isErasing); if (!isErasing) { setShowBrushMenu(false); setShowColorMenu(false); setShowModelMenu(false); } }; const presetColors = ['#000000', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#00FFFF', '#FF00FF', '#808080', '#A52A2A']; useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const preventTouchDefault = (e: TouchEvent) => { if (isDrawing) e.preventDefault(); }; canvas.addEventListener('touchstart', preventTouchDefault, {passive: false}); canvas.addEventListener('touchmove', preventTouchDefault, {passive: false}); return () => { canvas.removeEventListener('touchstart', preventTouchDefault); canvas.removeEventListener('touchmove', preventTouchDefault); }; }, [isDrawing]); return ( <>
{showBrushMenu && (
setBrushSize(Number(e.target.value))} className="w-full" />
)}
{showColorMenu && (
{presetColors.map((color) => (
Custom: { setBrushColor(e.target.value); setIsErasing(false); }} className="h-8 flex-1 cursor-pointer bg-transparent" />
)}
{showModelMenu && (
)}
setPrompt(e.target.value)} placeholder="Describe what to generate or how to edit the drawing..." className="prompt-input" required />
{showInfoModal && (

Information

Nano-banana-pro-sketch-board is a web-based app where you can draw or sketch anything and transform it into your desired style. The Gemini API key you provide is used only on your device and will be removed when the page is closed or refreshed. The key will not be exposed anywhere.

About: This app is powered by the Gemini 2.5 Flash Image / Gemini 3 Pro Preview Image model through the Gemini API and built by{' '} Prithiv Sakthi.

)} {showErrorModal && (

{errorTitle}

{errorMessage}

)} {showApiKeyModal && (

Enter Your Gemini API Key

Your API key is only stored for this session and will be lost when you reload or exit the page. It is not shared or exposed anywhere.

setTempApiKey(e.target.value)} className="custom-input !w-full" placeholder="Enter your Gemini API Key" required /> {/* FIXED BUTTON STYLING */}
)}
); }