Trudy's picture
cycled API key + shortened docker files
e1a68dd
import { useState, useRef, useEffect } from "react";
import { SendHorizontal, LoaderCircle, Trash2, X } from "lucide-react";
import Head from "next/head";
export default function Home() {
const canvasRef = useRef(null);
const backgroundImageRef = useRef(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("");
const [customApiKey, setCustomApiKey] = useState("");
// Load background image when generatedImage changes
useEffect(() => {
if (generatedImage && canvasRef.current) {
// Use the window.Image constructor to avoid conflict with Next.js Image component
const img = new window.Image();
img.onload = () => {
backgroundImageRef.current = img;
drawImageToCanvas();
};
img.src = generatedImage;
}
}, [generatedImage]);
// Initialize canvas with white background when component mounts
useEffect(() => {
if (canvasRef.current) {
initializeCanvas();
}
}, []);
// Initialize canvas with white background
const initializeCanvas = () => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
// Fill canvas with white background
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
// Draw the background image to the canvas
const drawImageToCanvas = () => {
if (!canvasRef.current || !backgroundImageRef.current) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
// Fill with white background first
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw the background image
ctx.drawImage(
backgroundImageRef.current,
0, 0,
canvas.width, canvas.height
);
};
// Get the correct coordinates based on canvas scaling
const getCoordinates = (e) => {
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
// Calculate the scaling factor between the internal canvas size and displayed size
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
// Apply the scaling to get accurate coordinates
return {
x: (e.nativeEvent.offsetX || (e.nativeEvent.touches?.[0]?.clientX - rect.left)) * scaleX,
y: (e.nativeEvent.offsetY || (e.nativeEvent.touches?.[0]?.clientY - rect.top)) * scaleY
};
};
const startDrawing = (e) => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const { x, y } = getCoordinates(e);
// Prevent default behavior to avoid scrolling on touch devices
if (e.type === 'touchstart') {
e.preventDefault();
}
// Start a new path without clearing the canvas
ctx.beginPath();
ctx.moveTo(x, y);
setIsDrawing(true);
};
const draw = (e) => {
if (!isDrawing) return;
// Prevent default behavior to avoid scrolling on touch devices
if (e.type === 'touchmove') {
e.preventDefault();
}
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const { x, y } = getCoordinates(e);
ctx.lineWidth = 5;
ctx.lineCap = "round";
ctx.strokeStyle = penColor;
ctx.lineTo(x, y);
ctx.stroke();
};
const stopDrawing = () => {
setIsDrawing(false);
};
const clearCanvas = () => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
// Fill with white instead of just clearing
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, canvas.width, canvas.height);
setGeneratedImage(null);
backgroundImageRef.current = null;
};
const handleColorChange = (e) => {
setPenColor(e.target.value);
};
const openColorPicker = () => {
if (colorInputRef.current) {
colorInputRef.current.click();
}
};
const handleKeyDown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
openColorPicker();
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!canvasRef.current) return;
setIsLoading(true);
try {
// Get the drawing as base64 data
const canvas = canvasRef.current;
// Create a temporary canvas to add white background
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
const tempCtx = tempCanvas.getContext('2d');
// Fill with white background
tempCtx.fillStyle = '#FFFFFF';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// Draw the original canvas content on top of the white background
tempCtx.drawImage(canvas, 0, 0);
const drawingData = tempCanvas.toDataURL("image/png").split(",")[1];
// Create request payload
const requestPayload = {
prompt,
drawingData,
customApiKey // Add the custom API key to the payload if it exists
};
// Log the request payload (without the full image data for brevity)
console.log("Request payload:", {
...requestPayload,
drawingData: drawingData ? `${drawingData.substring(0, 50)}... (truncated)` : null,
customApiKey: customApiKey ? "**********" : null
});
// Send the drawing and prompt to the API
const response = await fetch("/api/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestPayload),
});
const data = await response.json();
// Log the response (without the full image data for brevity)
console.log("Response:", {
...data,
imageData: data.imageData ? `${data.imageData.substring(0, 50)}... (truncated)` : null
});
if (data.success && data.imageData) {
const imageUrl = `data:image/png;base64,${data.imageData}`;
setGeneratedImage(imageUrl);
} else {
console.error("Failed to generate image:", data.error);
// Check if the error is related to quota exhaustion or other API errors
if (data.error && (
data.error.includes("Resource has been exhausted") ||
data.error.includes("quota") ||
response.status === 429 ||
response.status === 500
)) {
setErrorMessage(data.error);
setShowErrorModal(true);
} else {
alert("Failed to generate image. Please try again.");
}
}
} catch (error) {
console.error("Error submitting drawing:", error);
setErrorMessage(error.message || "An unexpected error occurred.");
setShowErrorModal(true);
} finally {
setIsLoading(false);
}
};
// Close the error modal
const closeErrorModal = () => {
setShowErrorModal(false);
};
// Handle the custom API key submission
const handleApiKeySubmit = (e) => {
e.preventDefault();
setShowErrorModal(false);
// Will use the customApiKey state in the next API call
};
// Add touch event prevention function
useEffect(() => {
// Function to prevent default touch behavior on canvas
const preventTouchDefault = (e) => {
if (isDrawing) {
e.preventDefault();
}
};
// Add event listener when component mounts
const canvas = canvasRef.current;
if (canvas) {
canvas.addEventListener('touchstart', preventTouchDefault, { passive: false });
canvas.addEventListener('touchmove', preventTouchDefault, { passive: false });
}
// Remove event listener when component unmounts
return () => {
if (canvas) {
canvas.removeEventListener('touchstart', preventTouchDefault);
canvas.removeEventListener('touchmove', preventTouchDefault);
}
};
}, [isDrawing]);
return (
<>
<Head>
<title>Gemini Co-Drawing</title>
<meta name="description" content="Gemini Co-Drawing" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="min-h-screen notebook-paper-bg 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">
{/* Header section with title and tools */}
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-end mb-2 sm:mb-6 gap-2">
<div>
<h1 className="text-2xl sm:text-3xl font-bold mb-0 leading-tight font-mega">Gemini Co-Drawing</h1>
<p className="text-sm sm:text-base text-gray-500 mt-1">
Built with{" "}
<a className="underline" href="https://ai.google.dev/gemini-api/docs/image-generation" target="_blank" rel="noopener noreferrer">
Gemini 2.0 native image generation
</a>
</p>
<p className="text-sm sm:text-base text-gray-500 mt-1">
by{" "}
<a className="underline" href="https://x.com/trudypainter" target="_blank" rel="noopener noreferrer">
@trudypainter
</a>
{" "}and{" "}
<a className="underline" href="https://x.com/alexanderchen" target="_blank" rel="noopener noreferrer">
@alexanderchen
</a>
</p>
</div>
<menu className="flex items-center bg-gray-300 rounded-full p-2 shadow-sm self-start sm:self-auto">
<button
type="button"
className="w-10 h-10 rounded-full overflow-hidden mr-2 flex items-center justify-center border-2 border-white shadow-sm transition-transform hover:scale-110"
onClick={openColorPicker}
onKeyDown={handleKeyDown}
aria-label="Open color picker"
style={{ backgroundColor: penColor }}
>
<input
ref={colorInputRef}
type="color"
value={penColor}
onChange={handleColorChange}
className="opacity-0 absolute w-px h-px"
aria-label="Select pen color"
/>
</button>
<button
type="button"
onClick={clearCanvas}
className="w-10 h-10 rounded-full flex items-center justify-center bg-white shadow-sm transition-all hover:bg-gray-50 hover:scale-110"
>
<Trash2 className="w-5 h-5 text-gray-700" aria-label="Clear Canvas" />
</button>
</menu>
</div>
{/* Canvas section with notebook paper background */}
<div className="w-full mb-6">
<canvas
ref={canvasRef}
width={960}
height={540}
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
onTouchStart={startDrawing}
onTouchMove={draw}
onTouchEnd={stopDrawing}
className="border-2 border-black w-full hover:cursor-crosshair sm:h-[60vh]
h-[30vh] min-h-[320px] bg-white/90 touch-none"
/>
</div>
{/* Input form that matches canvas width */}
<form onSubmit={handleSubmit} className="w-full">
<div className="relative">
<input
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Add your change..."
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
/>
<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"
>
{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" aria-label="Submit" />
)}
</button>
</div>
</form>
</main>
{/* 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">Failed to generate</h3>
<button
onClick={closeErrorModal}
className="text-gray-400 hover:text-gray-500"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleApiKeySubmit} className="mb-4">
<label className="block text-sm font-medium text-gray-600 mb-2">
This space is pretty popular... add your own Gemini API key from <a
href="https://ai.google.dev/"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Google AI Studio
</a>:
</label>
<input
type="text"
value={customApiKey}
onChange={(e) => setCustomApiKey(e.target.value)}
placeholder="API Key..."
className="w-full p-3 border border-gray-300 rounded mb-4 font-mono text-sm"
required
/>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={closeErrorModal}
className="px-4 py-2 text-sm border border-gray-300 rounded hover:bg-gray-50"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 text-sm bg-black text-white rounded hover:bg-gray-800"
>
Use My API Key
</button>
</div>
</form>
</div>
</div>
)}
</div>
</>
);
}