import { useImageAcquisition, useUnsplashSearch, } from '@agent/hooks/useImageAcquisition'; import { preloadImage } from '@agent/lib/replace-image'; import { useStampedPreview } from '@shared/hooks/useStampedPreview'; import { addCustomMediaViewsCss, removeCustomMediaViewsCss, } from '@shared/lib/media-views'; import { Spinner } from '@wordpress/components'; import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { close, Icon, pencil } from '@wordpress/icons'; import { MediaUpload } from '@wordpress/media-utils'; import classNames from 'classnames'; const TABS = { generate: 'generate', media: 'media', stock: 'unsplash' }; export const ImagePicker = ({ prompt = '', search = '', tab, autoGenerate = false, target, onSelect, onSubmit, onError, footer, }) => { const initialSource = TABS[tab] ?? 'generate'; const [source, setSource] = useState(initialSource); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [disclose, setDisclose] = useState(false); const [editingPrompt, setEditingPrompt] = useState(false); const [draftPrompt, setDraftPrompt] = useState(''); const [editedPrompt, setEditedPrompt] = useState(null); const generateButton = useRef(null); const onSelectRef = useRef(onSelect); onSelectRef.current = onSelect; const previewed = useRef(null); const applyPreview = useCallback(async (url) => { previewed.current = url; if (url) await preloadImage(url); await onSelectRef.current?.(url); }, []); const { states, imageCredits, generate, attachImage, pickUnsplash, markAwaiting, resolveImage, } = useImageAcquisition({ source: 'agent', onUrlReady: (_key, url) => applyPreview(url), }); const state = states[source]; const ready = state?.status === 'ready'; const activeUrl = state?.image?.url ?? state?.url ?? null; const previewUrl = useStampedPreview( activeUrl, disclose && source === 'generate', ); // wp.media rebuilds its frame on open, leaving any still-open modal behind empty. const openedModal = useRef(null); const closeMediaModal = useCallback(() => { const remembered = openedModal.current; openedModal.current = null; // A rebuilt frame takes the class with it, so this lookup misses the shell. const mounted = document .querySelector('.image__media-modal') ?.closest('.media-modal'); for (const modal of new Set([remembered, mounted].filter(Boolean))) { modal.querySelector('.media-modal-close')?.click(); } }, []); const openMediaModal = useCallback( (open) => { closeMediaModal(); open(); openedModal.current = document .querySelector('.image__media-modal') ?.closest('.media-modal') ?? null; }, [closeMediaModal], ); // The picker modal mounts under the Agent without these. useEffect(() => { addCustomMediaViewsCss(); const style = document.createElement('style'); style.textContent = '.media-modal { z-index: 999999 !important; }'; document.head.appendChild(style); return () => { closeMediaModal(); removeCustomMediaViewsCss(); style.remove(); }; }, [closeMediaModal]); // The first title segment reads as the page name ("Hoodie – Iron Forge Gym"). const pageName = document.title.split(/[|–—]/)[0].trim(); const stockSeed = search || pageName; const generatePrompt = editedPrompt ?? (prompt || pageName); // Re-renders must not re-kick generation and burn extra credits. const started = useRef(false); useEffect(() => { // The image endpoint 400s on an empty prompt; only an asked-for one counts. if (started.current || !autoGenerate || !prompt) return; started.current = true; generate(initialSource, prompt, target?.()); }, [autoGenerate, generate, initialSource, prompt, target]); useEffect(() => { if (previewed.current === previewUrl) return; applyPreview(previewUrl); }, [previewUrl, applyPreview]); const switchSource = (next) => { // Left open, the media modal covers the tab just switched to. closeMediaModal(); setSource(next); }; const submit = async () => { if (busy) return; setBusy(true); setError(null); try { await onSubmit(await resolveImage(source, { disclose })); } catch (failure) { setBusy(false); // Handing it back lets the caller's own flow react — the agent sends // it to the model rather than parking the user on a dead picker. if (onError) return onError(failure); setError( failure?.message || __('Failed to save image', 'extendify-local'), ); } }; return (
{label}
}{__('No images found.', 'extendify-local')}