import { walkAndUpdateImageDetails } from '@agent/lib/blocks'; import { useChatStore } from '@agent/state/chat'; import { useQuickEditStore } from '@quick-edit/state/store'; import { generateImage } from '@shared/api/DataApi'; import { downloadImage } from '@shared/api/wp'; import { useImageGenerationStore } from '@shared/state/generate-images'; import { registerCoreBlocks } from '@wordpress/block-library'; import { getBlockTypes } from '@wordpress/blocks'; import { Spinner } from '@wordpress/components'; import { humanTimeDiff } from '@wordpress/date'; import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; const preload = (src) => new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(src); img.onerror = reject; img.src = src; }); export const GenerateImageConfirm = ({ inputs, onConfirm, onCancel, onRetry, }) => { const [generatedImage, setGeneratedImage] = useState(null); const [generating, setGenerating] = useState(false); const [error, setError] = useState(null); const [refreshCheck, setRefreshCheck] = useState(0); const [importing, setImporting] = useState(false); const { imageCredits, updateImageCredits, subtractOneCredit, resetImageCredits, } = useImageGenerationStore(); const { addMessage, messages } = useChatStore(); const block = useQuickEditStore((s) => s.agentBlock); const noCredits = Number(imageCredits.remaining) === 0; const generatingImageRef = useRef(false); const confirmed = useRef(false); const handleConfirm = async () => { if (importing) return; confirmed.current = true; setImporting(true); try { const importedImage = await downloadImage( null, generatedImage, 'ai-generated', ); await onConfirm({ data: { previousContent: inputs.previousContent, newContent: walkAndUpdateImageDetails(inputs, importedImage), }, shouldRefreshPage: true, }); } catch (err) { confirmed.current = false; setImporting(false); setError(err?.message || __('Failed to save image', 'extendify-local')); } }; const resetImagePreview = useCallback(() => { if (!generatedImage) return false; // The CSS.escape() method can also be used for escaping strings // https://developer.mozilla.org/en-US/docs/Web/API/CSS/escape_static const imageElement = document.querySelector( `[data-extendify-agent-block-id="${block?.id}"] img[src="${CSS.escape( generatedImage, )}"]`, ); if (imageElement) { imageElement.src = inputs.url.replaceAll(',', '%2C'); return true; } return false; }, [block?.id, generatedImage, inputs.url]); useEffect(() => { if (!generatedImage) return; return () => { if (!confirmed.current) resetImagePreview(); }; }, [generatedImage]); const handleRetry = useCallback(() => { resetImagePreview(); onRetry(); }, [onRetry, resetImagePreview]); useEffect(() => { if (getBlockTypes().length !== 0) return; registerCoreBlocks(); }, []); useEffect(() => { if (generatedImage || noCredits || generatingImageRef.current) return; generatingImageRef.current = true; setGenerating(true); subtractOneCredit(); const generate = async () => { try { const { imageCredits, images } = await generateImage({ prompt: inputs.prompt, source: 'agent', }); updateImageCredits(imageCredits); const url = images?.[0]?.url; if (!url) throw new Error(__('No image returned', 'extendify-local')); await preload(url); setGeneratedImage(url); } catch (e) { setError( e?.message || __('An unknown error occurred.', 'extendify-local'), ); if (e?.imageCredits) updateImageCredits(e.imageCredits); generatingImageRef.current = false; } finally { setGenerating(false); } }; generate(); }, [ inputs.prompt, generatedImage, noCredits, subtractOneCredit, updateImageCredits, ]); // Copied from Draft. Maybe not the best way to do this. useEffect(() => { const handle = () => { setRefreshCheck((prev) => prev + 1); if (!imageCredits.refresh) return; if (new Date(Number(imageCredits.refresh)) > new Date()) return; resetImageCredits(); }; if (refreshCheck === 0) handle(); // First run const id = setTimeout(handle, 1000); return () => clearTimeout(id); }, [imageCredits, resetImageCredits, refreshCheck]); useEffect(() => { if (!generatedImage) return; const originalImage = document.querySelector( `[data-extendify-agent-block-id="${block?.id}"] img[src="${CSS.escape( inputs.url.replaceAll(',', '%2C'), )}"]`, ); if (!originalImage) return; originalImage.srcset = ''; // replace the original image source with the new image url originalImage.src = generatedImage; }, [generatedImage, inputs.url, block?.id]); useEffect(() => { if (!error) return; const timer = setTimeout(() => onCancel(), 100); const content = sprintf( // translators: A chat message shown to the user __('Error generating image: %s', 'extendify-local'), error, ); const last = messages.at(-1)?.details?.content; if (content === last) return () => clearTimeout(timer); addMessage('message', { role: 'assistant', content, error: true }); return () => clearTimeout(timer); }, [error, onCancel, addMessage, messages]); useEffect(() => { if (!noCredits || generating || generatedImage) return; const cancelTimer = setTimeout(() => onCancel(), 1500); const content = sprintf( // translator: %s is the time until credits reset. __( "It looks like you've run out of credits. They'll refresh in %s, so check back then.", 'extendify-local', ), humanTimeDiff(new Date(Number(imageCredits.refresh))), ); const last = messages.at(-1)?.details?.content; if (content === last) return () => clearTimeout(cancelTimer); const messageTimer = setTimeout(() => { addMessage('message', { role: 'assistant', content, error: true }); }, 1000); return () => { clearTimeout(cancelTimer); clearTimeout(messageTimer); }; }, [ noCredits, generating, generatedImage, onCancel, addMessage, messages, imageCredits.refresh, ]); if (error || (!generatedImage && noCredits && !generating)) return null; if (generating) { return (

{__('Generating image...', 'extendify-local')}

); } return (

{__('Image generated successfully!', 'extendify-local')}

{sprintf( // translators: %1$s is the number of credits remaining, %2$s is the total credits __( 'You have %1$s of %2$s daily image credits remaining.', 'extendify-local', ), imageCredits.remaining, imageCredits.total, )}
); }; const Wrapper = ({ children }) => (
{children}
); const Content = ({ children }) => (
{children}
);