# WP Agentic Writer: Image Generation & Selection Flow ## Executive Summary This document defines the **complete lifecycle** of images from agent recommendation → generation → variant management → final WordPress Media upload. **Key principle:** Regenerate creates NEW variants (doesn't delete old ones). All temp images belong to a post. Users see variants in modal, select one, and commit to WordPress Media with recommended alt text. --- ## Table of Contents 1. [Overview & Architecture](#overview--architecture) 2. [Data Model](#data-model) 3. [Flow 1: Article Generation & Image Recommendations](#flow-1-article-generation--image-recommendations) 4. [Flow 2: Image Block Toolbar & Modal](#flow-2-image-block-toolbar--modal) 5. [Flow 3: Image Generation (Variants)](#flow-3-image-generation-variants) 6. [Flow 4: Variant Selection & Media Upload](#flow-4-variant-selection--media-upload) 7. [Flow 5: Temp Image Management](#flow-5-temp-image-management) 8. [Admin Page: Image Library](#admin-page-image-library) 9. [REST API Endpoints](#rest-api-endpoints) 10. [Implementation Checklist](#implementation-checklist) --- ## Overview & Architecture ### Core concept ``` ┌─────────────────────────────────────────────────────────────────┐ │ WRITING AGENT generates article + 3 image recommendations │ └────────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ Plugin converts recommendations → 3 core/image blocks │ │ Each block has: data-agent-image-id="img_X" │ │ Stores recommendations in wp_agentic_images table │ └────────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ USER EDITS IN GUTENBERG EDITOR │ │ │ │ [Image block] [Image block] [Image block] │ │ ↓ Generate ↓ Generate ↓ Generate │ │ ↓ (Toolbar btn) ↓ (Toolbar btn) ↓ (Toolbar btn) │ │ │ │ Each opens YOUR modal with prompt + alt editable │ └────────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ USER ACTIONS IN MODAL │ │ │ │ [View Prompt] [Edit Prompt] │ │ [View Alt] [Edit Alt] │ │ [Generate] → generates 1-3 variants │ │ stores ALL in /wp-content/agentic-writer-temp/ │ │ │ │ [Regenerate] → generates MORE variants (doesn't delete old) │ │ adds to same image_id pool │ │ │ │ [Use Media Library] → opens core media modal │ │ with pre-filled alt suggestion │ └────────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ USER SELECTS A VARIANT │ │ │ │ [Variant 1] [Variant 2] [Variant 3] [Variant 4] │ │ [Select] [Select] [Select] [Select] │ │ │ │ Other variants stay in temp folder + DB │ └────────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ BACKEND: Commit Selected Variant to WP Media │ │ │ │ 1. media_handle_sideload(temp_image_path) │ │ 2. Set attachment alt → recommended alt (or user-edited) │ │ 3. Update wp_agentic_images: status='committed' │ │ attachment_id=123 │ │ 4. Return attachment ID + URL │ └────────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ FRONTEND: Update Gutenberg Image Block │ │ │ │ updateBlockAttributes(block.clientId, { │ │ id: attachment_id, │ │ url: attachment_url, │ │ alt: recommended_alt │ │ }) │ │ │ │ Remove data-agent-image-id (no longer a placeholder) │ └────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ TEMP IMAGE CLEANUP (Manual + Auto) │ │ │ │ Admin page "Generated Images" tab shows: │ │ - All temp images (by post, by status) │ │ - [Delete selected] [Auto-cleanup old (>7 days)] │ │ │ │ Cron job: wp_schedule_event() → delete temps > 7 days │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Data Model ### Table: `wp_agentic_images` Stores recommendations + generation history for each image per post. ```sql CREATE TABLE wp_agentic_images ( id BIGINT AUTO_INCREMENT PRIMARY KEY, post_id BIGINT NOT NULL, -- Recommendation from agent agent_image_id VARCHAR(50) NOT NULL, -- e.g., "img_hero_1" placement VARCHAR(100), -- "intro_hero", "after_section_2" section_title VARCHAR(255), -- "Introduction to n8n" -- Original recommendation prompt_initial TEXT NOT NULL, -- Agent's initial prompt alt_text_initial TEXT, -- Agent's suggested alt -- User edits (nullable) prompt_edited TEXT, -- Null if user didn't edit alt_text_edited TEXT, -- Null if user didn't edit -- Committed image (when user selects a variant) attachment_id BIGINT, -- WP attachment ID (null until committed) status VARCHAR(30) DEFAULT 'pending', -- pending, generating, committed, discarded -- Cost tracking cost_estimate DECIMAL(10, 4), -- Based on image model pricing cost_actual DECIMAL(10, 4), -- Updated after generation image_model VARCHAR(100), -- Which model was used -- Metadata created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_post (post_id), KEY idx_agent_image_id (post_id, agent_image_id), KEY idx_status (status), KEY idx_created (created_at) ); ``` ### Table: `wp_agentic_images_variants` Tracks all generated variants (temp images) for each agent_image_id. ```sql CREATE TABLE wp_agentic_images_variants ( id BIGINT AUTO_INCREMENT PRIMARY KEY, -- Reference to main image record agentic_image_id BIGINT NOT NULL, post_id BIGINT NOT NULL, agent_image_id VARCHAR(50) NOT NULL, -- e.g., "img_hero_1" -- Variant details variant_number INT DEFAULT 1, -- 1st, 2nd, 3rd generation attempt temp_file_path VARCHAR(500) NOT NULL, -- /wp-content/agentic-writer-temp/xxx.jpg temp_file_url VARCHAR(500) NOT NULL, -- URL to temp image file_size INT, -- In bytes -- Generation details prompt_used TEXT, -- Exact prompt sent to image model image_model_used VARCHAR(100), -- Which model generated this generation_time INT, -- Seconds to generate cost DECIMAL(10, 4), -- Cost of this generation -- Selection status is_selected TINYINT DEFAULT 0, -- 1 if user selected this variant selected_at TIMESTAMP NULL, -- Lifecycle status VARCHAR(30) DEFAULT 'temp', -- temp, selected, discarded, auto_deleted created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP NULL, KEY idx_agentic_image (agentic_image_id), KEY idx_post (post_id), KEY idx_status (status), KEY idx_created (created_at) ); ``` ### File system: Temp images ``` /wp-content/agentic-writer-temp/ ├── post_/ │ ├── img_hero_1/ │ │ ├── variant_1.jpg │ │ ├── variant_2.jpg │ │ └── variant_3.jpg │ ├── img_diag_1/ │ │ └── variant_1.jpg │ └── img_section_2/ │ ├── variant_1.jpg │ └── variant_2.jpg └── [cleanup cron removes files 7+ days old] ``` --- ## Flow 1: Article Generation & Image Recommendations ### What the agent returns After writing article, agent provides JSON response: ```json { "status": "article_complete", "article_blocks": [ { "blockName": "core/paragraph", "attrs": {}, "innerBlocks": [], "innerHTML": "

Introduction text...

" }, { "blockName": "core/image", "attrs": { "id": null, "url": null, "alt": "", "data-agent-image-id": "img_hero_1" } } ], "images": [ { "agent_image_id": "img_hero_1", "placement": "intro_hero", "section_title": "Introduction", "prompt": "N8n workflow automation dashboard...", "alt": "N8n automation dashboard with workflow nodes", "image_model": "sourceful/riverflow-v2-max" }, { "agent_image_id": "img_diag_1", "placement": "after_section_2", "section_title": "How Workflows Run", "prompt": "Workflow architecture diagram...", "alt": "Workflow trigger-condition-action diagram", "image_model": "sourceful/riverflow-v2-max" }, { "agent_image_id": "img_section_4", "placement": "before_conclusion", "section_title": "Real-world Example", "prompt": "Developer using N8n dashboard...", "alt": "Developer working with N8n automation dashboard", "image_model": "sourceful/riverflow-v2-max" } ] } ``` ### Backend handling ```php $post_id, 'post_content' => $post_content, 'post_status' => 'draft' ]); // 2. Save each image recommendation foreach ( $agent_response['images'] as $image_spec ) { self::save_image_recommendation( $post_id, $image_spec ); } // 3. Return success message for chat return [ 'status' => 'article_complete', 'post_id' => $post_id, 'message' => sprintf( 'Article created. %d image suggestions ready in the Images panel.', count( $agent_response['images'] ) ) ]; } /** * Store individual image recommendation */ private static function save_image_recommendation( $post_id, $image_spec ) { global $wpdb; $wpdb->insert( $wpdb->prefix . 'agentic_images', [ 'post_id' => $post_id, 'agent_image_id' => $image_spec['agent_image_id'], 'placement' => $image_spec['placement'], 'section_title' => $image_spec['section_title'], 'prompt_initial' => $image_spec['prompt'], 'alt_text_initial' => $image_spec['alt'], 'image_model' => $image_spec['image_model'], 'status' => 'pending' ], [ '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ] ); } ``` --- ## Flow 2: Image Block Toolbar & Modal ### Block toolbar button In Gutenberg, each `core/image` block with `data-agent-image-id` gets a toolbar button: ```jsx // registerPlugin('agentic-image-toolbar', { // render() { // return // } // }) function ImageBlockToolbar() { const { selectedBlockClientId } = useSelect(blockEditorStore); const block = useSelect( select => select(blockEditorStore).getBlock(selectedBlockClientId), [selectedBlockClientId] ); if (!block || block.name !== 'core/image') return null; const agentImageId = block.attributes['data-agent-image-id']; if (!agentImageId) return null; // Not an agent placeholder return ( openImageModal(agentImageId, block)} icon="image" /> ); } /** * Opens YOUR custom modal (not WP media modal) */ function openImageModal(agentImageId, block) { wp.data.dispatch('agentic-writer').openImageGenerationModal({ agentImageId, blockClientId: block.clientId, postId: wp.data.select('core/editor').getCurrentPostId() }); } ``` ### Your custom modal ```jsx function ImageGenerationModal({ agentImageId, blockClientId, postId }) { const [prompt, setPrompt] = useState(initialPrompt); const [alt, setAlt] = useState(initialAlt); const [variants, setVariants] = useState([]); const [isGenerating, setIsGenerating] = useState(false); const [step, setStep] = useState('edit'); // 'edit' | 'generating' | 'select' const handleGenerate = async () => { setIsGenerating(true); setStep('generating'); try { const response = await fetch('/wp-json/agentic-writer/v1/generate-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ post_id: postId, agent_image_id: agentImageId, prompt: prompt, // User-edited if changed alt: alt // User-edited if changed }) }); const result = await response.json(); setVariants(result.variants); setStep('select'); } catch (error) { console.error('Generation failed:', error); } finally { setIsGenerating(false); } }; const handleRegenerate = async () => { // Re-generate: creates MORE variants // Does NOT delete existing ones await handleGenerate(); }; const handleSelect = async (variantId) => { // Commit this variant to WP Media const response = await fetch('/wp-json/agentic-writer/v1/commit-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ post_id: postId, agent_image_id: agentImageId, variant_id: variantId, alt: alt // Final alt text }) }); const result = await response.json(); // Update Gutenberg block wp.data.dispatch('core/block-editor').updateBlockAttributes( blockClientId, { id: result.attachment_id, url: result.attachment_url, alt: result.alt, 'data-agent-image-id': undefined // Remove placeholder marker } ); // Close modal wp.data.dispatch('agentic-writer').closeImageGenerationModal(); }; if (step === 'edit') { return (