import { useState, useCallback } from 'react'; import { ToastMessage } from './useToasts'; import { dataURLtoBlob, calculateSHA256, copyToClipboard } from '../utils/canvasUtils'; import { CanvasHistoryHook } from './useCanvasHistory'; const SHARE_API_URL = 'https://sharefile.suisuy.eu.org'; const ASK_API_URL = 'https://getai.deno.dev/'; const MAX_UPLOAD_SIZE_BYTES = 50 * 1024 * 1024; // 50MB export type AiImageQuality = 'low' | 'medium' | 'hd'; export type AiDimensionsMode = 'api_default' | 'match_canvas' | 'fixed_1024'; interface AiFeaturesHook { isMagicUploading: boolean; showAiEditModal: boolean; aiPrompt: string; isGeneratingAiImage: boolean; sharedImageUrlForAi: string | null; aiEditError: string | null; isAskingAi: boolean; askUrl: string | null; handleMagicUpload: () => Promise; handleGenerateAiImage: () => Promise; handleAskAi: () => Promise; handleCancelAiEdit: () => void; setAiPrompt: React.Dispatch>; clearAskUrl: () => void; } interface UseAiFeaturesProps { currentDataURL: string | null; showToast: (message: string, type: ToastMessage['type']) => void; updateCanvasState: CanvasHistoryHook['updateCanvasState']; setZoomLevel: (zoom: number) => void; aiImageQuality: AiImageQuality; aiApiEndpoint: string; aiDimensionsMode: AiDimensionsMode; currentCanvasWidth: number; currentCanvasHeight: number; } export const useAiFeatures = ({ currentDataURL, showToast, updateCanvasState, setZoomLevel, aiImageQuality, aiApiEndpoint, aiDimensionsMode, currentCanvasWidth, currentCanvasHeight, }: UseAiFeaturesProps): AiFeaturesHook => { const [isMagicUploading, setIsMagicUploading] = useState(false); const [showAiEditModal, setShowAiEditModal] = useState(false); const [aiPrompt, setAiPrompt] = useState(''); const [isGeneratingAiImage, setIsGeneratingAiImage] = useState(false); const [sharedImageUrlForAi, setSharedImageUrlForAi] = useState(null); const [aiEditError, setAiEditError] = useState(null); const [isAskingAi, setIsAskingAi] = useState(false); const [askUrl, setAskUrl] = useState(null); const clearAskUrl = useCallback(() => { setAskUrl(null); }, []); const loadAiImageOntoCanvas = useCallback((aiImageDataUrl: string) => { const img = new Image(); img.onload = () => { const tempCanvas = document.createElement('canvas'); tempCanvas.width = currentCanvasWidth; tempCanvas.height = currentCanvasHeight; const tempCtx = tempCanvas.getContext('2d'); if (tempCtx) { // Fill background (e.g., white) tempCtx.fillStyle = '#FFFFFF'; tempCtx.fillRect(0, 0, currentCanvasWidth, currentCanvasHeight); let drawWidth = img.naturalWidth; let drawHeight = img.naturalHeight; // Scale image if it's larger than the canvas, preserving aspect ratio if (img.naturalWidth > currentCanvasWidth || img.naturalHeight > currentCanvasHeight) { const aspectRatio = img.naturalWidth / img.naturalHeight; if (currentCanvasWidth / aspectRatio <= currentCanvasHeight) { drawWidth = currentCanvasWidth; drawHeight = currentCanvasWidth / aspectRatio; } else { drawHeight = currentCanvasHeight; drawWidth = currentCanvasHeight * aspectRatio; } } // Ensure dimensions are at least 1px drawWidth = Math.max(1, Math.floor(drawWidth)); drawHeight = Math.max(1, Math.floor(drawHeight)); // Change: Draw image at top-left (0,0) const drawX = 0; const drawY = 0; tempCtx.drawImage(img, drawX, drawY, drawWidth, drawHeight); const newCanvasState = tempCanvas.toDataURL('image/png'); // Update canvas state with the new image, but keep original canvas dimensions updateCanvasState(newCanvasState, currentCanvasWidth, currentCanvasHeight); setShowAiEditModal(false); showToast('AI image applied to canvas!', 'success'); setAiPrompt(''); setSharedImageUrlForAi(null); } else { setAiEditError('Failed to create drawing context for AI image.'); } setIsGeneratingAiImage(false); }; img.onerror = () => { setAiEditError('Failed to load the generated AI image. It might be an invalid image format.'); setIsGeneratingAiImage(false); }; img.crossOrigin = "anonymous"; img.src = aiImageDataUrl; }, [showToast, updateCanvasState, currentCanvasWidth, currentCanvasHeight]); const handleMagicUpload = useCallback(async () => { if (isMagicUploading || !currentDataURL) { showToast('No canvas content to share.', 'info'); return; } setIsMagicUploading(true); setAiEditError(null); showToast('Uploading image...', 'info'); try { const blob = await dataURLtoBlob(currentDataURL); if (blob.size > MAX_UPLOAD_SIZE_BYTES) { showToast(`Image too large (${(blob.size / 1024 / 1024).toFixed(2)}MB). Max 50MB.`, 'error'); setIsMagicUploading(false); return; } const hash = await calculateSHA256(blob); const filename = `tempaint_${hash}.png`; const uploadUrl = `${SHARE_API_URL}/${filename}`; const response = await fetch(uploadUrl, { method: 'POST', headers: { 'Content-Type': blob.type || 'image/png' }, body: blob, }); if (!response.ok) { let errorMsg = `Upload failed: ${response.statusText}`; try { const errorBody = await response.text(); errorMsg = `Upload failed: ${errorBody || response.statusText}`; } catch (e) { /* ignore */ } throw new Error(errorMsg); } const returnedObjectName = await response.text(); const finalShareUrl = `https://pub-cb2c87ea7373408abb1050dd43e3cd8e.r2.dev/${returnedObjectName}`; setSharedImageUrlForAi(finalShareUrl); setShowAiEditModal(true); setAiPrompt(''); setAiEditError(null); clearAskUrl(); } catch (error: any) { console.error('Magic upload error:', error); showToast(error.message || 'Magic upload failed. Check console.', 'error'); } finally { setIsMagicUploading(false); } }, [isMagicUploading, currentDataURL, showToast, clearAskUrl]); const handleGenerateAiImage = useCallback(async () => { if (!aiPrompt.trim() || !sharedImageUrlForAi) { setAiEditError('Please enter a prompt and ensure an image was uploaded.'); return; } setIsGeneratingAiImage(true); setAiEditError(null); clearAskUrl(); showToast('Generating AI image...', 'info'); const encodedPrompt = encodeURIComponent(aiPrompt); const encodedImageUrl = encodeURIComponent(sharedImageUrlForAi); let finalApiUrl = aiApiEndpoint .replace('{prompt}', encodedPrompt) .replace('{imgurl.url}', encodedImageUrl); if (finalApiUrl.includes('{quality}')) { finalApiUrl = finalApiUrl.replace('{quality}', aiImageQuality); } if (finalApiUrl.includes('{size_params}')) { let sizeParamsString = ''; if (aiDimensionsMode === 'match_canvas') { sizeParamsString = `&width=${currentCanvasWidth}&height=${currentCanvasHeight}`; } else if (aiDimensionsMode === 'fixed_1024') { sizeParamsString = `&width=1024&height=1024`; } finalApiUrl = finalApiUrl.replace('{size_params}', sizeParamsString); } try { const response = await fetch(finalApiUrl); if (!response.ok) { let errorMsg = `AI image generation failed: ${response.status} ${response.statusText}`; try { const errorBody = await response.text(); if (errorBody && !errorBody.toLowerCase().includes(' { if (typeof reader.result === 'string') { loadAiImageOntoCanvas(reader.result); } else { setAiEditError('Failed to read AI image data as string.'); setIsGeneratingAiImage(false); } }; reader.onerror = () => { setAiEditError('Failed to read AI image data.'); setIsGeneratingAiImage(false); } reader.readAsDataURL(imageBlob); } catch (error: any) { console.error('AI image generation error:', error); setAiEditError(error.message || 'An unknown error occurred during AI image generation.'); setIsGeneratingAiImage(false); } }, [aiPrompt, sharedImageUrlForAi, showToast, loadAiImageOntoCanvas, aiImageQuality, aiApiEndpoint, aiDimensionsMode, currentCanvasWidth, currentCanvasHeight, clearAskUrl]); const handleAskAi = useCallback(async () => { if (!aiPrompt.trim() || !sharedImageUrlForAi) { setAiEditError('Please enter a question about the image.'); return; } setIsAskingAi(true); setAiEditError(null); setAskUrl(null); // Clear previous URL first try { const encodedPrompt = encodeURIComponent(aiPrompt); const encodedImageUrl = encodeURIComponent(sharedImageUrlForAi); const finalAskUrl = `${ASK_API_URL}?q=${encodedPrompt}&image=${encodedImageUrl}`; setAskUrl(finalAskUrl); } catch (error: any) { console.error('AI ask error:', error); setAiEditError(error.message || 'An unknown error occurred while asking AI.'); } finally { setIsAskingAi(false); } }, [aiPrompt, sharedImageUrlForAi]); const handleCancelAiEdit = () => { setShowAiEditModal(false); if (sharedImageUrlForAi) { copyToClipboard(sharedImageUrlForAi, (msg, type) => showToast(msg,type as 'info' | 'error')).then(copied => { if(copied) { showToast(`Image uploaded! URL: ${sharedImageUrlForAi} (Copied!)`, 'success'); } }); } setAiPrompt(''); setAiEditError(null); setSharedImageUrlForAi(null); clearAskUrl(); }; return { isMagicUploading, showAiEditModal, aiPrompt, isGeneratingAiImage, sharedImageUrlForAi, aiEditError, isAskingAi, askUrl, handleMagicUpload, handleGenerateAiImage, handleAskAi, handleCancelAiEdit, setAiPrompt, clearAskUrl, }; };