Spaces:
Running
Running
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 ( | |
<div className="flex min-h-screen flex-col items-center justify-start bg-gray-50 p-2 md:p-4 overflow-y-auto"> | |
{showLibrary ? ( | |
<LibraryPage onBack={toggleLibrary} onUseAsTemplate={handleUseAsTemplate} /> | |
) : ( | |
<div className="w-full max-w-[1800px] mx-auto pb-4"> | |
<div className="space-y-1"> | |
<div className="flex flex-col sm:flex-row items-start justify-between gap-2"> | |
<div className="flex-shrink-0"> | |
<Header /> | |
</div> | |
{/* Header Buttons Section - only visible on desktop */} | |
<div className="hidden md:flex items-center gap-2 mt-auto sm:mt-8"> | |
<HeaderButtons | |
hasHistory={hasHistory} | |
openHistoryModal={openHistoryModal} | |
toggleLibrary={toggleLibrary} | |
handleSaveImage={handleSaveImage} | |
isLoading={isLoading} | |
hasGeneratedContent={hasGeneratedContent} | |
/> | |
</div> | |
</div> | |
{/* New single row layout */} | |
<div className="flex flex-col md:flex-row items-stretch gap-4 w-full md:mt-4"> | |
{/* Toolbar - fixed width on desktop, full width horizontal on mobile */} | |
<div className="w-full md:w-[60px] md:flex-shrink-0"> | |
{/* Mobile toolbar (horizontal) */} | |
<div className="block md:hidden w-fit"> | |
<ToolBar | |
currentTool={currentTool} | |
setCurrentTool={setCurrentTool} | |
handleUndo={handleUndo} | |
clearCanvas={clearCanvas} | |
orientation="horizontal" | |
currentWidth={penWidth} | |
setStrokeWidth={handleStrokeWidth} | |
currentDimension={currentDimension} | |
onDimensionChange={handleDimensionChange} | |
/> | |
</div> | |
{/* Desktop toolbar (vertical) */} | |
<div className="hidden md:block"> | |
<ToolBar | |
currentTool={currentTool} | |
setCurrentTool={setCurrentTool} | |
handleUndo={handleUndo} | |
clearCanvas={clearCanvas} | |
orientation="vertical" | |
currentWidth={penWidth} | |
setStrokeWidth={handleStrokeWidth} | |
currentDimension={currentDimension} | |
onDimensionChange={handleDimensionChange} | |
/> | |
</div> | |
</div> | |
{/* Main content area */} | |
<div className="flex-1 flex flex-col gap-4"> | |
{/* Canvas row */} | |
<div className="flex flex-col md:flex-row gap-2"> | |
{/* Canvas */} | |
<div className="flex-1 w-full relative"> | |
<Canvas | |
ref={canvasComponentRef} | |
canvasRef={canvasRef} | |
currentTool={currentTool} | |
isDrawing={isDrawing} | |
startDrawing={startDrawing} | |
draw={draw} | |
stopDrawing={stopDrawing} | |
handleCanvasClick={handleCanvasClick} | |
handlePenClick={handlePenClick} | |
handleGeneration={handleGeneration} | |
tempPoints={tempPoints} | |
setTempPoints={setTempPoints} | |
handleUndo={handleUndo} | |
clearCanvas={clearCanvas} | |
setCurrentTool={setCurrentTool} | |
currentDimension={currentDimension} | |
currentColor={penColor} | |
currentWidth={penWidth} | |
onImageUpload={handleImageUpload} | |
onGenerate={handleGeneration} | |
isGenerating={isLoading} | |
setIsGenerating={setIsLoading} | |
saveCanvasState={saveCanvasState} | |
onDrawingChange={setHasDrawing} | |
styleMode={styleMode} | |
setStyleMode={setStyleMode} | |
isSendingToDoodle={isSendingToDoodle} | |
customApiKey={customApiKey} | |
onOpenApiKeyModal={() => setShowApiKeyModal(true)} | |
/> | |
</div> | |
{/* Display Canvas */} | |
<div className="flex-1 w-full"> | |
<DisplayCanvas | |
displayCanvasRef={displayCanvasRef} | |
isLoading={isLoading} | |
handleRegenerate={handleRegenerate} | |
hasGeneratedContent={hasGeneratedContent} | |
currentDimension={currentDimension} | |
onOpenHistory={openHistoryModal} | |
onRefineImage={handleImageRefinement} | |
onSendToDoodle={handleSendToDoodle} | |
hasHistory={hasHistory} | |
openHistoryModal={openHistoryModal} | |
toggleLibrary={toggleLibrary} | |
handleSaveImage={handleSaveImage} | |
/> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
)} | |
<ErrorModal | |
showErrorModal={showErrorModal} | |
closeErrorModal={closeErrorModal} | |
customApiKey={customApiKey} | |
setCustomApiKey={setCustomApiKey} | |
handleApiKeySubmit={handleApiKeySubmit} | |
debugMode={debugMode} | |
setDebugMode={setDebugMode} | |
/> | |
<ApiKeyModal | |
isOpen={showApiKeyModal} | |
onClose={() => setShowApiKeyModal(false)} | |
onSubmit={handleApiKeySubmit} | |
initialValue={customApiKey} | |
/> | |
<TextInput | |
isTyping={isTyping} | |
textInputRef={textInputRef} | |
textInput={textInput} | |
setTextInput={setTextInput} | |
handleTextInput={handleTextInput} | |
textPosition={textPosition} | |
/> | |
<HistoryModal | |
isOpen={isHistoryModalOpen} | |
onClose={() => setIsHistoryModalOpen(false)} | |
history={imageHistory} | |
onSelectImage={handleSelectHistoricalImage} | |
currentDimension={currentDimension} | |
/> | |
{/* Template loading overlay */} | |
{isTemplateLoading && ( | |
<div className="fixed inset-0 flex flex-col items-center justify-center bg-black/50 z-50"> | |
<div className="bg-white shadow-lg rounded-xl p-6 flex flex-col items-center max-w-md"> | |
<LoaderCircle className="w-12 h-12 text-blue-600 animate-spin mb-4" /> | |
<p className="text-gray-900 font-medium text-lg">{templateLoadingMessage || "Processing template..."}</p> | |
<p className="text-gray-500 text-sm mt-2">This may take a moment</p> | |
</div> | |
</div> | |
)} | |
</div> | |
); | |
}; | |
export default CanvasContainer; | |