import { generateImage } from '@shared/api/DataApi';
import { importImage, importImageServer } from '@shared/api/wp';
import { useImageGenerationStore } from '@shared/state/generate-images';
import {
Button,
Modal,
Notice,
Spinner,
TextareaControl,
__experimentalToggleGroupControl as ToggleGroupControl,
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
} from '@wordpress/components';
import { useRef, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { loadProduct, save, saveProduct } from '../../lib/api';
import { invalidateBlockSource } from '../../lib/block-source-cache';
import { useCmdEnterSave } from '../../lib/cmd-enter-save';
import { splice } from '../../lib/dom';
import { friendlyMessage } from '../../lib/errors';
import { track } from '../../lib/insights';
import { QE_MODAL_BODY_OPEN_CLASS } from '../../lib/modal-root';
import { pushUndo } from '../../state/undo';
import { ModalCloseButton } from './ModalCloseButton';
const readImageAttrs = (liveEl) => {
const img =
liveEl.querySelector('.wp-block-cover__image-background') ||
liveEl.querySelector('img');
if (!img) return null;
const url = img.getAttribute('src') || '';
const alt = img.getAttribute('alt') || '';
let id = null;
for (const cls of img.classList) {
const m = /^wp-image-(\d+)$/.exec(cls);
if (m) {
id = Number(m[1]);
break;
}
}
return { url, id, alt };
};
export const AiImagePickerModal = ({ selected, field, onAfterSave }) => {
const {
imageCredits,
updateImageCredits,
subtractOneCredit,
aiImageOptions,
setAiImageOption,
} = useImageGenerationStore();
const [generating, setGenerating] = useState(false);
const [applying, setApplying] = useState(false);
const [error, setError] = useState('');
const [preview, setPreview] = useState(null); // { src, id }
const abortRef = useRef(null);
const noCredits = imageCredits.remaining === 0;
const usedCredits = imageCredits.total - imageCredits.remaining;
const onGenerate = async (e) => {
e?.preventDefault?.();
setError('');
if (noCredits || !aiImageOptions.prompt) return;
try {
setGenerating(true);
subtractOneCredit();
abortRef.current = new AbortController();
const {
imageCredits: newCredits,
images,
id: gid,
} = await generateImage(aiImageOptions, abortRef.current.signal);
updateImageCredits(newCredits);
setPreview({ src: images[0].url, id: gid });
track('ai_image_generated', { size: aiImageOptions.size });
} catch (err) {
if (err?.code === 20) return; // aborted
if (!err?.imageCredits) {
setError(
err?.message || __('Image generation failed', 'extendify-local'),
);
updateImageCredits({ remaining: imageCredits.remaining });
track('ai_image_failed');
return;
}
updateImageCredits(err.imageCredits);
setError(err.message);
track('ai_image_failed', { reason: 'no_credits' });
} finally {
setGenerating(false);
}
};
const onUse = async () => {
if (!preview?.src || applying) return;
setError('');
setApplying(true);
try {
let attachment;
try {
attachment = await importImage(preview.src, {
alt: aiImageOptions.prompt,
filename: 'ai-image.jpg',
caption: '',
});
} catch (_e) {
attachment = await importImageServer(preview.src, {
alt: aiImageOptions.prompt,
caption: '',
});
}
const mediaId = attachment?.id;
if (!mediaId) throw new Error('No media id returned');
// Product image: write the featured image via WC's save
// endpoint and reload (the same image cascades to many
// surfaces — splice can't reach all of them).
if (selected.source?.kind === 'product') {
let beforeImageId = 0;
try {
const cur = await loadProduct(selected.source.id);
beforeImageId = Number(cur?.image_id) || 0;
} catch (_) {
// non-fatal — undo entry just won't have a before-state
}
await saveProduct({
productId: selected.source.id,
field: 'image',
value: mediaId,
});
if (beforeImageId && beforeImageId !== mediaId) {
pushUndo({
kind: 'product-image',
productReplay: true,
productId: selected.source.id,
field: 'image',
beforeValue: beforeImageId,
});
}
track('image_replaced', { source: 'ai', kind: 'product' });
onAfterSave(true);
return;
}
const before = readImageAttrs(selected.mediaEl ?? selected.el);
const res = await save({
source: selected.source,
blockId: selected.blockId,
blockType: selected.blockName ?? selected.blockType,
patches: [
{
fieldKey: field,
value: {
url: attachment.url || attachment.source_url,
id: mediaId,
alt: aiImageOptions.prompt || '',
},
},
],
});
if (!res.rendered) throw new Error('No rendered HTML');
const newEl = splice(selected.el, res.rendered);
if (!newEl) throw new Error('Splice failed');
invalidateBlockSource(selected.source, selected.blockId);
if (before) {
pushUndo({
kind: 'image',
source: selected.source,
blockId: selected.blockId,
blockType: selected.blockName ?? selected.blockType,
patches: [{ fieldKey: field, value: before }],
});
}
track('image_replaced', { source: 'ai' });
onAfterSave(true);
} catch (err) {
track('save_failed', { kind: 'image', source: 'ai' });
setError(friendlyMessage(err));
setApplying(false);
}
};
const onClear = () => {
setPreview(null);
setError('');
};
const onClose = () => {
abortRef.current?.abort();
onAfterSave(false);
};
useCmdEnterSave(
onGenerate,
!preview?.src && !generating && !!aiImageOptions.prompt && !noCredits,
);
return (