# WP Agentic Writer: Image Generation Implementation Plan **Document Version:** 1.1 **Date:** January 28, 2026 **Status:** Ready for Implementation **Estimated Time:** 16-20 hours **Changelog v1.1:** - Updated table names to use `wp_wpaw_` prefix (consistent with existing tables) - Changed temp folder location to `/wp-content/uploads/wpaw/{post_id}/` - Added user-controlled variant count setting (1-3 variants per image) --- ## Executive Summary This document provides a comprehensive implementation plan for adding AI-powered image generation to WP Agentic Writer. The implementation follows **Option A (Cost-Optimized with User Control)** from the recommendations, ensuring maximum cost efficiency and quality control. ### Key Features - ✅ **Cost-optimized flow:** Analysis + prompts cost ~$0.002, user controls image generation - ✅ **Model-specific prompts:** Tailored for FLUX.2 klein (Budget), Riverflow V2 Max (Balanced), FLUX.2 max (Premium) - ✅ **Variant management:** User controls variant count (1-3), selects best one - ✅ **WordPress integration:** Direct upload to Media Library with alt text - ✅ **Temp file management:** Automatic cleanup of unused variants --- ## Table of Contents 1. [Current State Analysis](#current-state-analysis) 2. [Architecture Overview](#architecture-overview) 3. [Database Schema](#database-schema) 4. [Implementation Phases](#implementation-phases) 5. [Phase 1: Database & Core Infrastructure](#phase-1-database--core-infrastructure) 6. [Phase 2: Backend Image Generation](#phase-2-backend-image-generation) 7. [Phase 3: Frontend UI & Modal](#phase-3-frontend-ui--modal) 8. [Phase 4: Gutenberg Integration](#phase-4-gutenberg-integration) 9. [Phase 5: Temp File Management](#phase-5-temp-file-management) 10. [Phase 6: Testing & Polish](#phase-6-testing--polish) 11. [Configuration & Settings](#configuration--settings) 12. [Cost Tracking Integration](#cost-tracking-integration) 13. [Security Considerations](#security-considerations) 14. [Rollout Strategy](#rollout-strategy) --- ## Current State Analysis ### ✅ What We Have 1. **Image Model Configuration** - Settings page has image model selector - Default: `openai/gpt-4o` - Preset support: Budget/Balanced/Premium all use GPT-4o - Model stored in `wp_agentic_writer_settings['image_model']` 2. **Image Placeholder Support** - Writing agent can suggest images using `[IMAGE: description]` format - Markdown parser converts `[IMAGE: ...]` to `core/image` blocks - Post config has `include_images` boolean flag 3. **OpenRouter Provider** - Has `generate_image()` method (basic implementation) - Uses chat completion for images (not optimal) - No variant support, no temp file management 4. **Cost Tracking** - Existing table: `wp_wpaw_cost_tracking` - Tracks model, action, tokens, cost per request - Can be extended for image generation costs ### ❌ What We Need 1. **Database Tables** - `wp_wpaw_images` - Image recommendations and metadata - `wp_wpaw_images_variants` - Generated image variants 2. **Image Generation Flow** - Analyze article for placement - Generate model-specific prompts - Generate image variants via OpenRouter - Download and store temp files - Upload selected variant to WordPress Media 3. **Frontend UI** - Image review modal after article generation - Variant selection interface - Gutenberg block toolbar integration - Progress indicators 4. **File Management** - Temp directory: `/wp-content/uploads/wpaw/{post_id}/` - Automatic cleanup (7+ days) - Cron job for maintenance 5. **User Controls** - Variant count selector (1-3 variants per image) - Cost preview before generation - Per-image generation control 6. **Model-Specific Prompting** - FLUX.2 klein: Simple 1-2 sentence prompts - Riverflow V2 Max: Detailed 3-4 sentence prompts - FLUX.2 max: Complex 4-6 sentence prompts --- ## Architecture Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ ARTICLE GENERATION COMPLETE │ │ (Writing agent finishes, returns markdown with [IMAGE: ...])│ └────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ AUTOMATIC: Analyze & Generate Prompts │ │ • analyze_article_for_images() → placement points │ │ • generate_image_prompts() → 3 image specs │ │ • Store in wp_wpaw_images table │ │ Cost: ~$0.002 (uses writing model) │ └────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ FRONTEND: Image Review Modal │ │ • Show 3 image recommendations │ │ • Display prompts (editable) │ │ • Display alt text (editable) │ │ • Variant count selector (1-3 per image) │ │ • Show cost estimate per image × variant count │ │ • [Generate All] [Generate Selected] [Skip] │ └────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ USER SELECTS: Generate Image(s) │ │ • Choose variant count (1-3) per image │ │ • Click [Generate] on individual image │ │ • OR click [Generate All] │ └────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ BACKEND: Generate Variants │ │ • Call OpenRouter image generation API │ │ • Generate N variants (user-specified: 1-3) │ │ • Download to /wp-content/uploads/wpaw/{post_id}/ │ │ • Store in wp_wpaw_images_variants table │ │ • Return variant URLs to frontend │ └────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ FRONTEND: Variant Selection Modal │ │ • Display all variants in grid │ │ • Show generation info (cost, time, model) │ │ • [Select] button per variant │ │ • [Regenerate] for more variants │ └────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ BACKEND: Commit to WordPress Media │ │ • media_handle_sideload() - upload to Media Library │ │ • Set alt text from recommendation │ │ • Update wp_wpaw_images: status='committed' │ │ • Return attachment ID + URL │ └────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ FRONTEND: Update Gutenberg Block │ │ • updateBlockAttributes() with attachment ID/URL │ │ • Remove data-agent-image-id placeholder marker │ │ • Image now permanent in WordPress │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Database Schema ### Table 1: `wp_wpaw_images` Stores image recommendations from the writing agent. ```sql CREATE TABLE IF NOT EXISTS `wp_wpaw_images` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `post_id` bigint(20) NOT NULL, -- Recommendation from agent `agent_image_id` varchar(50) NOT NULL, -- e.g., "img_hero_1" `placement` varchar(100) DEFAULT NULL, -- "intro_hero", "after_section_2" `section_title` varchar(255) DEFAULT NULL, -- "Introduction to n8n" -- Original recommendation `prompt_initial` text NOT NULL, -- Agent's initial prompt `alt_text_initial` text DEFAULT NULL, -- Agent's suggested alt -- User edits (nullable) `prompt_edited` text DEFAULT NULL, -- NULL if user didn't edit `alt_text_edited` text DEFAULT NULL, -- NULL if user didn't edit -- Committed image (when user selects a variant) `attachment_id` bigint(20) DEFAULT NULL, -- WP attachment ID (null until committed) `status` varchar(30) DEFAULT 'pending', -- pending, generating, committed, discarded -- Cost tracking `cost_estimate` decimal(10, 4) DEFAULT NULL, -- Based on image model pricing `cost_actual` decimal(10, 4) DEFAULT NULL, -- Updated after generation `image_model` varchar(100) DEFAULT NULL, -- Which model was used -- Metadata `created_at` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (`id`), KEY `idx_post` (`post_id`), KEY `idx_agent_image_id` (`post_id`, `agent_image_id`), KEY `idx_status` (`status`), KEY `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ``` ### Table 2: `wp_wpaw_images_variants` Tracks all generated variants (temp images) for each agent_image_id. ```sql CREATE TABLE IF NOT EXISTS `wp_wpaw_images_variants` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, -- Reference to main image record `agentic_image_id` bigint(20) NOT NULL, `post_id` bigint(20) NOT NULL, `agent_image_id` varchar(50) NOT NULL, -- e.g., "img_hero_1" -- Variant details `variant_number` int(11) DEFAULT 1, -- 1st, 2nd, 3rd generation attempt `temp_file_path` varchar(500) NOT NULL, -- /wp-content/uploads/wpaw/{post_id}/xxx.jpg `temp_file_url` varchar(500) NOT NULL, -- URL to temp image `file_size` int(11) DEFAULT NULL, -- In bytes -- Generation details `prompt_used` text DEFAULT NULL, -- Exact prompt sent to image model `image_model_used` varchar(100) DEFAULT NULL, -- Which model generated this `generation_time` int(11) DEFAULT NULL, -- Seconds to generate `cost` decimal(10, 4) DEFAULT NULL, -- Cost of this generation -- Selection status `is_selected` tinyint(1) DEFAULT 0, -- 1 if user selected this variant `selected_at` datetime DEFAULT NULL, -- Lifecycle `status` varchar(30) DEFAULT 'temp', -- temp, selected, discarded, auto_deleted `created_at` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, `deleted_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_agentic_image` (`agentic_image_id`), KEY `idx_post` (`post_id`), KEY `idx_status` (`status`), KEY `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ``` --- ## Implementation Phases ### Phase 1: Database & Core Infrastructure (2-3 hours) - Create database tables - Create temp directory structure - Add activation/deactivation hooks ### Phase 2: Backend Image Generation (4-5 hours) - Implement image analysis - Implement prompt generation (model-specific) - Implement OpenRouter image generation - Implement variant management - Implement Media Library upload ### Phase 3: Frontend UI & Modal (4-5 hours) - Image review modal - Variant selection interface - Progress indicators - Error handling ### Phase 4: Gutenberg Integration (2-3 hours) - Block toolbar button - Block attribute updates - Placeholder management ### Phase 5: Temp File Management (2 hours) - Cleanup cron job - Admin page for temp images - Manual cleanup interface ### Phase 6: Testing & Polish (2-3 hours) - End-to-end testing - Error scenarios - Performance optimization - Documentation **Total Estimated Time:** 16-21 hours --- ## Phase 1: Database & Core Infrastructure ### 1.1 Create Database Tables **File:** `includes/class-image-manager.php` (NEW) ```php get_charset_collate(); // Table 1: wp_wpaw_images $table_images = $wpdb->prefix . 'wpaw_images'; $sql_images = "CREATE TABLE IF NOT EXISTS `{$table_images}` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `post_id` bigint(20) NOT NULL, `agent_image_id` varchar(50) NOT NULL, `placement` varchar(100) DEFAULT NULL, `section_title` varchar(255) DEFAULT NULL, `prompt_initial` text NOT NULL, `alt_text_initial` text DEFAULT NULL, `prompt_edited` text DEFAULT NULL, `alt_text_edited` text DEFAULT NULL, `attachment_id` bigint(20) DEFAULT NULL, `status` varchar(30) DEFAULT 'pending', `cost_estimate` decimal(10, 4) DEFAULT NULL, `cost_actual` decimal(10, 4) DEFAULT NULL, `image_model` varchar(100) DEFAULT NULL, `created_at` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (`id`), KEY `idx_post` (`post_id`), KEY `idx_agent_image_id` (`post_id`, `agent_image_id`), KEY `idx_status` (`status`), KEY `idx_created` (`created_at`) ) {$charset_collate};"; // Table 2: wp_wpaw_images_variants $table_variants = $wpdb->prefix . 'wpaw_images_variants'; $sql_variants = "CREATE TABLE IF NOT EXISTS `{$table_variants}` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `agentic_image_id` bigint(20) NOT NULL, `post_id` bigint(20) NOT NULL, `agent_image_id` varchar(50) NOT NULL, `variant_number` int(11) DEFAULT 1, `temp_file_path` varchar(500) NOT NULL, `temp_file_url` varchar(500) NOT NULL, `file_size` int(11) DEFAULT NULL, `prompt_used` text DEFAULT NULL, `image_model_used` varchar(100) DEFAULT NULL, `generation_time` int(11) DEFAULT NULL, `cost` decimal(10, 4) DEFAULT NULL, `is_selected` tinyint(1) DEFAULT 0, `selected_at` datetime DEFAULT NULL, `status` varchar(30) DEFAULT 'temp', `created_at` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, `deleted_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_agentic_image` (`agentic_image_id`), KEY `idx_post` (`post_id`), KEY `idx_status` (`status`), KEY `idx_created` (`created_at`) ) {$charset_collate};"; require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); dbDelta( $sql_images ); dbDelta( $sql_variants ); // Create temp directory $this->create_temp_directory(); } /** * Create temp directory for image storage. */ private function create_temp_directory() { $upload_dir = wp_upload_dir(); $temp_dir = $upload_dir['basedir'] . '/wpaw'; if ( ! file_exists( $temp_dir ) ) { wp_mkdir_p( $temp_dir ); // Add .htaccess to prevent direct access $htaccess = $temp_dir . '/.htaccess'; if ( ! file_exists( $htaccess ) ) { file_put_contents( $htaccess, "Options -Indexes\n" ); } // Add index.php for security $index = $temp_dir . '/index.php'; if ( ! file_exists( $index ) ) { file_put_contents( $index, "create_tables(); } register_activation_hook( __FILE__, 'wp_agentic_writer_activate' ); ``` ### 1.3 Update Autoloader **File:** `includes/class-autoloader.php` ```php // Add to class map 'WP_Agentic_Writer_Image_Manager' => 'class-image-manager.php', ``` --- ## Phase 2: Backend Image Generation ### 2.1 Analyze Article for Image Placement **File:** `includes/class-image-manager.php` (add methods) ```php /** * Analyze article for optimal image placement. * * @param string $article_markdown Article content in markdown. * @param int $post_id Post ID. * @return array|WP_Error Placement data or error. */ public function analyze_article_for_images( $article_markdown, $post_id ) { $settings = get_option( 'wp_agentic_writer_settings', array() ); $writing_model = $settings['writing_model'] ?? 'anthropic/claude-3.5-sonnet'; $system_prompt = "You are an expert content strategist analyzing articles for optimal image placement. Your task: Identify 2-3 strategic locations where images would enhance understanding and engagement. RULES: 1. Prioritize placement after introduction (hero image) 2. Consider complex sections that need visual aids 3. Look for opportunities before conclusions 4. Maximum 3 images per article Return JSON: { \"recommended_image_count\": 3, \"image_placement_points\": [ { \"agent_image_id\": \"img_hero_1\", \"placement\": \"after_introduction\", \"section_title\": \"Introduction\", \"image_type\": \"hero_dashboard\", \"reasoning\": \"Sets visual tone for article\" } ] }"; $messages = array( array( 'role' => 'user', 'content' => "Analyze this article for image placement:\n\n" . $article_markdown, ), ); $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $response = $provider->chat( $messages, array( 'temperature' => 0.3 ), 'planning' ); if ( is_wp_error( $response ) ) { return $response; } // Extract JSON from response $json_match = array(); if ( preg_match( '/\{[\s\S]*\}/m', $response['content'], $json_match ) ) { $placement_data = json_decode( $json_match[0], true ); if ( json_last_error() === JSON_ERROR_NONE ) { return $placement_data; } } return new WP_Error( 'parse_error', 'Failed to parse placement analysis' ); } ``` ### 2.2 Generate Model-Specific Prompts ```php /** * Generate image prompts optimized for specific image model. * * @param string $article_markdown Article content. * @param array $placement_data Placement analysis. * @param int $post_id Post ID. * @return array|WP_Error Image specifications or error. */ public function generate_image_prompts( $article_markdown, $placement_data, $post_id ) { $settings = get_option( 'wp_agentic_writer_settings', array() ); $writing_model = $settings['writing_model'] ?? 'anthropic/claude-3.5-sonnet'; $image_model = $settings['image_model'] ?? 'openai/gpt-4o'; // Get model-specific prompt guidance $prompt_guidance = $this->get_prompt_guidance_for_model( $image_model ); $system_prompt = "You are an Image Prompt Engineer specializing in {$prompt_guidance['model_name']}. TARGET MODEL: {$prompt_guidance['model_name']} PROMPT LENGTH: {$prompt_guidance['prompt_length']} COMPLEXITY: {$prompt_guidance['complexity']} {$prompt_guidance['guidance']} TEMPLATE: {$prompt_guidance['template']} Generate precise, cost-efficient prompts that exploit this model's strengths. Return JSON: { \"images\": [ { \"agent_image_id\": \"img_hero_1\", \"placement\": \"after_introduction\", \"section_title\": \"Introduction\", \"prompt\": \"[Model-optimized prompt]\", \"alt\": \"Descriptive alt text\", \"image_model\": \"{$image_model}\" } ] }"; $user_input = json_encode( array( 'article' => $article_markdown, 'placement_points' => $placement_data['image_placement_points'], 'image_count' => $placement_data['recommended_image_count'], 'target_image_model' => $image_model, ) ); $messages = array( array( 'role' => 'user', 'content' => "Generate image prompts:\n\n" . $user_input, ), ); $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $response = $provider->chat( $messages, array( 'temperature' => 0.7 ), 'planning' ); if ( is_wp_error( $response ) ) { return $response; } // Extract JSON $json_match = array(); if ( preg_match( '/\{[\s\S]*\}/m', $response['content'], $json_match ) ) { $image_specs = json_decode( $json_match[0], true ); if ( json_last_error() === JSON_ERROR_NONE ) { // Save to database $this->save_image_recommendations( $post_id, $image_specs['images'] ); return $image_specs; } } return new WP_Error( 'parse_error', 'Failed to parse image prompts' ); } /** * Get prompt guidance for specific image model. */ private function get_prompt_guidance_for_model( $image_model ) { $model_configs = array( 'black-forest-labs/flux.2-klein' => array( 'model_name' => 'FLUX.2 [klein]', 'prompt_length' => '1-2 sentences', 'complexity' => 'simple', 'guidance' => 'Keep prompts short and simple. Focus on main subject, key details, and style. Avoid complex scenes or technical specifications.', 'template' => 'Subject, key elements, style, color palette', ), 'sourceful/riverflow-v2-max' => array( 'model_name' => 'Riverflow V2 Max', 'prompt_length' => '3-4 sentences', 'complexity' => 'medium-detailed', 'guidance' => 'Include context, environment details, lighting style, and photographic specifications. Model excels at photorealism.', 'template' => 'Subject + context, environment details, lighting style, photography style, technical specs', ), 'black-forest-labs/flux.2-max' => array( 'model_name' => 'FLUX.2 [max]', 'prompt_length' => '4-6 sentences', 'complexity' => 'very-detailed-technical', 'guidance' => 'Use detailed technical vocabulary. Include exact materials, color codes (HEX), spatial relationships, and specifications.', 'template' => 'Technical foundation, main subject + action, environment, lighting + mood, style + aesthetics, technical specifications', ), ); // Default to Riverflow if model not found return $model_configs[ $image_model ] ?? $model_configs['sourceful/riverflow-v2-max']; } /** * Save image recommendations to database. */ private function save_image_recommendations( $post_id, $images ) { global $wpdb; $table = $wpdb->prefix . 'wpaw_images'; foreach ( $images as $image_spec ) { $wpdb->insert( $table, array( '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', ), array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' ) ); } } ``` ### 2.3 Generate Image via OpenRouter **File:** `includes/class-openrouter-provider.php` (update existing method) ```php /** * Generate image using OpenRouter image generation API. * * @param string $prompt Image prompt. * @param string $model Image model (optional, uses default if not provided). * @param array $options Additional options (size, quality, etc). * @return array|WP_Error Response with image URL or error. */ public function generate_image( $prompt, $model = null, $options = array() ) { if ( empty( $this->api_key ) ) { return new WP_Error( 'no_api_key', 'OpenRouter API key not configured' ); } $model = $model ?? $this->image_model; $size = $options['size'] ?? '1024x576'; // Default blog size $quality = $options['quality'] ?? 'hd'; $n = $options['n'] ?? 1; // Number of variants $start_time = microtime( true ); $response = wp_remote_post( 'https://openrouter.ai/api/v1/images/generations', array( 'headers' => array( 'Authorization' => 'Bearer ' . $this->api_key, 'Content-Type' => 'application/json', 'HTTP-Referer' => home_url(), 'X-Title' => get_bloginfo( 'name' ), ), 'body' => wp_json_encode( array( 'model' => $model, 'prompt' => $prompt, 'n' => $n, 'size' => $size, 'quality' => $quality, ) ), 'timeout' => 60, ) ); $generation_time = microtime( true ) - $start_time; if ( is_wp_error( $response ) ) { return $response; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! isset( $body['data'][0]['url'] ) ) { return new WP_Error( 'image_generation_failed', $body['error']['message'] ?? 'Unknown error' ); } return array( 'url' => $body['data'][0]['url'], 'cost' => $body['usage']['cost'] ?? 0.03, // Fallback estimate 'generation_time' => $generation_time, 'model' => $model, ); } ``` ### 2.4 REST API Endpoints **File:** `includes/class-gutenberg-sidebar.php` (add to `register_routes()`) ```php // Image generation endpoints register_rest_route( 'wp-agentic-writer/v1', '/analyze-images', array( 'methods' => 'POST', 'callback' => array( $this, 'handle_analyze_images' ), 'permission_callback' => array( $this, 'check_permissions' ), ) ); register_rest_route( 'wp-agentic-writer/v1', '/generate-image', array( 'methods' => 'POST', 'callback' => array( $this, 'handle_generate_image' ), 'permission_callback' => array( $this, 'check_permissions' ), ) ); register_rest_route( 'wp-agentic-writer/v1', '/commit-image', array( 'methods' => 'POST', 'callback' => array( $this, 'handle_commit_image' ), 'permission_callback' => array( $this, 'check_permissions' ), ) ); register_rest_route( 'wp-agentic-writer/v1', '/image-recommendations/(?P\d+)', array( 'methods' => 'GET', 'callback' => array( $this, 'handle_get_image_recommendations' ), 'permission_callback' => array( $this, 'check_permissions' ), ) ); ``` --- ## Phase 3: Frontend UI & Modal ### 3.1 Image Review Modal Component **File:** `assets/js/image-modal.js` (NEW) ```javascript /** * Image Generation Modal Component * * Handles image review, generation, variant selection, and commitment. */ (function() { const { Modal, Button, Spinner, TextControl, TextareaControl } = wp.components; const { useState, useEffect } = wp.element; window.wpAgenticWriter = window.wpAgenticWriter || {}; /** * Image Review Modal * Shows after article generation with image recommendations */ window.wpAgenticWriter.ImageReviewModal = function({ postId, onClose, onComplete }) { const [step, setStep] = useState('loading'); // loading, review, generating, selecting const [images, setImages] = useState([]); const [selectedImages, setSelectedImages] = useState([]); const [variantCounts, setVariantCounts] = useState({}); // Track variant count per image const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(null); // Load image recommendations 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); // Initialize variant counts to 2 for each image const initialCounts = {}; imgs.forEach(img => { initialCounts[img.agent_image_id] = 2; // Default: 2 variants }); 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'; // Cost per image by model (estimates) 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(); // Update image with variants 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(); // Update Gutenberg block updateGutenbergBlock(imageId, result); // Mark as committed 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) => { // Find block with matching data-agent-image-id 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, // Remove placeholder marker } ); return true; } if (block.innerBlocks && block.innerBlocks.length > 0) { if (findAndUpdateBlock(block.innerBlocks)) { return true; } } } return false; }; findAndUpdateBlock(blocks); }; // Render functions for each step 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.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') ) ) ); } }; })(); ``` --- ## Configuration & Settings ### Update Model Presets **File:** `includes/class-settings.php` and `includes/class-settings-v2.php` Update presets to use recommended image models: ```php $presets = array( 'budget' => array( 'chat' => 'google/gemini-2.5-flash', 'clarity' => 'google/gemini-2.5-flash', 'planning' => 'google/gemini-2.5-flash', 'writing' => 'mistralai/mistral-small-creative', 'refinement' => 'google/gemini-2.5-flash', 'image' => 'black-forest-labs/flux.2-klein', // UPDATED ), 'balanced' => array( 'chat' => 'google/gemini-2.5-flash', 'clarity' => 'google/gemini-2.5-flash', 'planning' => 'google/gemini-2.5-flash', 'writing' => 'anthropic/claude-3.5-sonnet', 'refinement' => 'anthropic/claude-3.5-sonnet', 'image' => 'sourceful/riverflow-v2-max', // UPDATED ), 'premium' => array( 'chat' => 'google/gemini-3-flash-preview', 'clarity' => 'anthropic/claude-sonnet-4', 'planning' => 'google/gemini-3-flash-preview', 'writing' => 'openai/gpt-4.1', 'refinement' => 'openai/gpt-4.1', 'image' => 'black-forest-labs/flux.2-max', // UPDATED ), ); ``` --- ## Cost Tracking Integration Update cost tracking to include image generation: ```php // In class-cost-tracker.php public function track_image_generation( $post_id, $image_model, $cost, $generation_time ) { global $wpdb; $wpdb->insert( $wpdb->prefix . 'wpaw_cost_tracking', array( 'post_id' => $post_id, 'model' => $image_model, 'action' => 'image_generation', 'input_tokens' => 0, // Images don't use tokens 'output_tokens' => 0, 'cost' => $cost, ), array( '%d', '%s', '%s', '%d', '%d', '%f' ) ); } ``` --- ## Security Considerations 1. **File Upload Security** - Validate image file types (JPEG, PNG only) - Sanitize filenames - Check file size limits - Use WordPress media_handle_sideload() 2. **Permission Checks** - Verify user can edit post - Check nonces on all AJAX requests - Validate post ownership 3. **Temp Directory** - Add .htaccess to prevent direct access - Regular cleanup of old files - Limit directory size 4. **API Security** - Never expose API keys to frontend - Rate limiting on image generation - Cost limits per user/post --- ## Rollout Strategy ### Phase 1: Beta (Week 1) - Enable for admin users only - Test with Budget preset - Monitor costs and errors ### Phase 2: Limited Release (Week 2) - Enable for all users - Add usage limits (e.g., 10 images/day) - Collect feedback ### Phase 3: Full Release (Week 3) - Remove limits - Add admin page for image management - Documentation and tutorials --- ## Testing Checklist - [ ] Database tables created successfully - [ ] Temp directory created with proper permissions - [ ] Article analysis generates placement points - [ ] Prompt generation creates model-specific prompts - [ ] Image generation via OpenRouter works - [ ] Variants saved to temp directory - [ ] Variant selection updates Gutenberg block - [ ] Media Library upload works with alt text - [ ] Cost tracking records image generation - [ ] Cleanup cron job removes old temps - [ ] Error handling for API failures - [ ] Permission checks work correctly - [ ] UI responsive on mobile - [ ] Works with all 3 presets --- ## Success Metrics - **Cost Efficiency:** Average cost per article with images < $0.15 - **User Adoption:** >50% of users generate at least 1 image - **Quality:** <10% regeneration rate (first variant selected) - **Performance:** Image generation completes in <30 seconds - **Errors:** <5% API failure rate --- ## Next Steps 1. **Review this plan** with team 2. **Create Phase 1 branch** in git 3. **Implement database schema** (2 hours) 4. **Build backend API** (4 hours) 5. **Create frontend modal** (4 hours) 6. **Test end-to-end** (2 hours) 7. **Deploy to staging** for beta testing --- **End of Implementation Plan**