|
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 = () => { |
|
|
|
const isMobileDevice = () => { |
|
if (typeof window !== 'undefined') { |
|
return window.innerWidth < 768; |
|
} |
|
return false; |
|
}; |
|
|
|
|
|
const getDefaultDimension = () => { |
|
if (isMobileDevice()) { |
|
|
|
return { |
|
id: "square", |
|
label: "1:1", |
|
width: 1000, |
|
height: 1000, |
|
}; |
|
} else { |
|
|
|
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; |
|
const [currentTool, setCurrentTool] = useState("pencil"); |
|
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); |
|
|
|
const needsRegenerationRef = useRef(false); |
|
|
|
const isManualRegenerationRef = useRef(false); |
|
const [isSendingToDoodle, setIsSendingToDoodle] = useState(false); |
|
|
|
const [showApiKeyModal, setShowApiKeyModal] = useState(false); |
|
const [showLibrary, setShowLibrary] = useState(false); |
|
|
|
const [isTemplateLoading, setIsTemplateLoading] = useState(false); |
|
const [templateLoadingMessage, setTemplateLoadingMessage] = useState(""); |
|
|
|
|
|
useEffect(() => { |
|
const savedApiKey = localStorage.getItem("geminiApiKey"); |
|
if (savedApiKey) { |
|
setCustomApiKey(savedApiKey); |
|
|
|
validateApiKey(savedApiKey); |
|
} |
|
|
|
|
|
const debugParam = new URLSearchParams(window.location.search).get('debug'); |
|
|
|
const savedDebug = debugParam !== "false" && localStorage.getItem("debugMode") === "true"; |
|
|
|
if (debugParam === "true" || savedDebug) { |
|
|
|
setDebugMode(true); |
|
setShowErrorModal(true); |
|
} else { |
|
|
|
setDebugMode(false); |
|
|
|
if (localStorage.getItem("debugMode") === "true") { |
|
localStorage.setItem("debugMode", "false"); |
|
} |
|
} |
|
}, []); |
|
|
|
|
|
useEffect(() => { |
|
|
|
localStorage.setItem("debugMode", debugMode.toString()); |
|
|
|
|
|
if (debugMode === true) { |
|
setShowErrorModal(true); |
|
} |
|
}, [debugMode]); |
|
|
|
|
|
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"); |
|
|
|
localStorage.removeItem("geminiApiKey"); |
|
setCustomApiKey(""); |
|
|
|
} |
|
} catch (error) { |
|
console.error("Error validating API key:", error); |
|
|
|
} |
|
}; |
|
|
|
|
|
useEffect(() => { |
|
if (generatedImage && canvasRef.current) { |
|
|
|
const img = new window.Image(); |
|
img.onload = () => { |
|
backgroundImageRef.current = img; |
|
drawImageToCanvas(canvasRef.current, backgroundImageRef.current); |
|
}; |
|
img.src = generatedImage; |
|
} |
|
}, [generatedImage]); |
|
|
|
|
|
useEffect(() => { |
|
if (canvasRef.current) { |
|
initializeCanvas(canvasRef.current); |
|
} |
|
|
|
|
|
if (displayCanvasRef.current) { |
|
const displayCtx = displayCanvasRef.current.getContext("2d"); |
|
displayCtx.fillStyle = "#FFFFFF"; |
|
displayCtx.fillRect( |
|
0, |
|
0, |
|
displayCanvasRef.current.width, |
|
displayCanvasRef.current.height |
|
); |
|
} |
|
}, []); |
|
|
|
|
|
useEffect(() => { |
|
let isMobile = isMobileDevice(); |
|
|
|
const handleResize = () => { |
|
const newIsMobile = isMobileDevice(); |
|
|
|
if (newIsMobile !== isMobile) { |
|
isMobile = newIsMobile; |
|
|
|
|
|
if (canvasRef.current && !hasDrawing && !hasGeneratedContent) { |
|
setCurrentDimension(getDefaultDimension()); |
|
} |
|
} |
|
}; |
|
|
|
window.addEventListener('resize', handleResize); |
|
return () => window.removeEventListener('resize', handleResize); |
|
}, [hasDrawing, hasGeneratedContent]); |
|
|
|
|
|
useEffect(() => { |
|
if (canvasRef.current && displayCanvasRef.current) { |
|
|
|
canvasRef.current.width = currentDimension.width; |
|
canvasRef.current.height = currentDimension.height; |
|
displayCanvasRef.current.width = currentDimension.width; |
|
displayCanvasRef.current.height = currentDimension.height; |
|
|
|
|
|
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"); |
|
|
|
|
|
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); |
|
|
|
|
|
saveCanvasState(); |
|
}; |
|
|
|
const draw = (e) => { |
|
if (!isDrawing) return; |
|
|
|
const canvas = canvasRef.current; |
|
const ctx = canvas.getContext("2d"); |
|
const { x, y } = getCoordinates(e, canvas); |
|
|
|
|
|
if (Math.random() < 0.05) { |
|
|
|
console.log("draw called", { currentTool, isDrawing, x, y }); |
|
} |
|
|
|
|
|
ctx.lineWidth = currentTool === "eraser" ? 60 : penWidth * 4; |
|
ctx.lineCap = "round"; |
|
ctx.lineJoin = "round"; |
|
|
|
if (currentTool === "eraser") { |
|
ctx.strokeStyle = "#FFFFFF"; |
|
} else { |
|
ctx.strokeStyle = penColor; |
|
} |
|
|
|
if (currentTool === "pen") { |
|
|
|
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); |
|
|
|
|
|
if (strokeTimeoutRef.current) { |
|
clearTimeout(strokeTimeoutRef.current); |
|
strokeTimeoutRef.current = null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
e && |
|
(e.type === "mouseup" || e.type === "touchend") && |
|
currentTool !== "pen" && |
|
currentTool !== "pencil" |
|
) { |
|
console.log("stopDrawing: detected mouseup/touchend event", { |
|
strokeCount, |
|
}); |
|
|
|
if (strokeCount >= 10) { |
|
console.log( |
|
"stopDrawing: calling handleGeneration due to stroke count" |
|
); |
|
await handleGeneration(); |
|
setStrokeCount(0); |
|
} |
|
} |
|
}; |
|
|
|
const clearCanvas = () => { |
|
|
|
if (canvasComponentRef.current?.handleClearCanvas) { |
|
canvasComponentRef.current.handleClearCanvas(); |
|
return; |
|
} |
|
|
|
|
|
const canvas = canvasRef.current; |
|
if (!canvas) return; |
|
|
|
initializeCanvas(canvas); |
|
|
|
setGeneratedImage(null); |
|
backgroundImageRef.current = null; |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
saveCanvasState(); |
|
}; |
|
|
|
const handleGeneration = useCallback( |
|
async (isManualRegeneration = false) => { |
|
console.log("handleGeneration called", { isManualRegeneration }); |
|
|
|
|
|
if (isManualRegeneration) { |
|
isManualRegenerationRef.current = true; |
|
} |
|
|
|
|
|
|
|
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"); |
|
|
|
|
|
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}`; |
|
|
|
|
|
const displayCanvas = displayCanvasRef.current; |
|
if (!displayCanvas) { |
|
console.error("Display canvas ref is null"); |
|
return; |
|
} |
|
|
|
const displayCtx = displayCanvas.getContext("2d"); |
|
|
|
|
|
displayCtx.clearRect(0, 0, displayCanvas.width, displayCanvas.height); |
|
displayCtx.fillStyle = "#FFFFFF"; |
|
displayCtx.fillRect(0, 0, displayCanvas.width, displayCanvas.height); |
|
|
|
|
|
const img = new Image(); |
|
|
|
|
|
img.onload = () => { |
|
console.log("Generated image loaded, drawing to display canvas"); |
|
|
|
|
|
displayCtx.clearRect( |
|
0, |
|
0, |
|
displayCanvas.width, |
|
displayCanvas.height |
|
); |
|
|
|
|
|
displayCtx.fillStyle = "#000000"; |
|
displayCtx.fillRect( |
|
0, |
|
0, |
|
displayCanvas.width, |
|
displayCanvas.height |
|
); |
|
|
|
|
|
const imgRatio = img.width / img.height; |
|
const canvasRatio = displayCanvas.width / displayCanvas.height; |
|
|
|
let drawWidth, drawHeight, x, y; |
|
|
|
if (imgRatio > canvasRatio) { |
|
|
|
drawWidth = displayCanvas.width; |
|
drawHeight = displayCanvas.width / imgRatio; |
|
x = 0; |
|
y = (displayCanvas.height - drawHeight) / 2; |
|
} else { |
|
|
|
drawHeight = displayCanvas.height; |
|
drawWidth = displayCanvas.height * imgRatio; |
|
x = (displayCanvas.width - drawWidth) / 2; |
|
y = 0; |
|
} |
|
|
|
|
|
displayCtx.drawImage(img, x, y, drawWidth, drawHeight); |
|
|
|
|
|
setHasGeneratedContent(true); |
|
|
|
|
|
setImageHistory((prev) => [ |
|
...prev, |
|
{ |
|
imageUrl, |
|
timestamp: Date.now(), |
|
drawingData: canvas.toDataURL(), |
|
styleMode, |
|
dimensions: currentDimension, |
|
}, |
|
]); |
|
}; |
|
|
|
|
|
img.src = imageUrl; |
|
} else { |
|
console.error("Failed to generate image:", data.error); |
|
|
|
|
|
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); |
|
|
|
|
|
if ( |
|
data.error && |
|
(data.error.includes("Resource has been exhausted") || |
|
data.error.includes("quota") || |
|
data.error.includes("exceeded") || |
|
response.status === 429) |
|
) { |
|
|
|
setShowApiKeyModal(true); |
|
} else if (response.status === 500) { |
|
|
|
setErrorMessage(data.error); |
|
setShowErrorModal(true); |
|
} |
|
} |
|
} catch (error) { |
|
console.error("Error generating image:", error); |
|
|
|
|
|
if ( |
|
error.message && |
|
(error.message.includes("Resource has been exhausted") || |
|
error.message.includes("quota") || |
|
error.message.includes("exceeded") || |
|
error.message.includes("429")) |
|
) { |
|
|
|
setShowApiKeyModal(true); |
|
} else { |
|
|
|
setErrorMessage(error.message || "An unexpected error occurred."); |
|
setShowErrorModal(true); |
|
} |
|
|
|
|
|
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); |
|
} finally { |
|
setIsLoading(false); |
|
console.log("Generation process completed"); |
|
} |
|
}, |
|
[canvasRef, isLoading, styleMode, customApiKey, lastRequestTime] |
|
); |
|
|
|
|
|
const closeErrorModal = () => { |
|
setShowErrorModal(false); |
|
}; |
|
|
|
|
|
const handleApiKeySubmit = (apiKey) => { |
|
setCustomApiKey(apiKey); |
|
|
|
localStorage.setItem("geminiApiKey", apiKey); |
|
|
|
setShowApiKeyModal(false); |
|
|
|
setShowErrorModal(false); |
|
|
|
toast.success("API key saved successfully"); |
|
}; |
|
|
|
|
|
const handleUndo = () => { |
|
if (undoStack.length > 0) { |
|
const canvas = canvasRef.current; |
|
const ctx = canvas.getContext("2d"); |
|
const previousState = undoStack[undoStack.length - 2]; |
|
|
|
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 { |
|
|
|
ctx.fillStyle = "#FFFFFF"; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
} |
|
|
|
setUndoStack((prev) => prev.slice(0, -1)); |
|
} |
|
}; |
|
|
|
|
|
const saveCanvasState = () => { |
|
const canvas = canvasRef.current; |
|
if (!canvas) return; |
|
|
|
const dataURL = canvas.toDataURL(); |
|
setUndoStack((prev) => [...prev, dataURL]); |
|
}; |
|
|
|
|
|
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(); |
|
} |
|
}; |
|
|
|
|
|
const handleCanvasClick = (e) => { |
|
if (currentTool === "text") { |
|
const { x, y } = getCoordinates(e, canvasRef.current); |
|
setTextPosition({ x, y }); |
|
setIsTyping(true); |
|
if (textInputRef.current) { |
|
textInputRef.current.focus(); |
|
} |
|
} |
|
}; |
|
|
|
|
|
const handlePenClick = (e) => { |
|
if (currentTool !== "pen") return; |
|
|
|
|
|
|
|
|
|
|
|
console.log("handlePenClick called in CanvasContainer"); |
|
|
|
|
|
|
|
setIsDrawing(true); |
|
|
|
|
|
saveCanvasState(); |
|
}; |
|
|
|
|
|
const handleSaveImage = useCallback(() => { |
|
if (displayCanvasRef.current && hasGeneratedContent) { |
|
const canvas = displayCanvasRef.current; |
|
const link = document.createElement('a'); |
|
|
|
|
|
const now = new Date(); |
|
const timestamp = now.toISOString() |
|
.replace(/[-:T]/g, '') |
|
.slice(0, 12); |
|
|
|
|
|
const materialName = styleOptions[styleMode]?.name || styleMode; |
|
|
|
|
|
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]); |
|
|
|
|
|
const handleRegenerate = async () => { |
|
if (canvasRef.current) { |
|
|
|
isManualRegenerationRef.current = true; |
|
await handleGeneration(true); |
|
} |
|
}; |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (isManualRegenerationRef.current) { |
|
console.log("Skipping automatic generation due to manual regeneration"); |
|
return; |
|
} |
|
|
|
|
|
|
|
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); |
|
|
|
|
|
const hasDrawing = Array.from(imageData.data).some((pixel, index) => { |
|
|
|
return index % 4 !== 3 && pixel !== 255; |
|
}); |
|
|
|
|
|
if (hasDrawing && !hasGeneratedContent) { |
|
await handleGeneration(); |
|
} else if (hasDrawing) { |
|
|
|
needsRegenerationRef.current = true; |
|
} |
|
}; |
|
|
|
|
|
if (styleMode) { |
|
checkCanvasAndGenerate(); |
|
} |
|
}, [styleMode, hasGeneratedContent]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (isManualRegenerationRef.current) { |
|
console.log("Skipping automatic generation due to manual regeneration"); |
|
|
|
isManualRegenerationRef.current = false; |
|
return; |
|
} |
|
|
|
|
|
|
|
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); |
|
|
|
|
|
const hasDrawing = Array.from(imageData.data).some((pixel, index) => { |
|
|
|
return index % 4 !== 3 && pixel !== 255; |
|
}); |
|
|
|
if (hasDrawing) { |
|
needsRegenerationRef.current = false; |
|
await handleGeneration(); |
|
} |
|
}; |
|
|
|
checkDrawingAndRegenerate(); |
|
} |
|
}, [hasGeneratedContent]); |
|
|
|
|
|
useEffect(() => { |
|
return () => { |
|
if (strokeTimeoutRef.current) { |
|
clearTimeout(strokeTimeoutRef.current); |
|
strokeTimeoutRef.current = null; |
|
} |
|
}; |
|
}, []); |
|
|
|
|
|
const handleDimensionChange = (newDimension) => { |
|
console.log("Changing dimensions to:", newDimension); |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
setHasGeneratedContent(false); |
|
setGeneratedImage(null); |
|
backgroundImageRef.current = null; |
|
|
|
|
|
setCurrentDimension(newDimension); |
|
}; |
|
|
|
|
|
const handleSelectHistoricalImage = (historyItem) => { |
|
|
|
if (historyItem.dimensions) { |
|
|
|
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; |
|
} |
|
|
|
setCurrentDimension(historyItem.dimensions); |
|
} |
|
|
|
|
|
Promise.resolve().then(() => { |
|
|
|
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; |
|
|
|
|
|
const generatedImg = new Image(); |
|
generatedImg.onload = () => { |
|
const displayCanvas = displayCanvasRef.current; |
|
if (displayCanvas) { |
|
const ctx = displayCanvas.getContext("2d"); |
|
ctx.fillStyle = "#000000"; |
|
ctx.fillRect(0, 0, displayCanvas.width, displayCanvas.height); |
|
|
|
|
|
const imgRatio = generatedImg.width / generatedImg.height; |
|
const canvasRatio = displayCanvas.width / displayCanvas.height; |
|
|
|
let drawWidth, drawHeight, x, y; |
|
|
|
if (imgRatio > canvasRatio) { |
|
|
|
drawWidth = displayCanvas.width; |
|
drawHeight = displayCanvas.width / imgRatio; |
|
x = 0; |
|
y = (displayCanvas.height - drawHeight) / 2; |
|
} else { |
|
|
|
drawHeight = displayCanvas.height; |
|
drawWidth = displayCanvas.height * imgRatio; |
|
x = (displayCanvas.width - drawWidth) / 2; |
|
y = 0; |
|
} |
|
|
|
|
|
ctx.drawImage(generatedImg, x, y, drawWidth, drawHeight); |
|
setHasGeneratedContent(true); |
|
} |
|
}; |
|
generatedImg.src = historyItem.imageUrl; |
|
}); |
|
|
|
|
|
setIsHistoryModalOpen(false); |
|
}; |
|
|
|
|
|
const handleImageRefinement = async (refinementPrompt) => { |
|
if (!displayCanvasRef.current || !hasGeneratedContent) return; |
|
|
|
console.log("Starting image refinement with prompt:", refinementPrompt); |
|
setIsLoading(true); |
|
|
|
try { |
|
|
|
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}`; |
|
|
|
|
|
const displayCtx = displayCanvas.getContext("2d"); |
|
const img = new Image(); |
|
|
|
img.onload = () => { |
|
console.log("Refined image loaded, drawing to display canvas"); |
|
|
|
|
|
displayCtx.clearRect(0, 0, displayCanvas.width, displayCanvas.height); |
|
|
|
|
|
displayCtx.fillStyle = "#000000"; |
|
displayCtx.fillRect(0, 0, displayCanvas.width, displayCanvas.height); |
|
|
|
|
|
const imgRatio = img.width / img.height; |
|
const canvasRatio = displayCanvas.width / displayCanvas.height; |
|
|
|
let drawWidth, drawHeight, x, y; |
|
|
|
if (imgRatio > canvasRatio) { |
|
|
|
drawWidth = displayCanvas.width; |
|
drawHeight = displayCanvas.width / imgRatio; |
|
x = 0; |
|
y = (displayCanvas.height - drawHeight) / 2; |
|
} else { |
|
|
|
drawHeight = displayCanvas.height; |
|
drawWidth = displayCanvas.height * imgRatio; |
|
x = (displayCanvas.width - drawWidth) / 2; |
|
y = 0; |
|
} |
|
|
|
|
|
displayCtx.drawImage(img, x, y, drawWidth, drawHeight); |
|
|
|
|
|
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); |
|
|
|
|
|
if ( |
|
data.error && |
|
(data.error.includes("Resource has been exhausted") || |
|
data.error.includes("quota") || |
|
data.error.includes("exceeded") || |
|
response.status === 429) |
|
) { |
|
|
|
setShowApiKeyModal(true); |
|
} else { |
|
|
|
setErrorMessage(data.error || "Failed to refine image. Please try again."); |
|
setShowErrorModal(true); |
|
} |
|
} |
|
} catch (error) { |
|
console.error("Error during refinement:", error); |
|
|
|
|
|
if ( |
|
error.message && |
|
(error.message.includes("Resource has been exhausted") || |
|
error.message.includes("quota") || |
|
error.message.includes("exceeded") || |
|
error.message.includes("429")) |
|
) { |
|
|
|
setShowApiKeyModal(true); |
|
} else { |
|
|
|
setErrorMessage("An error occurred during refinement. Please try again."); |
|
setShowErrorModal(true); |
|
} |
|
} finally { |
|
setIsLoading(false); |
|
} |
|
}; |
|
|
|
|
|
const handleImageUpload = (imageDataUrl) => { |
|
if (!canvasRef.current) return; |
|
|
|
const canvas = canvasRef.current; |
|
const ctx = canvas.getContext("2d"); |
|
const img = new Image(); |
|
|
|
img.onload = () => { |
|
|
|
ctx.fillStyle = "#FFFFFF"; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
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; |
|
|
|
|
|
ctx.drawImage(img, x, y, img.width * scale, img.height * scale); |
|
|
|
|
|
saveCanvasState(); |
|
setHasGeneratedContent(true); |
|
}; |
|
|
|
img.src = imageDataUrl; |
|
}; |
|
|
|
|
|
const handleStrokeWidth = (width) => { |
|
setPenWidth(width); |
|
}; |
|
|
|
|
|
const handleSendToDoodle = useCallback( |
|
async (imageDataUrl) => { |
|
if (!imageDataUrl || isSendingToDoodle) return; |
|
|
|
console.log("Sending image back to doodle canvas..."); |
|
setIsSendingToDoodle(true); |
|
|
|
let response; |
|
try { |
|
const base64Data = imageDataUrl.split(",")[1]; |
|
|
|
response = await fetch("/api/convert-to-doodle", { |
|
|
|
method: "POST", |
|
headers: { |
|
"Content-Type": "application/json", |
|
}, |
|
body: JSON.stringify({ |
|
imageData: base64Data, |
|
customApiKey |
|
}), |
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
let errorBody = await response.text(); |
|
let errorMessage = `API Error: ${response.status}`; |
|
try { |
|
|
|
const errorData = JSON.parse(errorBody); |
|
errorMessage = errorData.error || errorMessage; |
|
} catch (parseError) { |
|
|
|
console.error("API response was not valid JSON:", errorBody); |
|
|
|
errorMessage = `${errorMessage}. Response: ${errorBody.substring( |
|
0, |
|
100 |
|
)}${errorBody.length > 100 ? "..." : ""}`; |
|
} |
|
|
|
|
|
if ( |
|
errorMessage.includes("quota") || |
|
errorMessage.includes("exceeded") || |
|
errorMessage.includes("Resource has been exhausted") || |
|
response.status === 429 |
|
) { |
|
|
|
setShowApiKeyModal(true); |
|
setIsSendingToDoodle(false); |
|
return; |
|
} |
|
|
|
throw new Error(errorMessage); |
|
} |
|
|
|
|
|
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 = () => { |
|
|
|
mainCtx.fillStyle = '#FFFFFF'; |
|
mainCtx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height); |
|
|
|
|
|
mainCtx.drawImage( |
|
img, |
|
0, |
|
0, |
|
canvasRef.current.width, |
|
canvasRef.current.height |
|
); |
|
|
|
|
|
Promise.resolve().then(() => { |
|
setTempPoints([]); |
|
if (canvasRef.current.setHasDrawing) { |
|
canvasRef.current.setHasDrawing(true); |
|
} |
|
|
|
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); |
|
}; |
|
img.src = `data:image/png;base64,${result.imageData}`; |
|
} else { |
|
throw new Error("Main canvas context not available."); |
|
} |
|
} else { |
|
|
|
throw new Error( |
|
result.error || "API returned success:false or missing data." |
|
); |
|
} |
|
} catch (error) { |
|
|
|
console.error("Error sending image back to doodle:", error); |
|
|
|
|
|
if ( |
|
error.message && |
|
(error.message.includes("quota") || |
|
error.message.includes("exceeded") || |
|
error.message.includes("Resource has been exhausted") || |
|
error.message.includes("429")) |
|
) { |
|
|
|
setShowApiKeyModal(true); |
|
} else { |
|
toast.error(`Error: ${error.message || "An unknown error occurred."}`); |
|
} |
|
|
|
|
|
setIsSendingToDoodle(false); |
|
} |
|
}, |
|
[isSendingToDoodle, clearCanvas, saveCanvasState, setTempPoints, toast, customApiKey] |
|
); |
|
|
|
|
|
const openHistoryModal = () => { |
|
setIsHistoryModalOpen(true); |
|
}; |
|
|
|
|
|
const toggleLibrary = () => { |
|
setShowLibrary(prev => !prev); |
|
}; |
|
|
|
|
|
const hasHistory = imageHistory && imageHistory.length > 0; |
|
|
|
|
|
const compressImage = useCallback(async (dataUrl, maxWidth = 1200) => { |
|
return new Promise((resolve) => { |
|
const img = new Image(); |
|
img.onload = () => { |
|
|
|
const canvas = document.createElement('canvas'); |
|
const ctx = canvas.getContext('2d'); |
|
|
|
|
|
let width = img.width; |
|
let height = img.height; |
|
|
|
if (width > maxWidth) { |
|
height = (height * maxWidth) / width; |
|
width = maxWidth; |
|
} |
|
|
|
canvas.width = width; |
|
canvas.height = height; |
|
|
|
|
|
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; |
|
}); |
|
}, []); |
|
|
|
|
|
const handleUseAsTemplate = useCallback(async (imageUrl) => { |
|
console.log('Using library image as template:', imageUrl); |
|
|
|
|
|
setTemplateLoadingMessage("Preparing template..."); |
|
setIsTemplateLoading(true); |
|
|
|
try { |
|
|
|
|
|
const response = await fetch(imageUrl); |
|
const blob = await response.blob(); |
|
|
|
|
|
const reader = new FileReader(); |
|
const imageDataPromise = new Promise((resolve) => { |
|
reader.onloadend = () => resolve(reader.result); |
|
reader.readAsDataURL(blob); |
|
}); |
|
|
|
const imageDataUrl = await imageDataPromise; |
|
|
|
|
|
setTemplateLoadingMessage("Analyzing image..."); |
|
const compressedImage = await compressImage(imageDataUrl, 1200); |
|
|
|
|
|
const customApiKey = localStorage.getItem("geminiApiKey"); |
|
|
|
|
|
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(); |
|
|
|
|
|
if (promptData.enhancedPrompt && promptData.suggestedName) { |
|
setTemplateLoadingMessage("Creating material..."); |
|
|
|
const thumbnailImage = await compressImage(imageDataUrl, 300); |
|
|
|
const materialObj = { |
|
name: promptData.suggestedName, |
|
prompt: promptData.enhancedPrompt, |
|
image: thumbnailImage |
|
}; |
|
|
|
|
|
const materialKey = addMaterialToLibrary(materialObj); |
|
|
|
|
|
setStyleMode(materialKey); |
|
|
|
|
|
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) { |
|
|
|
const mainCtx = canvasRef.current?.getContext("2d"); |
|
if (mainCtx && canvasRef.current) { |
|
const img = new Image(); |
|
img.onload = () => { |
|
|
|
mainCtx.fillStyle = '#FFFFFF'; |
|
mainCtx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height); |
|
|
|
|
|
|
|
const canvasWidth = canvasRef.current.width; |
|
const canvasHeight = canvasRef.current.height; |
|
|
|
const imgRatio = img.width / img.height; |
|
const canvasRatio = canvasWidth / canvasHeight; |
|
|
|
|
|
let drawWidth = 0; |
|
let drawHeight = 0; |
|
let x = 0; |
|
let y = 0; |
|
|
|
if (imgRatio > canvasRatio) { |
|
|
|
drawWidth = canvasWidth * 0.8; |
|
drawHeight = drawWidth / imgRatio; |
|
x = canvasWidth * 0.1; |
|
y = (canvasHeight - drawHeight) / 2; |
|
} else { |
|
|
|
drawHeight = canvasHeight * 0.8; |
|
drawWidth = drawHeight * imgRatio; |
|
x = (canvasWidth - drawWidth) / 2; |
|
y = canvasHeight * 0.1; |
|
} |
|
|
|
|
|
mainCtx.drawImage(img, x, y, drawWidth, drawHeight); |
|
|
|
|
|
if (typeof saveCanvasState === 'function') { |
|
saveCanvasState(); |
|
} |
|
|
|
|
|
setHasDrawing(true); |
|
|
|
|
|
setTemplateLoadingMessage("Generating material preview..."); |
|
|
|
|
|
if (displayCanvasRef.current) { |
|
const displayCtx = displayCanvasRef.current.getContext("2d"); |
|
if (displayCtx) { |
|
|
|
const displayImg = new Image(); |
|
displayImg.onload = () => { |
|
|
|
displayCtx.clearRect(0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height); |
|
|
|
|
|
displayCtx.fillStyle = "#000000"; |
|
displayCtx.fillRect(0, 0, displayCanvasRef.current.width, displayCanvasRef.current.height); |
|
|
|
|
|
const imgRatio = displayImg.width / displayImg.height; |
|
const canvasRatio = displayCanvasRef.current.width / displayCanvasRef.current.height; |
|
|
|
|
|
let dispDrawWidth = 0; |
|
let dispDrawHeight = 0; |
|
let dispX = 0; |
|
let dispY = 0; |
|
|
|
if (imgRatio > canvasRatio) { |
|
|
|
dispDrawWidth = displayCanvasRef.current.width; |
|
dispDrawHeight = displayCanvasRef.current.width / imgRatio; |
|
dispX = 0; |
|
dispY = (displayCanvasRef.current.height - dispDrawHeight) / 2; |
|
} else { |
|
|
|
dispDrawHeight = displayCanvasRef.current.height; |
|
dispDrawWidth = displayCanvasRef.current.height * imgRatio; |
|
dispX = (displayCanvasRef.current.width - dispDrawWidth) / 2; |
|
dispY = 0; |
|
} |
|
|
|
|
|
displayCtx.drawImage(displayImg, dispX, dispY, dispDrawWidth, dispDrawHeight); |
|
|
|
|
|
setHasGeneratedContent(true); |
|
|
|
|
|
|
|
setShowLibrary(false); |
|
|
|
|
|
setTimeout(() => { |
|
handleGeneration(); |
|
|
|
|
|
setIsTemplateLoading(false); |
|
setTemplateLoadingMessage(""); |
|
}, 500); |
|
}; |
|
|
|
|
|
displayImg.src = imageUrl; |
|
} |
|
} else { |
|
|
|
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; |
|
|