xpaintdev / hooks /useAiFeatures.ts
suisuyy
Refactor AI image handling in useAiFeatures hook for improved canvas integration and aspect ratio management
545f5f7
raw
history blame
9.93 kB
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 MAX_UPLOAD_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
export type AiImageQuality = 'low' | 'medium' | 'hd';
interface AiFeaturesHook {
isMagicUploading: boolean;
showAiEditModal: boolean;
aiPrompt: string;
isGeneratingAiImage: boolean;
sharedImageUrlForAi: string | null;
aiEditError: string | null;
handleMagicUpload: () => Promise<void>;
handleGenerateAiImage: () => Promise<void>;
handleCancelAiEdit: () => void;
setAiPrompt: React.Dispatch<React.SetStateAction<string>>;
}
interface UseAiFeaturesProps {
currentDataURL: string | null;
showToast: (message: string, type: ToastMessage['type']) => void;
updateCanvasState: CanvasHistoryHook['updateCanvasState'];
setZoomLevel: (zoom: number) => void;
aiImageQuality: AiImageQuality;
aiApiEndpoint: string;
currentCanvasWidth: number;
currentCanvasHeight: number;
}
export const useAiFeatures = ({
currentDataURL,
showToast,
updateCanvasState,
setZoomLevel,
aiImageQuality,
aiApiEndpoint,
currentCanvasWidth,
currentCanvasHeight,
}: 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 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));
// Calculate coordinates to center the image
const drawX = (currentCanvasWidth - drawWidth) / 2;
const drawY = (currentCanvasHeight - drawHeight) / 2;
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);
} catch (error: any) {
console.error('Magic upload error:', error);
showToast(error.message || 'Magic upload failed. Check console.', 'error');
} finally {
setIsMagicUploading(false);
}
}, [isMagicUploading, currentDataURL, showToast]);
const handleGenerateAiImage = useCallback(async () => {
if (!aiPrompt.trim() || !sharedImageUrlForAi) {
setAiEditError('Please enter a prompt and ensure an image was uploaded.');
return;
}
setIsGeneratingAiImage(true);
setAiEditError(null);
showToast('Generating AI image...', 'info');
const encodedPrompt = encodeURIComponent(aiPrompt);
const encodedImageUrl = encodeURIComponent(sharedImageUrlForAi);
let finalApiUrl = aiApiEndpoint
.replace('{prompt}', encodedPrompt)
.replace('{imgurl.url}', encodedImageUrl);
if (aiApiEndpoint.includes('pollinations.ai')) {
const pollinationsParams = new URLSearchParams({
model: 'gptimage',
private: 'true',
quality: aiImageQuality,
safe: 'false',
transparent: 'false',
width: '1024',
height: '1024',
seed: Date.now().toString(),
});
if (finalApiUrl.endsWith('/')) {
finalApiUrl = `${finalApiUrl}${encodedPrompt}?image=${encodedImageUrl}&${pollinationsParams.toString()}`;
} else if (finalApiUrl.includes('?')) {
finalApiUrl = `${finalApiUrl}&image=${encodedImageUrl}&${pollinationsParams.toString()}`;
} else {
finalApiUrl = `${finalApiUrl}?image=${encodedImageUrl}&${pollinationsParams.toString()}`;
}
}
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('<html')) {
errorMsg += ` - ${errorBody.substring(0,100)}`;
}
} catch(e) { /* ignore if can't read body */ }
throw new Error(errorMsg);
}
const imageBlob = await response.blob();
if (!imageBlob.type.startsWith('image/')) {
throw new Error('AI service did not return a valid image. Please try a different prompt or check the API endpoint.');
}
const reader = new FileReader();
reader.onloadend = () => {
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]);
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);
};
return {
isMagicUploading,
showAiEditModal,
aiPrompt,
isGeneratingAiImage,
sharedImageUrlForAi,
aiEditError,
handleMagicUpload,
handleGenerateAiImage,
handleCancelAiEdit,
setAiPrompt,
};
};