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 (
{__('Image generated successfully!', 'extendify-local')}