- 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
30 KiB
Focus Keyword Anchor System & Image Generation Fixes
Date: January 30, 2026
Version: 1.0
Status: Implementation Plan
Table of Contents
- Executive Summary
- Part A: Focus Keyword Anchor System
- Part B: Image Generation Fixes
- Part C: UI Redesign
- 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
- Empty State: Placeholder "Select or enter focus keyword..."
- After Response #1: 1 suggestion appears
- After Response #2: 2 suggestions (cumulative)
- Max 5 suggestions: Older ones rotate out, "Show all" option
- Custom Option: Always last, triggers text input
- 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:
- Method name is plural (
save_image_recommendations) vs singular (save_image_recommendation) - Method is
private, notpublic - 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
- Script not loaded: Check browser console for errors
- Block attribute missing:
data-agent-image-idnot set on blocks - 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