/** * Image Generation Modal Component * * Handles image review, generation, variant selection, and commitment. * * @package WP_Agentic_Writer */ (function() { const { Modal, Button, Spinner, TextControl, TextareaControl } = wp.components; const { useState, useEffect, render } = wp.element; window.wpAgenticWriter = window.wpAgenticWriter || {}; /** * Image Review Modal * Shows after article generation with image recommendations */ window.wpAgenticWriter.ImageReviewModal = function({ postId, initialImageId, onClose, onComplete }) { const [step, setStep] = useState('loading'); const [images, setImages] = useState([]); const [selectedImages, setSelectedImages] = useState([]); const [variantCounts, setVariantCounts] = useState({}); const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(null); useEffect(() => { loadImageRecommendations(); }, []); const loadImageRecommendations = async () => { try { const response = await fetch( `${wpAgenticWriter.apiUrl}/image-recommendations/${postId}`, { headers: { 'X-WP-Nonce': wpAgenticWriter.nonce, }, } ); if (!response.ok) { throw new Error('Failed to load image recommendations'); } const data = await response.json(); const imgs = data.images || []; setImages(imgs); const initialCounts = {}; imgs.forEach(img => { initialCounts[img.agent_image_id] = 2; }); setVariantCounts(initialCounts); setStep('review'); } catch (err) { setError(err.message); setStep('review'); } }; const handleEditPrompt = (imageId, newPrompt) => { setImages(prev => prev.map(img => img.agent_image_id === imageId ? { ...img, prompt_edited: newPrompt } : img )); }; const handleEditAlt = (imageId, newAlt) => { setImages(prev => prev.map(img => img.agent_image_id === imageId ? { ...img, alt_text_edited: newAlt } : img )); }; const handleVariantCountChange = (imageId, count) => { setVariantCounts(prev => ({ ...prev, [imageId]: parseInt(count, 10) })); }; const calculateTotalCost = () => { const settings = wpAgenticWriter.settings || {}; const imageModel = settings.image_model || 'sourceful/riverflow-v2-max'; const costPerImage = { 'black-forest-labs/flux.2-klein': 0.02, 'sourceful/riverflow-v2-max': 0.03, 'black-forest-labs/flux.2-max': 0.15, }; const baseCost = costPerImage[imageModel] || 0.03; let total = 0; selectedImages.forEach(imageId => { const count = variantCounts[imageId] || 1; total += baseCost * count; }); return total.toFixed(3); }; const handleGenerateSelected = async () => { if (selectedImages.length === 0) { alert('Please select at least one image to generate'); return; } setIsGenerating(true); setStep('generating'); try { for (const imageId of selectedImages) { const image = images.find(img => img.agent_image_id === imageId); const response = await fetch( `${wpAgenticWriter.apiUrl}/generate-image`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': wpAgenticWriter.nonce, }, body: JSON.stringify({ post_id: postId, agent_image_id: imageId, prompt: image.prompt_edited || image.prompt_initial, alt: image.alt_text_edited || image.alt_text_initial, variant_count: variantCounts[imageId] || 1, }), } ); if (!response.ok) { throw new Error(`Failed to generate image: ${imageId}`); } const result = await response.json(); setImages(prev => prev.map(img => img.agent_image_id === imageId ? { ...img, variants: result.variants } : img )); } setStep('selecting'); } catch (err) { setError(err.message); setStep('review'); } finally { setIsGenerating(false); } }; const handleSelectVariant = async (imageId, variantId) => { const image = images.find(img => img.agent_image_id === imageId); try { const response = await fetch( `${wpAgenticWriter.apiUrl}/commit-image`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': wpAgenticWriter.nonce, }, body: JSON.stringify({ post_id: postId, agent_image_id: imageId, variant_id: variantId, alt: image.alt_text_edited || image.alt_text_initial, }), } ); if (!response.ok) { throw new Error('Failed to commit image'); } const result = await response.json(); updateGutenbergBlock(imageId, result); setImages(prev => prev.map(img => img.agent_image_id === imageId ? { ...img, status: 'committed', attachment_id: result.attachment_id } : img )); } catch (err) { alert('Failed to commit image: ' + err.message); } }; const updateGutenbergBlock = (agentImageId, attachmentData) => { const blocks = wp.data.select('core/block-editor').getBlocks(); const findAndUpdateBlock = (blocks) => { for (const block of blocks) { if (block.name === 'core/image' && block.attributes['data-agent-image-id'] === agentImageId) { wp.data.dispatch('core/block-editor').updateBlockAttributes( block.clientId, { id: attachmentData.attachment_id, url: attachmentData.attachment_url, alt: attachmentData.alt, 'data-agent-image-id': undefined, } ); return true; } if (block.innerBlocks && block.innerBlocks.length > 0) { if (findAndUpdateBlock(block.innerBlocks)) { return true; } } } return false; }; findAndUpdateBlock(blocks); }; if (step === 'loading') { return wp.element.createElement(Modal, { title: 'Loading Image Recommendations', onRequestClose: onClose, }, wp.element.createElement('div', { style: { padding: '20px', textAlign: 'center' } }, wp.element.createElement(Spinner) ) ); } if (step === 'review') { return wp.element.createElement(Modal, { title: `Image Recommendations (${images.length})`, onRequestClose: onClose, style: { maxWidth: '800px' }, }, wp.element.createElement('div', { className: 'wpaw-image-review' }, error && wp.element.createElement('div', { className: 'notice notice-error', style: { marginBottom: '20px' } }, error), images.length === 0 && wp.element.createElement('div', { style: { padding: '40px 20px', textAlign: 'center', color: '#666', } }, wp.element.createElement('p', { style: { fontSize: '16px', marginBottom: '10px' } }, '📷 No image recommendations available' ), wp.element.createElement('p', { style: { fontSize: '14px', marginBottom: '20px' } }, 'Images are generated during article writing. You can add images manually or generate them later.' ), wp.element.createElement(Button, { variant: 'primary', onClick: onClose, }, 'Continue Without Images') ), images.map(image => wp.element.createElement('div', { key: image.agent_image_id, className: 'wpaw-image-card', style: { border: '1px solid #ddd', padding: '15px', marginBottom: '15px', borderRadius: '4px', }, }, wp.element.createElement('h3', null, `Image: ${image.section_title || image.placement}` ), wp.element.createElement(TextareaControl, { label: 'Prompt', value: image.prompt_edited || image.prompt_initial, onChange: (value) => handleEditPrompt(image.agent_image_id, value), rows: 3, }), wp.element.createElement(TextControl, { label: 'Alt Text', value: image.alt_text_edited || image.alt_text_initial, onChange: (value) => handleEditAlt(image.agent_image_id, value), }), wp.element.createElement('div', { style: { marginTop: '10px', marginBottom: '10px' } }, wp.element.createElement('label', { style: { display: 'block', marginBottom: '5px', fontWeight: '600' } }, 'Variant Count'), wp.element.createElement('select', { value: variantCounts[image.agent_image_id] || 2, onChange: (e) => handleVariantCountChange(image.agent_image_id, e.target.value), style: { padding: '5px', borderRadius: '3px', border: '1px solid #ddd', } }, wp.element.createElement('option', { value: '1' }, '1 variant'), wp.element.createElement('option', { value: '2' }, '2 variants'), wp.element.createElement('option', { value: '3' }, '3 variants') ), wp.element.createElement('p', { style: { fontSize: '12px', color: '#666', margin: '5px 0 0' } }, `Cost: ~$${((variantCounts[image.agent_image_id] || 2) * 0.03).toFixed(3)}`) ), wp.element.createElement('label', null, wp.element.createElement('input', { type: 'checkbox', checked: selectedImages.includes(image.agent_image_id), onChange: (e) => { if (e.target.checked) { setSelectedImages(prev => [...prev, image.agent_image_id]); } else { setSelectedImages(prev => prev.filter(id => id !== image.agent_image_id)); } }, }), ' Generate this image' ) ) ), wp.element.createElement('div', { style: { marginTop: '20px', display: 'flex', gap: '10px', justifyContent: 'flex-end', } }, wp.element.createElement(Button, { variant: 'secondary', onClick: onClose, }, 'Skip Images'), wp.element.createElement(Button, { variant: 'primary', onClick: handleGenerateSelected, disabled: selectedImages.length === 0 || isGenerating, }, `Generate ${selectedImages.length} Image(s) (~$${calculateTotalCost()})`) ) ) ); } if (step === 'generating') { return wp.element.createElement(Modal, { title: 'Generating Images', onRequestClose: () => {}, }, wp.element.createElement('div', { style: { padding: '20px', textAlign: 'center' } }, wp.element.createElement(Spinner), wp.element.createElement('p', null, `Generating images... This may take a minute.` ), wp.element.createElement('p', { style: { fontSize: '12px', color: '#666' } }, `Estimated cost: $${calculateTotalCost()}` ) ) ); } if (step === 'selecting') { return wp.element.createElement(Modal, { title: 'Select Image Variants', onRequestClose: onClose, style: { maxWidth: '900px' }, }, wp.element.createElement('div', { className: 'wpaw-variant-selection' }, images .filter(img => img.variants && img.variants.length > 0) .map(image => wp.element.createElement('div', { key: image.agent_image_id, style: { marginBottom: '30px' }, }, wp.element.createElement('h3', null, image.section_title), wp.element.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '15px', }, }, image.variants.map(variant => wp.element.createElement('div', { key: variant.id, style: { border: '1px solid #ddd', borderRadius: '4px', overflow: 'hidden', }, }, wp.element.createElement('img', { src: variant.temp_file_url, alt: 'Variant', style: { width: '100%', display: 'block' }, }), wp.element.createElement('div', { style: { padding: '10px' } }, wp.element.createElement('p', { style: { fontSize: '12px', margin: '0 0 10px' } }, `Cost: $${variant.cost.toFixed(3)} • ${variant.generation_time}s` ), wp.element.createElement(Button, { variant: 'primary', onClick: () => handleSelectVariant(image.agent_image_id, variant.id), style: { width: '100%' }, }, 'Select') ) ) ) ) ) ), wp.element.createElement('div', { style: { marginTop: '20px', textAlign: 'right' }, }, wp.element.createElement(Button, { variant: 'secondary', onClick: onComplete, }, 'Done') ) ) ); } }; // Initialize modal container and event listeners let modalContainer = null; let currentModalInstance = null; /** * Open image modal for review after article generation */ window.addEventListener('wpaw:open-image-review-modal', (event) => { const { postId, imageCount } = event.detail; if (!modalContainer) { modalContainer = document.createElement('div'); modalContainer.id = 'wpaw-image-modal-root'; document.body.appendChild(modalContainer); } currentModalInstance = render( wp.element.createElement(window.wpAgenticWriter.ImageReviewModal, { postId: postId, onClose: () => { if (modalContainer) { render(null, modalContainer); currentModalInstance = null; } }, onComplete: () => { if (modalContainer) { render(null, modalContainer); currentModalInstance = null; } }, }), modalContainer ); }); /** * Open image modal for single image from toolbar */ window.addEventListener('wpaw:open-image-modal', (event) => { const { agentImageId, blockId } = event.detail; const postId = wp.data.select('core/editor').getCurrentPostId(); if (!modalContainer) { modalContainer = document.createElement('div'); modalContainer.id = 'wpaw-image-modal-root'; document.body.appendChild(modalContainer); } currentModalInstance = render( wp.element.createElement(window.wpAgenticWriter.ImageReviewModal, { postId: postId, initialImageId: agentImageId, onClose: () => { if (modalContainer) { render(null, modalContainer); currentModalInstance = null; } }, onComplete: () => { if (modalContainer) { render(null, modalContainer); currentModalInstance = null; } }, }), modalContainer ); }); })();