| import { useState, useCallback } from 'react'; |
| import { ToastMessage } from './useToasts'; |
| import { dataURLtoBlob, calculateSHA256, copyToClipboard } from '../utils/canvasUtils'; |
| import { CanvasHistoryHook } from './useCanvasHistory'; |
| import { TFunction } from '../types'; |
|
|
| 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; |
|
|
| export type AiImageQuality = 'low' | 'medium' | 'high'; |
| 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; |
| generatedImageUrl: string | null; |
| handleMagicUpload: () => Promise<void>; |
| handleGenerateAiImage: () => Promise<void>; |
| handleAskAi: () => Promise<void>; |
| handleCancelAiEdit: () => void; |
| setAiPrompt: React.Dispatch<React.SetStateAction<string>>; |
| clearAiOutputs: () => 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; |
| t: TFunction; |
| } |
|
|
| export const useAiFeatures = ({ |
| currentDataURL, |
| showToast, |
| updateCanvasState, |
| setZoomLevel, |
| aiImageQuality, |
| aiApiEndpoint, |
| aiDimensionsMode, |
| currentCanvasWidth, |
| currentCanvasHeight, |
| t, |
| }: UseAiFeaturesProps): AiFeaturesHook => { |
| const [isMagicUploading, setIsMagicUploading] = useState<boolean>(false); |
| const [showAiEditModal, setShowAiEditModal] = useState<boolean>(false); |
| const [aiPrompt, setAiPrompt] = useState<string>(''); |
| const [isGeneratingAiImage, setIsGeneratingAiImage] = useState<boolean>(false); |
| const [sharedImageUrlForAi, setSharedImageUrlForAi] = useState<string | null>(null); |
| const [aiEditError, setAiEditError] = useState<string | null>(null); |
| const [isAskingAi, setIsAskingAi] = useState<boolean>(false); |
| const [askUrl, setAskUrl] = useState<string | null>(null); |
| const [generatedImageUrl, setGeneratedImageUrl] = useState<string | null>(null); |
|
|
| const clearAiOutputs = useCallback(() => { |
| setAskUrl(null); |
| setGeneratedImageUrl(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) { |
| |
| tempCtx.fillStyle = '#FFFFFF'; |
| tempCtx.fillRect(0, 0, currentCanvasWidth, currentCanvasHeight); |
|
|
| let drawWidth = img.naturalWidth; |
| let drawHeight = img.naturalHeight; |
|
|
| |
| 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; |
| } |
| } |
| |
| |
| drawWidth = Math.max(1, Math.floor(drawWidth)); |
| drawHeight = Math.max(1, Math.floor(drawHeight)); |
|
|
| |
| const drawX = 0; |
| const drawY = 0; |
|
|
| tempCtx.drawImage(img, drawX, drawY, drawWidth, drawHeight); |
| |
| const newCanvasState = tempCanvas.toDataURL('image/png'); |
|
|
| |
| updateCanvasState(newCanvasState, currentCanvasWidth, currentCanvasHeight); |
|
|
| setShowAiEditModal(false); |
| showToast(t('aiImageApplied'), 'success'); |
| setAiPrompt(''); |
| setSharedImageUrlForAi(null); |
| } else { |
| setAiEditError(t('failedToCreateContext')); |
| } |
| setIsGeneratingAiImage(false); |
| }; |
| img.onerror = () => { |
| setAiEditError(t('failedToLoadAiImage')); |
| setIsGeneratingAiImage(false); |
| }; |
| img.crossOrigin = "anonymous"; |
| img.src = aiImageDataUrl; |
| }, [showToast, updateCanvasState, currentCanvasWidth, currentCanvasHeight, t]); |
|
|
| const handleMagicUpload = useCallback(async () => { |
| if (isMagicUploading || !currentDataURL) { |
| showToast(t('noCanvasContentToShare'), 'info'); |
| return; |
| } |
|
|
| setIsMagicUploading(true); |
| setAiEditError(null); |
| showToast(t('uploadingImage'), 'info'); |
|
|
| try { |
| const blob = await dataURLtoBlob(currentDataURL); |
|
|
| if (blob.size > MAX_UPLOAD_SIZE_BYTES) { |
| showToast(t('imageTooLarge', { size: (blob.size / 1024 / 1024).toFixed(2) }), '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 = t('uploadFailed', { statusText: response.statusText }); |
| try { const errorBody = await response.text(); errorMsg = t('uploadFailed', { statusText: errorBody || response.statusText }); } catch (e) { } |
| throw new Error(errorMsg); |
| } |
|
|
| const returnedObjectName = await response.text(); |
| const finalShareUrl = `https://pub-cb2c87ea7373408abb1050dd43e3cd8e.r2.dev/${returnedObjectName}`; |
| |
| setSharedImageUrlForAi(finalShareUrl); |
| setShowAiEditModal(true); |
| setAiPrompt(''); |
| setAiEditError(null); |
| clearAiOutputs(); |
|
|
| } catch (error: any) { |
| console.error('Magic upload error:', error); |
| showToast(error.message || t('magicUploadError'), 'error'); |
| } finally { |
| setIsMagicUploading(false); |
| } |
| }, [isMagicUploading, currentDataURL, showToast, clearAiOutputs, t]); |
|
|
| const handleGenerateAiImage = useCallback(async () => { |
| if (!aiPrompt.trim() || !sharedImageUrlForAi) { |
| setAiEditError(t('enterPrompt')); |
| return; |
| } |
| setIsGeneratingAiImage(true); |
| setAiEditError(null); |
| clearAiOutputs(); |
| showToast(t('generatingAiImage'), '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); |
| } |
|
|
| setGeneratedImageUrl(finalApiUrl); |
| |
| try { |
| const response = await fetch(finalApiUrl); |
| if (!response.ok) { |
| let errorMsg = t('aiGenFailed', { status: response.status, statusText: response.statusText }); |
| try { |
| const errorBody = await response.text(); |
| if (errorBody && !errorBody.toLowerCase().includes('<html')) { |
| errorMsg += ` - ${errorBody}`; |
| } |
| } catch(e) { } |
| throw new Error(errorMsg); |
| } |
| |
| const imageBlob = await response.blob(); |
| if (!imageBlob.type.startsWith('image/')) { |
| throw new Error(t('aiGenInvalidImage')); |
| } |
|
|
| const reader = new FileReader(); |
| reader.onloadend = () => { |
| if (typeof reader.result === 'string') { |
| loadAiImageOntoCanvas(reader.result); |
| } else { |
| setAiEditError(t('failedToReadAiData')); |
| setIsGeneratingAiImage(false); |
| } |
| }; |
| reader.onerror = () => { |
| setAiEditError(t('failedToReadAiData')); |
| setIsGeneratingAiImage(false); |
| } |
| reader.readAsDataURL(imageBlob); |
|
|
| } catch (error: any) { |
| console.error('AI image generation error:', error); |
| setAiEditError(error.message || t('unknownAiError')); |
| setIsGeneratingAiImage(false); |
| } |
| }, [aiPrompt, sharedImageUrlForAi, showToast, loadAiImageOntoCanvas, aiImageQuality, aiApiEndpoint, aiDimensionsMode, currentCanvasWidth, currentCanvasHeight, clearAiOutputs, t]); |
|
|
| const handleAskAi = useCallback(async () => { |
| if (!aiPrompt.trim() || !sharedImageUrlForAi) { |
| setAiEditError(t('enterQuestion')); |
| return; |
| } |
| setIsAskingAi(true); |
| setAiEditError(null); |
| clearAiOutputs(); |
|
|
| 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 || t('unknownAskError')); |
| } finally { |
| setIsAskingAi(false); |
| } |
| }, [aiPrompt, sharedImageUrlForAi, t, clearAiOutputs]); |
| |
|
|
| const handleCancelAiEdit = () => { |
| setShowAiEditModal(false); |
| if (sharedImageUrlForAi) { |
| copyToClipboard(sharedImageUrlForAi, (msg, type) => showToast(msg,type as 'info' | 'error'), t).then(copied => { |
| if(copied) { |
| showToast(t('imageUploadedAndCopied', { url: sharedImageUrlForAi }), 'success'); |
| } |
| }); |
| } |
| setAiPrompt(''); |
| setAiEditError(null); |
| setSharedImageUrlForAi(null); |
| clearAiOutputs(); |
| }; |
|
|
| return { |
| isMagicUploading, |
| showAiEditModal, |
| aiPrompt, |
| isGeneratingAiImage, |
| sharedImageUrlForAi, |
| aiEditError, |
| isAskingAi, |
| askUrl, |
| generatedImageUrl, |
| handleMagicUpload, |
| handleGenerateAiImage, |
| handleAskAi, |
| handleCancelAiEdit, |
| setAiPrompt, |
| clearAiOutputs, |
| }; |
| }; |