import { useState, useRef, useEffect, useCallback } from "react"; import Canvas from "./Canvas"; import DisplayCanvas from "./DisplayCanvas"; import ToolBar from "./ToolBar"; import StyleSelector from "./StyleSelector"; import { getPromptForStyle, styleOptions, addMaterialToLibrary } from "./StyleSelector"; import ActionBar from "./ActionBar"; import ErrorModal from "./ErrorModal"; import TextInput from "./TextInput"; import Header from "./Header"; import DimensionSelector from "./DimensionSelector"; import HistoryModal from "./HistoryModal"; import BottomToolBar from "./BottomToolBar"; import LibraryPage from "./LibraryPage"; import { getCoordinates, initializeCanvas, drawImageToCanvas, drawBezierCurve, } from "./utils/canvasUtils"; import { toast } from "react-hot-toast"; import { Download, History as HistoryIcon, RefreshCw as RefreshIcon, Library as LibraryIcon, LoaderCircle } from "lucide-react"; import OutputOptionsBar from "./OutputOptionsBar"; import ApiKeyModal from "./ApiKeyModal"; import HeaderButtons from "./HeaderButtons"; const CanvasContainer = () => { // Check if the device is mobile based on screen width const isMobileDevice = () => { if (typeof window !== 'undefined') { return window.innerWidth < 768; // Common breakpoint for mobile devices } return false; // Default to desktop on server-side }; // Get default dimensions based on device type const getDefaultDimension = () => { if (isMobileDevice()) { // Square (1:1) for mobile return { id: "square", label: "1:1", width: 1000, height: 1000, }; } else { // Landscape (3:2) for desktop return { id: "landscape", label: "3:2", width: 1500, height: 1000, }; } }; const canvasRef = useRef(null); const canvasComponentRef = useRef(null); const displayCanvasRef = useRef(null); const backgroundImageRef = useRef(null); const [currentDimension, setCurrentDimension] = useState(getDefaultDimension()); const [isDrawing, setIsDrawing] = useState(false); const [penColor, setPenColor] = useState("#000000"); const [penWidth, setPenWidth] = useState(2); 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(""); const [debugMode, setDebugMode] = useState(false); const [styleMode, setStyleMode] = useState("material"); const [strokeCount, setStrokeCount] = useState(0); const strokeTimeoutRef = useRef(null); const [lastRequestTime, setLastRequestTime] = useState(0); const MIN_REQUEST_INTERVAL = 2000; // Minimum 2 seconds between requests const [currentTool, setCurrentTool] = useState("pencil"); // 'pencil', 'pen', 'eraser', 'text', 'rect', 'circle', 'line', 'star' const [isTyping, setIsTyping] = useState(false); const [undoStack, setUndoStack] = useState([]); const [bezierPoints, setBezierPoints] = useState([]); const [textInput, setTextInput] = useState(""); const [textPosition, setTextPosition] = useState({ x: 0, y: 0 }); const textInputRef = useRef(null); const [isPenDrawing, setIsPenDrawing] = useState(false); const [currentBezierPath, setCurrentBezierPath] = useState([]); const [tempPoints, setTempPoints] = useState([]); const [hasGeneratedContent, setHasGeneratedContent] = useState(false); const [imageHistory, setImageHistory] = useState([]); const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false); const [hasDrawing, setHasDrawing] = useState(false); // Add a ref to track style changes that need regeneration const needsRegenerationRef = useRef(false); // Add a ref to track if regeneration was manually triggered const isManualRegenerationRef = useRef(false); const [isSendingToDoodle, setIsSendingToDoodle] = useState(false); // Add state for API key modal const [showApiKeyModal, setShowApiKeyModal] = useState(false); const [showLibrary, setShowLibrary] = useState(false); // Add state for template loading const [isTemplateLoading, setIsTemplateLoading] = useState(false); const [templateLoadingMessage, setTemplateLoadingMessage] = useState(""); // Load saved API key from localStorage on component mount useEffect(() => { const savedApiKey = localStorage.getItem("geminiApiKey"); if (savedApiKey) { setCustomApiKey(savedApiKey); // Validate the API key silently validateApiKey(savedApiKey); } // Check if debug mode is enabled in localStorage or URL const debugParam = new URLSearchParams(window.location.search).get('debug'); // Only look at localStorage if debug parameter is not explicitly set to false const savedDebug = debugParam !== "false" && localStorage.getItem("debugMode") === "true"; if (debugParam === "true" || savedDebug) { // Set debug mode to true AND show error modal setDebugMode(true); setShowErrorModal(true); } else { // Ensure debug mode is OFF by default setDebugMode(false); // Also clean up any stale localStorage value if (localStorage.getItem("debugMode") === "true") { localStorage.setItem("debugMode", "false"); } } }, []); // Add effect to save debug mode to localStorage useEffect(() => { // Always save the current state to localStorage localStorage.setItem("debugMode", debugMode.toString()); // ONLY auto-show error modal when debug mode is enabled if (debugMode === true) { setShowErrorModal(true); } }, [debugMode]); // Add a function to validate the API key const validateApiKey = async (apiKey) => { if (!apiKey) return; try { const response = await fetch("/api/validate-key", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ apiKey }), }); const data = await response.json(); if (!data.valid) { console.warn("Invalid API key detected, will be cleared"); // Clear the invalid key localStorage.removeItem("geminiApiKey"); setCustomApiKey(""); // Don't show error to user for now - they'll see it when trying to use the app } } catch (error) { console.error("Error validating API key:", error); // Don't clear the key on connection errors } }; // 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(canvasRef.current, backgroundImageRef.current); }; img.src = generatedImage; } }, [generatedImage]); // Initialize canvas with white background when component mounts useEffect(() => { if (canvasRef.current) { initializeCanvas(canvasRef.current); } // Also initialize the display canvas if (displayCanvasRef.current) { const displayCtx = displayCanvasRef.current.getContext("2d"); displayCtx.fillStyle = "#FFFFFF"; displayCtx.fillRect( 0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height ); } }, []); // Add resize listener to update dimensions when switching between mobile and desktop useEffect(() => { let isMobile = isMobileDevice(); const handleResize = () => { const newIsMobile = isMobileDevice(); // Only update dimensions if the device type changed (mobile <-> desktop) if (newIsMobile !== isMobile) { isMobile = newIsMobile; // Only update dimensions if the canvas is empty (no drawing) if (canvasRef.current && !hasDrawing && !hasGeneratedContent) { setCurrentDimension(getDefaultDimension()); } } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [hasDrawing, hasGeneratedContent]); // Add an effect to sync canvas dimensions when they change useEffect(() => { if (canvasRef.current && displayCanvasRef.current) { // Ensure both canvases have the same dimensions canvasRef.current.width = currentDimension.width; canvasRef.current.height = currentDimension.height; displayCanvasRef.current.width = currentDimension.width; displayCanvasRef.current.height = currentDimension.height; // Initialize both canvases with white backgrounds initializeCanvas(canvasRef.current); const displayCtx = displayCanvasRef.current.getContext("2d"); displayCtx.fillStyle = "#FFFFFF"; displayCtx.fillRect( 0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height ); } }, [currentDimension]); const startDrawing = (e) => { const { x, y } = getCoordinates(e, canvasRef.current); if (e.type === "touchstart") { e.preventDefault(); } console.log("startDrawing called", { currentTool, x, y }); const ctx = canvasRef.current.getContext("2d"); // Set up the line style at the start of drawing ctx.lineWidth = currentTool === "eraser" ? 20 : penWidth; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.strokeStyle = currentTool === "eraser" ? "#FFFFFF" : penColor; ctx.beginPath(); ctx.moveTo(x, y); setIsDrawing(true); setStrokeCount((prev) => prev + 1); // Save canvas state before drawing saveCanvasState(); }; const draw = (e) => { if (!isDrawing) return; const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); const { x, y } = getCoordinates(e, canvas); // Occasionally log drawing activity if (Math.random() < 0.05) { // Only log ~5% of move events to avoid console spam console.log("draw called", { currentTool, isDrawing, x, y }); } // Set up the line style before drawing ctx.lineWidth = currentTool === "eraser" ? 60 : penWidth * 4; // Pen width now 4x original size ctx.lineCap = "round"; ctx.lineJoin = "round"; if (currentTool === "eraser") { ctx.strokeStyle = "#FFFFFF"; } else { ctx.strokeStyle = penColor; } if (currentTool === "pen") { // Show preview line while moving if (tempPoints.length > 0) { const lastPoint = tempPoints[tempPoints.length - 1]; ctx.beginPath(); ctx.moveTo(lastPoint.x, lastPoint.y); ctx.lineTo(x, y); ctx.stroke(); } } else { ctx.lineTo(x, y); ctx.stroke(); } }; const stopDrawing = async (e) => { console.log("stopDrawing called in CanvasContainer", { isDrawing, currentTool, hasEvent: !!e, eventType: e ? e.type : "none", }); if (!isDrawing) return; setIsDrawing(false); // Remove the timeout-based generation if (strokeTimeoutRef.current) { clearTimeout(strokeTimeoutRef.current); strokeTimeoutRef.current = null; } // The Canvas component will handle generation for pen and pencil tools directly // This function now primarily handles stroke counting for other tools // Only generate on mouse/touch up events when not using the pen or pencil tool // (since those are handled by the Canvas component) if ( e && (e.type === "mouseup" || e.type === "touchend") && currentTool !== "pen" && currentTool !== "pencil" ) { console.log("stopDrawing: detected mouseup/touchend event", { strokeCount, }); // Check if we have enough strokes to generate (increased to 10 from 3) if (strokeCount >= 10) { console.log( "stopDrawing: calling handleGeneration due to stroke count" ); await handleGeneration(); setStrokeCount(0); } } }; const clearCanvas = () => { // If we have a ref to our Canvas component, use its custom clear method if (canvasComponentRef.current?.handleClearCanvas) { canvasComponentRef.current.handleClearCanvas(); return; } // Fallback to original implementation const canvas = canvasRef.current; if (!canvas) return; initializeCanvas(canvas); setGeneratedImage(null); backgroundImageRef.current = null; // Also clear the display canvas and reset generated content flag if (displayCanvasRef.current) { const displayCtx = displayCanvasRef.current.getContext("2d"); displayCtx.clearRect( 0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height ); displayCtx.fillStyle = "#FFFFFF"; displayCtx.fillRect( 0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height ); setHasGeneratedContent(false); } // Save empty canvas state saveCanvasState(); }; const handleGeneration = useCallback( async (isManualRegeneration = false) => { console.log("handleGeneration called", { isManualRegeneration }); // Set our ref if this is a manual regeneration if (isManualRegeneration) { isManualRegenerationRef.current = true; } // Remove the time throttling for automatic generation after doodle conversion // but keep it for manual generations const isAutoGeneration = !lastRequestTime && !isManualRegeneration; if (!isAutoGeneration) { const now = Date.now(); if (now - lastRequestTime < MIN_REQUEST_INTERVAL) { console.log("Request throttled - too soon after last request"); return; } setLastRequestTime(now); } if (!canvasRef.current) return; console.log("Starting generation process"); // Check if we're already in a loading state before setting it if (!isLoading) { setIsLoading(true); } try { const canvas = canvasRef.current; const tempCanvas = document.createElement("canvas"); tempCanvas.width = canvas.width; tempCanvas.height = canvas.height; const tempCtx = tempCanvas.getContext("2d"); tempCtx.fillStyle = "#FFFFFF"; tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); tempCtx.drawImage(canvas, 0, 0); const drawingData = tempCanvas.toDataURL("image/png").split(",")[1]; const materialPrompt = getPromptForStyle(styleMode); const requestPayload = { prompt: materialPrompt, drawingData, customApiKey, }; console.log("Making API request with style:", styleMode); console.log(`Using prompt: ${materialPrompt.substring(0, 100)}...`); const response = await fetch("/api/generate", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(requestPayload), }); console.log("API response received, status:", response.status); const data = await response.json(); if (data.success && data.imageData) { console.log("Image generated successfully"); const imageUrl = `data:image/png;base64,${data.imageData}`; // Draw the generated image to the display canvas const displayCanvas = displayCanvasRef.current; if (!displayCanvas) { console.error("Display canvas ref is null"); return; } const displayCtx = displayCanvas.getContext("2d"); // Clear the display canvas first displayCtx.clearRect(0, 0, displayCanvas.width, displayCanvas.height); displayCtx.fillStyle = "#FFFFFF"; displayCtx.fillRect(0, 0, displayCanvas.width, displayCanvas.height); // Create and load the new image const img = new Image(); // Set up the onload handler before setting the src img.onload = () => { console.log("Generated image loaded, drawing to display canvas"); // Clear the canvas first displayCtx.clearRect( 0, 0, displayCanvas.width, displayCanvas.height ); // Fill with black background for letterboxing displayCtx.fillStyle = "#000000"; displayCtx.fillRect( 0, 0, displayCanvas.width, displayCanvas.height ); // Calculate aspect ratios const imgRatio = img.width / img.height; const canvasRatio = displayCanvas.width / displayCanvas.height; let drawWidth, drawHeight, x, y; if (imgRatio > canvasRatio) { // Image is wider than canvas (relative to height) drawWidth = displayCanvas.width; drawHeight = displayCanvas.width / imgRatio; x = 0; y = (displayCanvas.height - drawHeight) / 2; } else { // Image is taller than canvas (relative to width) drawHeight = displayCanvas.height; drawWidth = displayCanvas.height * imgRatio; x = (displayCanvas.width - drawWidth) / 2; y = 0; } // Draw the image with letterboxing displayCtx.drawImage(img, x, y, drawWidth, drawHeight); // Update our state to indicate we have generated content setHasGeneratedContent(true); // Add to history setImageHistory((prev) => [ ...prev, { imageUrl, timestamp: Date.now(), drawingData: canvas.toDataURL(), styleMode, dimensions: currentDimension, }, ]); }; // Set the src to trigger loading img.src = imageUrl; } else { console.error("Failed to generate image:", data.error); // When generation fails, ensure display canvas is cleared if (displayCanvasRef.current) { const displayCtx = displayCanvasRef.current.getContext("2d"); displayCtx.clearRect( 0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height ); displayCtx.fillStyle = "#FFFFFF"; displayCtx.fillRect( 0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height ); } // Make sure we mark that we don't have generated content setHasGeneratedContent(false); // Check for quota or API key errors if ( data.error && (data.error.includes("Resource has been exhausted") || data.error.includes("quota") || data.error.includes("exceeded") || response.status === 429) ) { // Show API key modal instead of error modal for quota issues setShowApiKeyModal(true); } else if (response.status === 500) { // Show regular error modal for other server errors setErrorMessage(data.error); setShowErrorModal(true); } } } catch (error) { console.error("Error generating image:", error); // Check for quota-related errors in the catch block too if ( error.message && (error.message.includes("Resource has been exhausted") || error.message.includes("quota") || error.message.includes("exceeded") || error.message.includes("429")) ) { // Show API key modal for quota issues setShowApiKeyModal(true); } else { // Show regular error modal for other errors setErrorMessage(error.message || "An unexpected error occurred."); setShowErrorModal(true); } // When generation errors, ensure display canvas is cleared if (displayCanvasRef.current) { const displayCtx = displayCanvasRef.current.getContext("2d"); displayCtx.clearRect( 0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height ); displayCtx.fillStyle = "#FFFFFF"; displayCtx.fillRect( 0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height ); } // Make sure we mark that we don't have generated content setHasGeneratedContent(false); } finally { setIsLoading(false); console.log("Generation process completed"); } }, [canvasRef, isLoading, styleMode, customApiKey, lastRequestTime] ); // Close the error modal const closeErrorModal = () => { setShowErrorModal(false); }; // Handle the custom API key submission const handleApiKeySubmit = (apiKey) => { setCustomApiKey(apiKey); // Save to localStorage for persistence localStorage.setItem("geminiApiKey", apiKey); // Close the API key modal setShowApiKeyModal(false); // Also close error modal if it was open setShowErrorModal(false); // Show confirmation toast toast.success("API key saved successfully"); }; // Add this function to handle undo const handleUndo = () => { if (undoStack.length > 0) { const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); const previousState = undoStack[undoStack.length - 2]; // Get second to last state if (previousState) { const img = new Image(); img.onload = () => { ctx.fillStyle = "#FFFFFF"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); }; img.src = previousState; } else { // If no previous state, clear to white ctx.fillStyle = "#FFFFFF"; ctx.fillRect(0, 0, canvas.width, canvas.height); } setUndoStack((prev) => prev.slice(0, -1)); } }; // Add this function to save canvas state const saveCanvasState = () => { const canvas = canvasRef.current; if (!canvas) return; const dataURL = canvas.toDataURL(); setUndoStack((prev) => [...prev, dataURL]); }; // Add this function to handle text input const handleTextInput = (e) => { if (e.key === "Enter") { const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); ctx.font = "24px Arial"; ctx.fillStyle = "#000000"; ctx.fillText(textInput, textPosition.x, textPosition.y); setTextInput(""); setIsTyping(false); saveCanvasState(); } }; // Modify the canvas click handler to handle text placement const handleCanvasClick = (e) => { if (currentTool === "text") { const { x, y } = getCoordinates(e, canvasRef.current); setTextPosition({ x, y }); setIsTyping(true); if (textInputRef.current) { textInputRef.current.focus(); } } }; // Handle pen click for bezier curve tool const handlePenClick = (e) => { if (currentTool !== "pen") return; // Note: Actual point creation is now handled in the Canvas component // This function is primarily used as a callback to inform the CanvasContainer // that a pen action happened console.log("handlePenClick called in CanvasContainer"); // Set isDrawing flag to true when using pen tool // This ensures handleStopDrawing knows we're in drawing mode with the pen setIsDrawing(true); // Save canvas state when adding new points saveCanvasState(); }; // Add this new function near your other utility functions const handleSaveImage = useCallback(() => { if (displayCanvasRef.current && hasGeneratedContent) { const canvas = displayCanvasRef.current; const link = document.createElement('a'); // Create timestamp in format: YYYYMMDD_HHMM const now = new Date(); const timestamp = now.toISOString() .replace(/[-:T]/g, '') // Remove all separators .slice(0, 12); // Keep only YYYYMMDDHHMM // Get the actual material name from styleOptions const materialName = styleOptions[styleMode]?.name || styleMode; // Create filename: timestamp_materialname.png const filename = `${timestamp}_${materialName}.png`; link.download = filename; link.href = canvas.toDataURL('image/png'); link.click(); toast.success(`Saved as "${filename}"`); } else { toast.error("No generated image to save."); } }, [displayCanvasRef, hasGeneratedContent, styleMode]); // Add this function to handle regeneration const handleRegenerate = async () => { if (canvasRef.current) { // Set flag to prevent useEffect hooks from triggering additional generations isManualRegenerationRef.current = true; await handleGeneration(true); } }; // Add useEffect to watch for styleMode changes and regenerate // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { // Skip if this was triggered by a manual regeneration if (isManualRegenerationRef.current) { console.log("Skipping automatic generation due to manual regeneration"); return; } // Only trigger if we have something drawn (check if canvas is not empty) // Note: handleGeneration is intentionally omitted from dependencies to prevent infinite loops const checkCanvasAndGenerate = async () => { if (!canvasRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // Check if canvas has any non-white pixels const hasDrawing = Array.from(imageData.data).some((pixel, index) => { // Check only RGB values (skip alpha) return index % 4 !== 3 && pixel !== 255; }); // Only generate if there's a drawing AND we don't already have generated content if (hasDrawing && !hasGeneratedContent) { await handleGeneration(); } else if (hasDrawing) { // Mark that regeneration is needed when style changes but we already have content needsRegenerationRef.current = true; } }; // Skip on first render if (styleMode) { checkCanvasAndGenerate(); } }, [styleMode, hasGeneratedContent]); // Removed handleGeneration from dependencies to prevent loop // Add new useEffect to handle regeneration when hasGeneratedContent changes to false // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { // Skip if this was triggered by a manual regeneration if (isManualRegenerationRef.current) { console.log("Skipping automatic generation due to manual regeneration"); // Reset the flag after the first render with it set isManualRegenerationRef.current = false; return; } // Note: handleGeneration is intentionally omitted from dependencies to prevent infinite loops // If we need regeneration and the generated content was cleared if (needsRegenerationRef.current && !hasGeneratedContent) { const checkDrawingAndRegenerate = async () => { if (!canvasRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // Check if canvas has any non-white pixels const hasDrawing = Array.from(imageData.data).some((pixel, index) => { // Check only RGB values (skip alpha) return index % 4 !== 3 && pixel !== 255; }); if (hasDrawing) { needsRegenerationRef.current = false; await handleGeneration(); } }; checkDrawingAndRegenerate(); } }, [hasGeneratedContent]); // Cleanup function - keep this to prevent memory leaks useEffect(() => { return () => { if (strokeTimeoutRef.current) { clearTimeout(strokeTimeoutRef.current); strokeTimeoutRef.current = null; } }; }, []); // Handle dimension change const handleDimensionChange = (newDimension) => { console.log("Changing dimensions to:", newDimension); // Clear both canvases if (canvasRef.current) { const canvas = canvasRef.current; canvas.width = newDimension.width; canvas.height = newDimension.height; initializeCanvas(canvas); } if (displayCanvasRef.current) { const displayCanvas = displayCanvasRef.current; displayCanvas.width = newDimension.width; displayCanvas.height = newDimension.height; const ctx = displayCanvas.getContext("2d"); ctx.fillStyle = "#FFFFFF"; ctx.fillRect(0, 0, displayCanvas.width, displayCanvas.height); } // Reset generation state setHasGeneratedContent(false); setGeneratedImage(null); backgroundImageRef.current = null; // Update dimension state AFTER canvas dimensions are updated setCurrentDimension(newDimension); }; // Add new function to handle selecting a historical image const handleSelectHistoricalImage = (historyItem) => { // First set the dimensions and wait for canvases to update if (historyItem.dimensions) { // Update canvas dimensions first if (canvasRef.current) { canvasRef.current.width = historyItem.dimensions.width; canvasRef.current.height = historyItem.dimensions.height; } if (displayCanvasRef.current) { displayCanvasRef.current.width = historyItem.dimensions.width; displayCanvasRef.current.height = historyItem.dimensions.height; } // Then update the dimension state setCurrentDimension(historyItem.dimensions); } // Use Promise to ensure images are loaded after dimensions are set Promise.resolve().then(() => { // Draw the original drawing to the canvas const drawingImg = new Image(); drawingImg.onload = () => { const canvas = canvasRef.current; if (canvas) { const ctx = canvas.getContext("2d"); ctx.fillStyle = "#FFFFFF"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(drawingImg, 0, 0, canvas.width, canvas.height); } }; drawingImg.src = historyItem.drawingData; // Draw the generated image to the display canvas const generatedImg = new Image(); generatedImg.onload = () => { const displayCanvas = displayCanvasRef.current; if (displayCanvas) { const ctx = displayCanvas.getContext("2d"); ctx.fillStyle = "#000000"; // Black background for letterboxing ctx.fillRect(0, 0, displayCanvas.width, displayCanvas.height); // Calculate aspect ratios const imgRatio = generatedImg.width / generatedImg.height; const canvasRatio = displayCanvas.width / displayCanvas.height; let drawWidth, drawHeight, x, y; if (imgRatio > canvasRatio) { // Image is wider than canvas drawWidth = displayCanvas.width; drawHeight = displayCanvas.width / imgRatio; x = 0; y = (displayCanvas.height - drawHeight) / 2; } else { // Image is taller than canvas drawHeight = displayCanvas.height; drawWidth = displayCanvas.height * imgRatio; x = (displayCanvas.width - drawWidth) / 2; y = 0; } // Draw the image with letterboxing ctx.drawImage(generatedImg, x, y, drawWidth, drawHeight); setHasGeneratedContent(true); } }; generatedImg.src = historyItem.imageUrl; }); // Close the history modal setIsHistoryModalOpen(false); }; // Add new function to handle image refinement const handleImageRefinement = async (refinementPrompt) => { if (!displayCanvasRef.current || !hasGeneratedContent) return; console.log("Starting image refinement with prompt:", refinementPrompt); setIsLoading(true); try { // Get the current image data const displayCanvas = displayCanvasRef.current; const imageData = displayCanvas.toDataURL("image/png").split(",")[1]; const requestPayload = { prompt: refinementPrompt, imageData, customApiKey, }; console.log("Making refinement API request"); const response = await fetch("/api/refine", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(requestPayload), }); console.log("Refinement API response received, status:", response.status); const data = await response.json(); if (data.success && data.imageData) { console.log("Image refined successfully"); const imageUrl = `data:image/png;base64,${data.imageData}`; // Draw the refined image to the display canvas const displayCtx = displayCanvas.getContext("2d"); const img = new Image(); img.onload = () => { console.log("Refined image loaded, drawing to display canvas"); // Clear the canvas displayCtx.clearRect(0, 0, displayCanvas.width, displayCanvas.height); // Fill with black background for letterboxing displayCtx.fillStyle = "#000000"; displayCtx.fillRect(0, 0, displayCanvas.width, displayCanvas.height); // Calculate aspect ratios const imgRatio = img.width / img.height; const canvasRatio = displayCanvas.width / displayCanvas.height; let drawWidth, drawHeight, x, y; if (imgRatio > canvasRatio) { // Image is wider than canvas (relative to height) drawWidth = displayCanvas.width; drawHeight = displayCanvas.width / imgRatio; x = 0; y = (displayCanvas.height - drawHeight) / 2; } else { // Image is taller than canvas (relative to width) drawHeight = displayCanvas.height; drawWidth = displayCanvas.height * imgRatio; x = (displayCanvas.width - drawWidth) / 2; y = 0; } // Draw the image with letterboxing displayCtx.drawImage(img, x, y, drawWidth, drawHeight); // Add to history setImageHistory((prev) => [ ...prev, { imageUrl, timestamp: Date.now(), drawingData: canvasRef.current.toDataURL(), styleMode, dimensions: currentDimension, }, ]); }; img.src = imageUrl; } else { console.error("Failed to refine image:", data.error); // Check for quota or API key errors if ( data.error && (data.error.includes("Resource has been exhausted") || data.error.includes("quota") || data.error.includes("exceeded") || response.status === 429) ) { // Show API key modal instead of error modal for quota issues setShowApiKeyModal(true); } else { // Show regular error modal for other errors setErrorMessage(data.error || "Failed to refine image. Please try again."); setShowErrorModal(true); } } } catch (error) { console.error("Error during refinement:", error); // Check for quota-related errors in the catch block if ( error.message && (error.message.includes("Resource has been exhausted") || error.message.includes("quota") || error.message.includes("exceeded") || error.message.includes("429")) ) { // Show API key modal for quota issues setShowApiKeyModal(true); } else { // Show regular error modal for other errors setErrorMessage("An error occurred during refinement. Please try again."); setShowErrorModal(true); } } finally { setIsLoading(false); } }; // Add onImageUpload function const handleImageUpload = (imageDataUrl) => { if (!canvasRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); const img = new Image(); img.onload = () => { // Clear the canvas ctx.fillStyle = "#FFFFFF"; ctx.fillRect(0, 0, canvas.width, canvas.height); // Calculate dimensions to maintain aspect ratio and fit within canvas const scale = Math.min( canvas.width / img.width, canvas.height / img.height ); const x = (canvas.width - img.width * scale) / 2; const y = (canvas.height - img.height * scale) / 2; // Draw the image centered and scaled ctx.drawImage(img, x, y, img.width * scale, img.height * scale); // Save canvas state after uploading image saveCanvasState(); setHasGeneratedContent(true); }; img.src = imageDataUrl; }; // Add stroke width handler const handleStrokeWidth = (width) => { setPenWidth(width); }; // Function to handle sending the generated image back to the doodle canvas const handleSendToDoodle = useCallback( async (imageDataUrl) => { if (!imageDataUrl || isSendingToDoodle) return; console.log("Sending image back to doodle canvas..."); setIsSendingToDoodle(true); let response; // Define response outside try try { const base64Data = imageDataUrl.split(",")[1]; response = await fetch("/api/convert-to-doodle", { // Assign to outer scope variable method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ imageData: base64Data, customApiKey // Pass the custom API key }), }); // Check for non-OK HTTP status first if (!response.ok) { let errorBody = await response.text(); // Get raw text first let errorMessage = `API Error: ${response.status}`; try { // Try parsing error response as JSON const errorData = JSON.parse(errorBody); errorMessage = errorData.error || errorMessage; } catch (parseError) { // If response wasn't JSON (like the "Body exceeded" error) console.error("API response was not valid JSON:", errorBody); // Use truncated raw text in the error message errorMessage = `${errorMessage}. Response: ${errorBody.substring( 0, 100 )}${errorBody.length > 100 ? "..." : ""}`; } // Check if this is a quota error if ( errorMessage.includes("quota") || errorMessage.includes("exceeded") || errorMessage.includes("Resource has been exhausted") || response.status === 429 ) { // Show API key modal for quota issues setShowApiKeyModal(true); setIsSendingToDoodle(false); return; } throw new Error(errorMessage); // Throw error to be caught below } // If response.ok, proceed to parse the JSON body const result = await response.json(); if (result.success && result.imageData) { const mainCtx = canvasRef.current?.getContext("2d"); if (mainCtx && canvasRef.current) { const img = new Image(); img.onload = () => { // Clear canvas without triggering state updates mainCtx.fillStyle = '#FFFFFF'; mainCtx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height); // Draw the new image mainCtx.drawImage( img, 0, 0, canvasRef.current.width, canvasRef.current.height ); // Batch our state updates Promise.resolve().then(() => { setTempPoints([]); if (canvasRef.current.setHasDrawing) { canvasRef.current.setHasDrawing(true); } // Save canvas state after all state updates are complete requestAnimationFrame(() => { saveCanvasState(); toast.success("Image sent back to doodle canvas!"); setIsSendingToDoodle(false); }); }); }; img.onerror = (err) => { console.error("Error loading converted doodle image:", err); toast.error("Failed to load the converted doodle."); setIsSendingToDoodle(false); // Turn off loading on image load error }; img.src = `data:image/png;base64,${result.imageData}`; } else { throw new Error("Main canvas context not available."); } } else { // Handle cases where API returns success: false or missing imageData throw new Error( result.error || "API returned success:false or missing data." ); } } catch (error) { // This catches errors from fetch, response.ok check, response.json(), or explicit throws console.error("Error sending image back to doodle:", error); // Check for quota errors in catch block if ( error.message && (error.message.includes("quota") || error.message.includes("exceeded") || error.message.includes("Resource has been exhausted") || error.message.includes("429")) ) { // Show API key modal for quota issues setShowApiKeyModal(true); } else { toast.error(`Error: ${error.message || "An unknown error occurred."}`); } // Ensure loading state is turned off in *any* error scenario setIsSendingToDoodle(false); } }, [isSendingToDoodle, clearCanvas, saveCanvasState, setTempPoints, toast, customApiKey] ); // Function to open history modal const openHistoryModal = () => { setIsHistoryModalOpen(true); }; // Updated function for library button const toggleLibrary = () => { setShowLibrary(prev => !prev); }; // Calculate if history exists const hasHistory = imageHistory && imageHistory.length > 0; // Add this helper function for image compression const compressImage = useCallback(async (dataUrl, maxWidth = 1200) => { return new Promise((resolve) => { const img = new Image(); img.onload = () => { // Create a canvas to resize the image const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // Calculate new dimensions let width = img.width; let height = img.height; if (width > maxWidth) { height = (height * maxWidth) / width; width = maxWidth; } canvas.width = width; canvas.height = height; // Draw and export as JPEG with lower quality ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, width, height); ctx.drawImage(img, 0, 0, width, height); resolve(canvas.toDataURL('image/jpeg', 0.85)); }; img.src = dataUrl; }); }, []); // Add this new function to handle using a library image as template const handleUseAsTemplate = useCallback(async (imageUrl) => { console.log('Using library image as template:', imageUrl); // Show loading state with specific messages for each step setTemplateLoadingMessage("Preparing template..."); setIsTemplateLoading(true); try { // 1. Create a material from the image // First, fetch the image and convert to base64 const response = await fetch(imageUrl); const blob = await response.blob(); // Convert blob to base64 const reader = new FileReader(); const imageDataPromise = new Promise((resolve) => { reader.onloadend = () => resolve(reader.result); reader.readAsDataURL(blob); }); const imageDataUrl = await imageDataPromise; // Process with visual-enhance-prompt API (compress the image first) setTemplateLoadingMessage("Analyzing image..."); const compressedImage = await compressImage(imageDataUrl, 1200); // Get custom API key if it exists const customApiKey = localStorage.getItem("geminiApiKey"); // Call the visual-enhance-prompt API const promptResponse = await fetch('/api/visual-enhance-prompt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: compressedImage, customApiKey, basePrompt: 'Transform this sketch into a material with professional studio lighting against a pure black background. Render it in Cinema 4D with Octane for a high-end 3D visualization.' }), }); if (!promptResponse.ok) { throw new Error(`API returned ${promptResponse.status}`); } const promptData = await promptResponse.json(); // 2. Add material to StyleSelector if (promptData.enhancedPrompt && promptData.suggestedName) { setTemplateLoadingMessage("Creating material..."); // Create material object - use a smaller compressed image for thumbnail const thumbnailImage = await compressImage(imageDataUrl, 300); const materialObj = { name: promptData.suggestedName, prompt: promptData.enhancedPrompt, image: thumbnailImage // Use compressed thumbnail }; // Add material to library and get the key const materialKey = addMaterialToLibrary(materialObj); // Select this new material setStyleMode(materialKey); // 3. Convert the library image to a doodle and render in Canvas setTemplateLoadingMessage("Converting to doodle..."); const doodleResponse = await fetch('/api/convert-to-doodle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageData: compressedImage.split(',')[1], customApiKey }), }); if (!doodleResponse.ok) { throw new Error(`Doodle conversion API returned ${doodleResponse.status}`); } const doodleData = await doodleResponse.json(); if (doodleData.success && doodleData.imageData) { // Render the doodle on the canvas const mainCtx = canvasRef.current?.getContext("2d"); if (mainCtx && canvasRef.current) { const img = new Image(); img.onload = () => { // Clear canvas mainCtx.fillStyle = '#FFFFFF'; mainCtx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height); // Calculate appropriate dimensions while maintaining aspect ratio // and respecting the current canvas dimensions const canvasWidth = canvasRef.current.width; const canvasHeight = canvasRef.current.height; const imgRatio = img.width / img.height; const canvasRatio = canvasWidth / canvasHeight; // Declare variables separately to fix linter warning let drawWidth = 0; let drawHeight = 0; let x = 0; let y = 0; if (imgRatio > canvasRatio) { // Image is wider relative to canvas drawWidth = canvasWidth * 0.8; drawHeight = drawWidth / imgRatio; x = canvasWidth * 0.1; y = (canvasHeight - drawHeight) / 2; } else { // Image is taller relative to canvas drawHeight = canvasHeight * 0.8; drawWidth = drawHeight * imgRatio; x = (canvasWidth - drawWidth) / 2; y = canvasHeight * 0.1; } // Draw doodle mainCtx.drawImage(img, x, y, drawWidth, drawHeight); // Save canvas state if (typeof saveCanvasState === 'function') { saveCanvasState(); } // Mark as having drawing setHasDrawing(true); // 4. Show the original image in the display canvas first setTemplateLoadingMessage("Generating material preview..."); // Draw the original image to the display canvas if (displayCanvasRef.current) { const displayCtx = displayCanvasRef.current.getContext("2d"); if (displayCtx) { // Create a new image for the display canvas const displayImg = new Image(); displayImg.onload = () => { // Clear display canvas first displayCtx.clearRect(0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height); // Fill with black background for letterboxing displayCtx.fillStyle = "#000000"; displayCtx.fillRect(0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height); // Calculate aspect ratios for display canvas const imgRatio = displayImg.width / displayImg.height; const canvasRatio = displayCanvasRef.current.width / displayCanvasRef.current.height; // Declare variables separately let dispDrawWidth = 0; let dispDrawHeight = 0; let dispX = 0; let dispY = 0; if (imgRatio > canvasRatio) { // Image is wider than canvas dispDrawWidth = displayCanvasRef.current.width; dispDrawHeight = displayCanvasRef.current.width / imgRatio; dispX = 0; dispY = (displayCanvasRef.current.height - dispDrawHeight) / 2; } else { // Image is taller than canvas dispDrawHeight = displayCanvasRef.current.height; dispDrawWidth = displayCanvasRef.current.height * imgRatio; dispX = (displayCanvasRef.current.width - dispDrawWidth) / 2; dispY = 0; } // Draw the image with letterboxing displayCtx.drawImage(displayImg, dispX, dispY, dispDrawWidth, dispDrawHeight); // Set flag to indicate we have generated content setHasGeneratedContent(true); // 5. Finally, trigger generation to show the styled version // Close the library view and finish the template process setShowLibrary(false); // Slight delay before starting generation setTimeout(() => { handleGeneration(); // Turn off template loading setIsTemplateLoading(false); setTemplateLoadingMessage(""); }, 500); }; // Load the original image for display displayImg.src = imageUrl; } } else { // If no display canvas, just trigger generation and finish handleGeneration(); setShowLibrary(false); setIsTemplateLoading(false); setTemplateLoadingMessage(""); } }; img.src = `data:image/png;base64,${doodleData.imageData}`; } else { throw new Error("Canvas context unavailable"); } } else { throw new Error("Failed to convert to doodle"); } } else { throw new Error("Failed to analyze image"); } } catch (error) { console.error('Error using image as template:', error); toast.error('Failed to use image as template'); setIsTemplateLoading(false); setTemplateLoadingMessage(""); } }, [compressImage, handleGeneration]); return (
{showLibrary ? ( ) : (
{/* Header Buttons Section - only visible on desktop */}
{/* New single row layout */}
{/* Toolbar - fixed width on desktop, full width horizontal on mobile */}
{/* Mobile toolbar (horizontal) */}
{/* Desktop toolbar (vertical) */}
{/* Main content area */}
{/* Canvas row */}
{/* Canvas */}
setShowApiKeyModal(true)} />
{/* Display Canvas */}
)} setShowApiKeyModal(false)} onSubmit={handleApiKeySubmit} initialValue={customApiKey} /> setIsHistoryModalOpen(false)} history={imageHistory} onSelectImage={handleSelectHistoricalImage} currentDimension={currentDimension} /> {/* Template loading overlay */} {isTemplateLoading && (

{templateLoadingMessage || "Processing template..."}

This may take a moment

)}
); }; export default CanvasContainer;