Files
wp-agentic-writer/IMAGE_GENERATION_IMPLEMENTATION_PLAN.md
Dwindi Ramadhana d2c10756ab Add AI writing assistant plugin with local backend, brave search, and image generation support
- Implement local backend AI provider with Ollama integration
- Add Brave Search API integration for real-time search suggestions
- Add image generation manager with multiple AI providers
- Create hybrid provider system with local/cloud fallback
- Add comprehensive settings UI with provider management
- Implement Gutenberg sidebar with writing assistance controls
- Add SEO schema generation for AI-generated content
- Multiple provider support: OpenRouter, local backend, Codex
2026-05-17 10:48:05 +07:00

53 KiB
Raw Blame History

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
  2. Architecture Overview
  3. Database Schema
  4. Implementation Phases
  5. Phase 1: Database & Core Infrastructure
  6. Phase 2: Backend Image Generation
  7. Phase 3: Frontend UI & Modal
  8. Phase 4: Gutenberg Integration
  9. Phase 5: Temp File Management
  10. Phase 6: Testing & Polish
  11. Configuration & Settings
  12. Cost Tracking Integration
  13. Security Considerations
  14. 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.

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.

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
/**
 * Image Manager Class
 *
 * Handles image generation, variant management, and WordPress Media integration.
 *
 * @package WP_Agentic_Writer
 */

class WP_Agentic_Writer_Image_Manager {
    
    /**
     * Singleton instance.
     */
    private static $instance = null;
    
    /**
     * Get singleton instance.
     */
    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    /**
     * Constructor.
     */
    private function __construct() {
        // Register activation hook
        register_activation_hook( WP_AGENTIC_WRITER_FILE, array( $this, 'create_tables' ) );
    }
    
    /**
     * Create database tables on plugin activation.
     */
    public function create_tables() {
        global $wpdb;
        
        $charset_collate = $wpdb->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, "<?php // Silence is golden\n" );
            }
        }
    }
}

1.2 Update Plugin Activation

File: wp-agentic-writer.php

// Add to plugin initialization
function wp_agentic_writer_activate() {
    // Existing activation code...
    
    // Initialize image manager (creates tables)
    WP_Agentic_Writer_Image_Manager::get_instance()->create_tables();
}
register_activation_hook( __FILE__, 'wp_agentic_writer_activate' );

1.3 Update Autoloader

File: includes/class-autoloader.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)

/**
 * 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

/**
 * 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)

/**
 * 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())

// 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<post_id>\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)

/**
 * 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:

$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:

// 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