Files
wp-agentic-writer/FOCUS_KEYWORD_ANCHOR_AND_IMAGE_FIXES.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

30 KiB

Focus Keyword Anchor System & Image Generation Fixes

Date: January 30, 2026
Version: 1.0
Status: Implementation Plan


Table of Contents

  1. Executive Summary
  2. Part A: Focus Keyword Anchor System
  3. Part B: Image Generation Fixes
  4. Part C: UI Redesign
  5. Implementation Priority

Executive Summary

This document addresses three interconnected issues:

Issue Root Cause Solution
Context loss during conversation No persistent topic anchor Focus Keyword as central context driver
Image tables not created Activation hook not re-run Manual table creation + version check
Image generation errors Method name mismatch + missing public method Fix method signatures
Image toolbar not showing Script dependency or block attribute issues Debug and fix filter

Part A: Focus Keyword Anchor System

Problem Statement

The agent loses conversation context because:

  • Generic topic passed to outline generation
  • Chat history truncated to last 10 messages
  • No persistent anchor for user's intent
  • Recency bias causes LLM to focus on recent refinements

Solution: Focus Keyword as Context Anchor

Core Concept

┌─────────────────────────────────────────────────────────────┐
│  Focus Keyword = Single Source of Truth for Article Topic   │
│                                                             │
│  • Always visible at top of chat                            │
│  • Drives ALL API calls (clarity, planning, writing)        │
│  • Accumulates suggestions from each AI response            │
│  • User can select, change, or enter custom keyword         │
└─────────────────────────────────────────────────────────────┘

UI Design

Location: Top of Chatbox (Replacing Context Indicator)

Current UI:

┌─────────────────────────────────────────────────────────────┐
│ [1 messages]  [$0.0221]  [~500 tokens]  ............  [↕]   │
└─────────────────────────────────────────────────────────────┘

New UI (Compact - Default):

┌─────────────────────────────────────────────────────────────┐
│ 🎯 [Switch Career Usia 30+ ▼]  [$0.02]  ............  [↕]   │
└─────────────────────────────────────────────────────────────┘

New UI (Expanded - When textarea expanded):

┌─────────────────────────────────────────────────────────────┐
│ 🎯 Focus Keyword                                            │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Switch Career Usia 30+                              [▼] │ │
│ └─────────────────────────────────────────────────────────┘ │
│                                                             │
│ Suggestions:                                                │
│ ○ Switch Career Usia 30+  (from response #1)               │
│ ○ Vibe Coding untuk Pemula  (from response #2)             │
│ ○ AI Web Design Tanpa Coding  (from response #3)           │
│ ○ Custom...                                                 │
│                                                             │
│ Session: $0.02  │  ~500 tokens                              │
└─────────────────────────────────────────────────────────────┘

Dropdown Behavior

  1. Empty State: Placeholder "Select or enter focus keyword..."
  2. After Response #1: 1 suggestion appears
  3. After Response #2: 2 suggestions (cumulative)
  4. Max 5 suggestions: Older ones rotate out, "Show all" option
  5. Custom Option: Always last, triggers text input
  6. Selection: Immediately saves to postConfig.focus_keyword

Data Flow

User Message
     │
     ▼
┌────────────────┐
│ AI Response    │
│ + Extract      │──────► Add to suggestions dropdown
│   keyword      │
└────────────────┘
     │
     ▼
User Selects Keyword (or AI auto-selects first suggestion)
     │
     ▼
┌────────────────────────────────────────────────────────────┐
│ ALL subsequent API calls include:                          │
│ {                                                          │
│   focus_keyword: "Switch Career Usia 30+",                 │
│   topic: "...",                                            │
│   chatHistory: [...],                                      │
│   ...                                                      │
│ }                                                          │
└────────────────────────────────────────────────────────────┘
     │
     ▼
Backend System Prompt includes:
"PRIMARY TOPIC: {focus_keyword}
All content must relate to this topic.
Recent conversation refinements should ENHANCE this topic, not replace it."

Keyword Extraction Logic

// In chat response handler
const extractFocusKeywordSuggestion = (aiResponse) => {
    // Method 1: AI explicitly suggests (preferred)
    // Look for pattern: "Focus Keyword Suggestion: ..."
    const explicitMatch = aiResponse.match(/focus keyword suggestion[:\s]+["']?([^"'\n]+)["']?/i);
    if (explicitMatch) return explicitMatch[1].trim();
    
    // Method 2: Extract from first heading or bold text
    const headingMatch = aiResponse.match(/^#+\s+(.+)$/m);
    if (headingMatch) return headingMatch[1].trim();
    
    // Method 3: Extract prominent phrase (first bold)
    const boldMatch = aiResponse.match(/\*\*([^*]+)\*\*/);
    if (boldMatch) return boldMatch[1].trim();
    
    return null;
};

Backend Integration

1. Update System Prompts

File: includes/class-gutenberg-sidebar.php

// In stream_generate_plan() - Line ~1723
$focus_keyword = $post_config['focus_keyword'] ?? '';
$focus_keyword_instruction = '';
if (!empty($focus_keyword)) {
    $focus_keyword_instruction = "
PRIMARY TOPIC ANCHOR: \"{$focus_keyword}\"

CRITICAL: This article MUST be about \"{$focus_keyword}\".
- The title MUST include or relate to \"{$focus_keyword}\"
- All sections MUST support this primary topic
- Recent conversation refinements are meant to ENHANCE this topic, not REPLACE it
- If user discussed sub-topics (e.g., AI tools, web design), treat them as ASPECTS of the primary topic
";
}

$system_prompt = "You are an expert content strategist...

{$focus_keyword_instruction}

IMPORTANT CONSTRAINT: {$section_limit}
...";

2. Update Chat Response to Include Keyword Suggestion

File: includes/class-gutenberg-sidebar.php

// In handle_chat_request() - Add to system prompt
$system_prompt .= "

At the END of your response, if appropriate, suggest a focus keyword for the article in this format:
**Focus Keyword Suggestion:** [your suggested keyword]

The keyword should be:
- 2-5 words
- SEO-friendly
- Capture the main topic discussed
";

3. Persist Focus Keyword

File: includes/class-gutenberg-sidebar.php

// In handle_generate_plan() - Line ~1237
$focus_keyword = $post_config['focus_keyword'] ?? '';

// Save to post meta for persistence
if ($post_id > 0 && !empty($focus_keyword)) {
    update_post_meta($post_id, '_wpaw_focus_keyword', $focus_keyword);
}

Frontend Implementation

1. New State Variables

File: assets/js/sidebar.js

// Add new state
const [focusKeywordSuggestions, setFocusKeywordSuggestions] = wp.element.useState([]);
const [selectedFocusKeyword, setSelectedFocusKeyword] = wp.element.useState('');

// Load from postConfig on mount
wp.element.useEffect(() => {
    if (postConfig.focus_keyword) {
        setSelectedFocusKeyword(postConfig.focus_keyword);
    }
}, [postConfig.focus_keyword]);

2. Update postConfig When Keyword Changes

const handleFocusKeywordChange = (keyword) => {
    setSelectedFocusKeyword(keyword);
    updatePostConfig('focus_keyword', keyword);
};

3. Extract Suggestions from AI Responses

// In streaming response handler, after accumulating content
const suggestion = extractFocusKeywordSuggestion(accumulatedContent);
if (suggestion && !focusKeywordSuggestions.includes(suggestion)) {
    setFocusKeywordSuggestions(prev => {
        const updated = [...prev, suggestion];
        // Keep max 5 suggestions
        return updated.slice(-5);
    });
    
    // Auto-select first suggestion if none selected
    if (!selectedFocusKeyword) {
        handleFocusKeywordChange(suggestion);
    }
}

4. Render Focus Keyword Bar (Replacing Context Indicator)

const renderFocusKeywordBar = () => {
    return wp.element.createElement('div', { 
        className: 'wpaw-focus-keyword-bar'
    },
        // Keyword dropdown
        wp.element.createElement('div', { className: 'wpaw-focus-keyword-wrapper' },
            wp.element.createElement('span', { className: 'wpaw-focus-keyword-icon' }, '🎯'),
            wp.element.createElement('select', {
                className: 'wpaw-focus-keyword-select',
                value: selectedFocusKeyword,
                onChange: (e) => {
                    if (e.target.value === '__custom__') {
                        // Show custom input
                        setShowCustomKeywordInput(true);
                    } else {
                        handleFocusKeywordChange(e.target.value);
                    }
                }
            },
                wp.element.createElement('option', { value: '' }, 'Select focus keyword...'),
                focusKeywordSuggestions.map((kw, idx) => 
                    wp.element.createElement('option', { key: idx, value: kw }, kw)
                ),
                wp.element.createElement('option', { value: '__custom__' }, '+ Custom keyword...')
            )
        ),
        
        // Cost display (compact)
        wp.element.createElement('span', { className: 'wpaw-cost-compact' },
            '$' + cost.session.toFixed(2)
        ),
        
        // Expand button
        wp.element.createElement('button', {
            className: 'wpaw-expand-btn',
            onClick: () => setIsTextareaExpanded(!isTextareaExpanded)
        }, isTextareaExpanded ? '↓' : '↑')
    );
};

CSS Styling

.wpaw-focus-keyword-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    background: #1e1e1e;
    border-bottom: 1px solid #3c3c3c;
    font-size: 12px;
}

.wpaw-focus-keyword-wrapper {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: 1;
}

.wpaw-focus-keyword-select {
    flex: 1;
    background: #2c2c2c;
    border: 1px solid #3c3c3c;
    color: #fff;
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 12px;
    max-width: 200px;
}

.wpaw-focus-keyword-select:focus {
    border-color: #007cba;
    outline: none;
}

.wpaw-cost-compact {
    color: #a7aaad;
    font-size: 11px;
}

.wpaw-expand-btn {
    background: transparent;
    border: none;
    color: #a7aaad;
    cursor: pointer;
    padding: 4px;
}

Part B: Image Generation Fixes

Error #1: Missing Database Tables

Symptoms

WordPress database error Unknown error 1146 for query 
SELECT * FROM wp_wpaw_images_variants...

Root Cause

Tables are created in activation hook, but plugin was already active when code was added. Activation hook only runs on fresh activation.

Fix: Add Version-Based Table Creation

File: wp-agentic-writer.php

// Add after plugin initialization
add_action('plugins_loaded', 'wp_agentic_writer_maybe_create_tables');

function wp_agentic_writer_maybe_create_tables() {
    $current_version = get_option('wpaw_db_version', '0');
    $required_version = '1.1.0'; // Bump when adding new tables
    
    if (version_compare($current_version, $required_version, '<')) {
        // Create cost tracking table
        wp_agentic_writer_create_cost_table();
        
        // Create image management tables
        WP_Agentic_Writer_Image_Manager::get_instance()->create_tables();
        
        // Update version
        update_option('wpaw_db_version', $required_version);
    }
}

Immediate Fix (Manual)

Run this SQL in phpMyAdmin or WP-CLI:

-- Table 1: wp_wpaw_images
CREATE TABLE IF NOT EXISTS `wp_wpaw_images` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `post_id` bigint(20) NOT NULL,
    `agent_image_id` varchar(50) NOT NULL,
    `placement` varchar(50) NOT NULL,
    `section_title` text,
    `prompt_initial` text,
    `prompt_refined` text,
    `alt_text_initial` text,
    `alt_text_refined` text,
    `image_model` varchar(100),
    `status` varchar(20) DEFAULT 'pending',
    `selected_variant_id` bigint(20) DEFAULT NULL,
    `attachment_id` bigint(20) DEFAULT NULL,
    `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
    `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `idx_agent_image_id` (`agent_image_id`),
    KEY `idx_post_id` (`post_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
CREATE TABLE IF NOT EXISTS `wp_wpaw_images_variants` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `agentic_image_id` bigint(20) NOT NULL,
    `post_id` bigint(20) NOT NULL,
    `variant_number` tinyint(3) NOT NULL,
    `prompt_used` text,
    `temp_url` text,
    `temp_path` text,
    `generation_model` varchar(100),
    `generation_cost` decimal(10,6) DEFAULT 0,
    `width` int(11) DEFAULT NULL,
    `height` int(11) DEFAULT NULL,
    `status` varchar(20) DEFAULT 'temp',
    `attachment_id` bigint(20) DEFAULT NULL,
    `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_agentic_image_id` (`agentic_image_id`),
    KEY `idx_post_id` (`post_id`),
    KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Error #2: Undefined Method save_image_recommendation()

Symptoms

PHP Fatal error: Call to undefined method 
WP_Agentic_Writer_Image_Manager::save_image_recommendation()

Root Cause

Caller (class-gutenberg-sidebar.php:2185):

$image_manager->save_image_recommendation(
    $post_id,
    $agent_image_id,
    'section_' . $section_id,
    $heading,
    trim( $description ),
    trim( $description )
);

Actual Method (class-image-manager.php:320):

private function save_image_recommendations( $post_id, $images ) {
    // Takes array of images, not individual params
}

Issues:

  1. Method name is plural (save_image_recommendations) vs singular (save_image_recommendation)
  2. Method is private, not public
  3. Method signature is different (expects array of images)

Fix: Add Public Method with Correct Signature

File: includes/class-image-manager.php

Add after line 340:

/**
 * Save single image recommendation to database.
 *
 * @param int    $post_id       Post ID.
 * @param string $agent_image_id Unique image identifier.
 * @param string $placement     Placement location.
 * @param string $section_title Section title.
 * @param string $prompt        Image prompt/description.
 * @param string $alt_text      Alt text for image.
 * @return int|false Insert ID or false on failure.
 */
public function save_image_recommendation( $post_id, $agent_image_id, $placement, $section_title, $prompt, $alt_text ) {
    global $wpdb;
    $table = $wpdb->prefix . 'wpaw_images';

    $settings = get_option( 'wp_agentic_writer_settings', array() );
    $image_model = $settings['image_model'] ?? 'openai/gpt-4o';

    $result = $wpdb->insert(
        $table,
        array(
            'post_id'          => $post_id,
            'agent_image_id'   => $agent_image_id,
            'placement'        => $placement,
            'section_title'    => $section_title,
            'prompt_initial'   => $prompt,
            'alt_text_initial' => $alt_text,
            'image_model'      => $image_model,
            'status'           => 'pending',
        ),
        array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
    );

    if ( $result ) {
        return $wpdb->insert_id;
    }

    return false;
}

Error #3: Image Block Toolbar Not Showing

Symptoms

No "Generate AI Image" button appears in image block toolbar.

Potential Causes

  1. Script not loaded: Check browser console for errors
  2. Block attribute missing: data-agent-image-id not set on blocks
  3. Filter not applied: WordPress filter may not be working

Debug Steps

Step 1: Check if script is loaded

// In browser console
console.log(typeof wp.hooks.applyFilters);
console.log(wp.hooks.hasFilter('editor.BlockEdit', 'wp-agentic-writer/image-generate-toolbar'));

Step 2: Check if blocks have the attribute

// In browser console
wp.data.select('core/block-editor').getBlocks().forEach(block => {
    if (block.name === 'core/image') {
        console.log('Image block:', block.clientId, block.attributes);
    }
});

Step 3: Verify attribute exists

The issue may be that data-agent-image-id is stored in block HTML but not in attributes.

Fix: Update Block Detection Logic

File: assets/js/block-image-generate.js

The current code checks block.attributes['data-agent-image-id'], but the attribute might be stored differently.

// Current (may not work)
const agentImageId = block?.attributes?.['data-agent-image-id'];

// Fix: Also check innerHTML for the attribute
const hasAgentImageId = () => {
    if (block?.attributes?.['data-agent-image-id']) return true;
    
    // Check innerHTML
    const innerHTML = block?.attributes?.innerHTML || '';
    if (innerHTML.includes('data-agent-image-id')) return true;
    
    // Check original HTML
    const originalContent = block?.originalContent || '';
    if (originalContent.includes('data-agent-image-id')) return true;
    
    return false;
};

if (!hasAgentImageId()) {
    return wp.element.createElement(BlockEdit, props);
}

Alternative Fix: Check for Placeholder Images

If the image block has no url but has alt text, it's likely a placeholder:

const isPlaceholder = block?.name === 'core/image' && 
    !block?.attributes?.url && 
    block?.attributes?.alt;

if (!agentImageId && !isPlaceholder) {
    return wp.element.createElement(BlockEdit, props);
}

Part C: UI Redesign

Current Context Indicator Bar

Location: assets/js/sidebar.js - renderContextIndicator()

┌─────────────────────────────────────────────────────────────┐
│ [💬 1 messages]  [💰 $0.0221]  [~500 tokens]  .....  [↕]   │
└─────────────────────────────────────────────────────────────┘

New Focus Keyword Bar Design

Compact Mode (Default)

┌─────────────────────────────────────────────────────────────┐
│ 🎯 [Switch Career Usia 30+ ▼]     [$0.02]           [↕]    │
└─────────────────────────────────────────────────────────────┘
     │                                │                 │
     └─ Dropdown with suggestions     └─ Session cost   └─ Expand

Expanded Mode (When textarea expanded)

┌─────────────────────────────────────────────────────────────┐
│ 🎯 FOCUS KEYWORD                                            │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Switch Career Usia 30+                              [▼] │ │
│ └─────────────────────────────────────────────────────────┘ │
│                                                             │
│ 📝 AI Suggestions:                                          │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ● Switch Career Usia 30+ (Response #1)                  │ │
│ │ ○ Vibe Coding untuk Pemula (Response #2)                │ │
│ │ ○ AI Web Design Tanpa Coding (Response #3)              │ │
│ │ ○ + Enter custom keyword...                             │ │
│ └─────────────────────────────────────────────────────────┘ │
│                                                             │
│ 💰 $0.02 this session  │  📊 ~500 tokens                    │
└─────────────────────────────────────────────────────────────┘

Code Changes Required

Replace renderContextIndicator() with renderFocusKeywordBar()

File: assets/js/sidebar.js

// REMOVE this function
const renderContextIndicator = () => { ... }

// ADD this function
const renderFocusKeywordBar = () => {
    const hasKeyword = selectedFocusKeyword && selectedFocusKeyword.length > 0;
    
    if (isTextareaExpanded) {
        return renderExpandedFocusKeywordBar();
    }
    
    // Compact mode
    return wp.element.createElement('div', { 
        className: 'wpaw-focus-keyword-bar wpaw-compact'
    },
        wp.element.createElement('div', { className: 'wpaw-fk-left' },
            wp.element.createElement('span', { className: 'wpaw-fk-icon' }, '🎯'),
            wp.element.createElement('select', {
                className: 'wpaw-fk-select',
                value: selectedFocusKeyword || '',
                onChange: handleKeywordSelect,
                disabled: isLoading
            },
                wp.element.createElement('option', { value: '' }, 
                    hasKeyword ? 'Change keyword...' : 'Select focus keyword...'
                ),
                ...focusKeywordSuggestions.map((kw, i) => 
                    wp.element.createElement('option', { key: i, value: kw }, 
                        kw.length > 25 ? kw.substring(0, 25) + '...' : kw
                    )
                ),
                wp.element.createElement('option', { value: '__custom__' }, '+ Custom...')
            )
        ),
        wp.element.createElement('span', { className: 'wpaw-fk-cost' },
            '$' + (cost.session || 0).toFixed(2)
        ),
        wp.element.createElement('button', {
            className: 'wpaw-fk-expand',
            onClick: () => setIsTextareaExpanded(true),
            title: 'Expand'
        }, '↕')
    );
};

const renderExpandedFocusKeywordBar = () => {
    return wp.element.createElement('div', { 
        className: 'wpaw-focus-keyword-bar wpaw-expanded'
    },
        // Header
        wp.element.createElement('div', { className: 'wpaw-fk-header' },
            wp.element.createElement('span', null, '🎯 FOCUS KEYWORD'),
            wp.element.createElement('button', {
                className: 'wpaw-fk-collapse',
                onClick: () => setIsTextareaExpanded(false)
            }, '↓')
        ),
        
        // Main input
        wp.element.createElement('div', { className: 'wpaw-fk-main-input' },
            showCustomKeywordInput 
                ? wp.element.createElement('input', {
                    type: 'text',
                    className: 'wpaw-fk-custom-input',
                    placeholder: 'Enter custom focus keyword...',
                    value: customKeywordInput,
                    onChange: (e) => setCustomKeywordInput(e.target.value),
                    onKeyDown: (e) => {
                        if (e.key === 'Enter') {
                            handleFocusKeywordChange(customKeywordInput);
                            setShowCustomKeywordInput(false);
                        }
                    },
                    autoFocus: true
                })
                : wp.element.createElement('select', {
                    className: 'wpaw-fk-select-full',
                    value: selectedFocusKeyword || '',
                    onChange: handleKeywordSelect
                },
                    wp.element.createElement('option', { value: '' }, 'Select focus keyword...'),
                    ...focusKeywordSuggestions.map((kw, i) => 
                        wp.element.createElement('option', { key: i, value: kw }, kw)
                    ),
                    wp.element.createElement('option', { value: '__custom__' }, '+ Enter custom keyword...')
                )
        ),
        
        // Suggestions list
        focusKeywordSuggestions.length > 0 && wp.element.createElement('div', { 
            className: 'wpaw-fk-suggestions' 
        },
            wp.element.createElement('div', { className: 'wpaw-fk-suggestions-label' }, 
                '📝 AI Suggestions:'
            ),
            focusKeywordSuggestions.map((kw, i) => 
                wp.element.createElement('div', {
                    key: i,
                    className: 'wpaw-fk-suggestion-item' + (kw === selectedFocusKeyword ? ' selected' : ''),
                    onClick: () => handleFocusKeywordChange(kw)
                },
                    wp.element.createElement('span', { className: 'wpaw-fk-radio' }, 
                        kw === selectedFocusKeyword ? '●' : '○'
                    ),
                    wp.element.createElement('span', { className: 'wpaw-fk-suggestion-text' }, kw),
                    wp.element.createElement('span', { className: 'wpaw-fk-suggestion-source' }, 
                        '(Response #' + (i + 1) + ')'
                    )
                )
            )
        ),
        
        // Stats
        wp.element.createElement('div', { className: 'wpaw-fk-stats' },
            wp.element.createElement('span', null, '💰 $' + (cost.session || 0).toFixed(2) + ' this session'),
            wp.element.createElement('span', null, '│'),
            wp.element.createElement('span', null, '📊 ~' + (messages.length * 500) + ' tokens')
        )
    );
};

Implementation Priority

Phase 1: Critical Fixes (Day 1)

Task File Priority
Create missing database tables Manual SQL or add version check CRITICAL
Add save_image_recommendation() method class-image-manager.php CRITICAL
Fix image toolbar block detection block-image-generate.js HIGH

Phase 2: Focus Keyword System (Day 2-3)

Task File Priority
Add state variables for focus keyword sidebar.js HIGH
Implement keyword extraction from responses sidebar.js HIGH
Create renderFocusKeywordBar() sidebar.js HIGH
Add CSS styling sidebar.css MEDIUM
Update backend to use focus_keyword class-gutenberg-sidebar.php HIGH

Phase 3: Integration & Testing (Day 4)

Task Priority
Test image generation flow end-to-end HIGH
Test focus keyword persistence across modes HIGH
Test context continuity with focus keyword HIGH
Verify outline generation uses focus keyword HIGH

Files to Modify Summary

File Changes
wp-agentic-writer.php Add plugins_loaded hook for table creation
includes/class-image-manager.php Add public save_image_recommendation() method
assets/js/block-image-generate.js Fix block detection logic
assets/js/sidebar.js Replace context indicator with focus keyword bar
assets/css/sidebar.css Add focus keyword bar styles
includes/class-gutenberg-sidebar.php Update prompts to use focus_keyword

Testing Checklist

Image Generation

  • Tables exist in database
  • No PHP errors on article generation
  • Image toolbar button appears on placeholder images
  • Modal opens when clicking toolbar button
  • Image generation works end-to-end

Focus Keyword System

  • Focus keyword bar appears above chat input
  • Suggestions accumulate after each AI response
  • Selecting keyword updates postConfig
  • Custom keyword input works
  • Expanded view shows all suggestions
  • Keyword persists after page refresh
  • Outline generation uses selected keyword
  • Context maintained across chat → planning → writing modes

Document Status: Ready for Implementation
Last Updated: January 30, 2026