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

861 lines
30 KiB
Markdown

# Focus Keyword Anchor System & Image Generation Fixes
**Date:** January 30, 2026
**Version:** 1.0
**Status:** Implementation Plan
---
## Table of Contents
1. [Executive Summary](#executive-summary)
2. [Part A: Focus Keyword Anchor System](#part-a-focus-keyword-anchor-system)
3. [Part B: Image Generation Fixes](#part-b-image-generation-fixes)
4. [Part C: UI Redesign](#part-c-ui-redesign)
5. [Implementation Priority](#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
```javascript
// 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`
```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`
```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`
```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`
```javascript
// 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
```javascript
const handleFocusKeywordChange = (keyword) => {
setSelectedFocusKeyword(keyword);
updatePostConfig('focus_keyword', keyword);
};
```
#### 3. Extract Suggestions from AI Responses
```javascript
// 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)
```javascript
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
```css
.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`
```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:
```sql
-- 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):**
```php
$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):**
```php
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:
```php
/**
* 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**
```javascript
// 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**
```javascript
// 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.
```javascript
// 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:
```javascript
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`
```javascript
// 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