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
This commit is contained in:
Dwindi Ramadhana
2026-05-17 10:48:05 +07:00
parent 97426d5ab1
commit d2c10756ab
61 changed files with 18725 additions and 806 deletions

View File

@@ -136,6 +136,39 @@ class WP_Agentic_Writer_Gutenberg_Sidebar {
true
);
// Enqueue image block toolbar script.
$block_image_script_path = WP_AGENTIC_WRITER_DIR . 'assets/js/block-image-generate.js';
wp_enqueue_script(
'wp-agentic-writer-block-image-generate',
WP_AGENTIC_WRITER_URL . 'assets/js/block-image-generate.js',
array(
'wp-block-editor',
'wp-components',
'wp-compose',
'wp-data',
'wp-element',
'wp-hooks',
'wp-i18n',
),
file_exists( $block_image_script_path ) ? filemtime( $block_image_script_path ) : WP_AGENTIC_WRITER_VERSION,
true
);
// Enqueue image modal script.
$image_modal_script_path = WP_AGENTIC_WRITER_DIR . 'assets/js/image-modal.js';
wp_enqueue_script(
'wp-agentic-writer-image-modal',
WP_AGENTIC_WRITER_URL . 'assets/js/image-modal.js',
array(
'wp-components',
'wp-element',
'wp-data',
'wp-block-editor',
),
file_exists( $image_modal_script_path ) ? filemtime( $image_modal_script_path ) : WP_AGENTIC_WRITER_VERSION,
true
);
// Enqueue sidebar styles.
$style_path = WP_AGENTIC_WRITER_DIR . 'assets/css/sidebar.css';
wp_enqueue_style(
@@ -464,6 +497,37 @@ class WP_Agentic_Writer_Gutenberg_Sidebar {
'permission_callback' => array( $this, 'check_permissions' ),
)
);
// Image generation endpoints.
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' ),
)
);
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' ),
)
);
}
/**
@@ -498,17 +562,39 @@ class WP_Agentic_Writer_Gutenberg_Sidebar {
$stored_language = get_post_meta( $post_id, '_wpaw_detected_language', true );
$effective_language = $this->resolve_language_preference( $post_config, $detected_from_message ?: $stored_language );
// Extract focus keyword for context anchoring
$focus_keyword = '';
if ( ! empty( $post_config['focus_keyword'] ) ) {
$focus_keyword = sanitize_text_field( $post_config['focus_keyword'] );
} elseif ( ! empty( $post_config['seo_focus_keyword'] ) ) {
$focus_keyword = sanitize_text_field( $post_config['seo_focus_keyword'] );
} elseif ( $post_id > 0 ) {
$focus_keyword = get_post_meta( $post_id, '_wpaw_focus_keyword', true );
}
// Build focus keyword instruction for chat
$focus_keyword_instruction = '';
if ( ! empty( $focus_keyword ) ) {
$focus_keyword_instruction = "
CONTEXT ANCHOR: The user is working on an article about \"{$focus_keyword}\".
Keep your responses relevant to this primary topic. If the conversation drifts, gently guide it back to \"{$focus_keyword}\".
At the END of your response, if you identify a good focus keyword from the discussion, suggest it in this format:
**Focus Keyword Suggestion:** [your suggested keyword]
";
}
$language_instruction = $this->build_language_instruction( $effective_language, 'chat responses' );
$system_prompt = "You are a helpful writing assistant. Answer clearly, with concise structure and practical suggestions.
{$focus_keyword_instruction}
CRITICAL LANGUAGE REQUIREMENT:
{$language_instruction}
{$post_config_context}";
$messages = $this->prepend_system_prompt( $messages, $system_prompt );
// Get OpenRouter provider.
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
// Get provider for this task type.
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( $type );
if ( $stream ) {
$web_search_options = $this->get_web_search_options( $post_config );
@@ -567,33 +653,55 @@ CRITICAL LANGUAGE REQUIREMENT:
header( 'Cache-Control: no-cache' );
header( 'X-Accel-Buffering: no' );
if ( ob_get_level() > 0 ) {
// Aggressively disable ALL output buffering layers (WordPress nests multiple)
@ini_set( 'output_buffering', 'Off' );
@ini_set( 'zlib.output_compression', false );
while ( ob_get_level() > 0 ) {
ob_end_flush();
}
flush();
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( $type );
$accumulated_content = '';
$total_cost = 0;
$chunks_emitted = 0;
$this->maybe_inject_brave_search( $messages, $provider, $web_search_options );
$response = $provider->chat_stream(
$messages,
$web_search_options,
$type,
function( $chunk, $is_complete, $full_content ) use ( &$accumulated_content ) {
function( $chunk, $is_complete, $full_content ) use ( &$accumulated_content, &$chunks_emitted ) {
$accumulated_content = $full_content;
if ( '' !== $chunk ) {
$chunks_emitted++;
echo "data: " . wp_json_encode(
array(
'type' => 'conversational_stream',
'content' => $accumulated_content,
)
) . "\n\n";
if ( ob_get_level() > 0 ) {
ob_end_flush();
}
flush();
}
}
);
// Fallback: if streaming produced no chunks but we have accumulated content, emit it now
if ( 0 === $chunks_emitted && ! is_wp_error( $response ) && ! empty( $response['content'] ) ) {
$accumulated_content = $response['content'];
echo "data: " . wp_json_encode(
array(
'type' => 'conversational_stream',
'content' => $accumulated_content,
)
) . "\n\n";
flush();
}
if ( is_wp_error( $response ) ) {
echo "data: " . wp_json_encode(
array(
@@ -1020,11 +1128,12 @@ CRITICAL LANGUAGE REQUIREMENT:
}
// If fallback is provided and not empty, use it
if ( ! empty( $fallback ) && 'auto' !== $fallback ) {
return $fallback;
if ( ! empty( $fallback ) && 'auto' !== strtolower( $fallback ) ) {
return strtolower( $fallback );
}
return 'english';
// Default to 'auto' instead of 'english' to let AI detect from context
return 'auto';
}
/**
@@ -1040,7 +1149,7 @@ CRITICAL LANGUAGE REQUIREMENT:
// If auto or empty, let AI detect from context
if ( empty( $language ) || 'auto' === strtolower( $language ) ) {
return "Write the {$context} in the most appropriate language based on the topic and context.";
return "CRITICAL: Detect the language from the conversation history and topic. Write ALL {$context} in the SAME language as the user's input. If the user wrote in Indonesian, write in Indonesian. If English, write in English. Match the user's language exactly.";
}
// Pass any language name directly to AI - AI models understand all languages
@@ -1072,6 +1181,63 @@ CRITICAL LANGUAGE REQUIREMENT:
return $messages;
}
/**
* Physically scrapes the web and injects the results as a system prompt if applicable.
*
* @since 0.1.0
* @param array &$messages Chat messages (passed by reference).
* @param object $provider AI Provider instance.
* @param array $web_search_options Web search options.
* @return void
*/
private function maybe_inject_brave_search( &$messages, $provider, $web_search_options ) {
if ( empty( $web_search_options['web_search_enabled'] ) ) {
return;
}
// Only inject if the provider doesn't natively support OpenRouter's web search routing plugins
if ( $provider instanceof WP_Agentic_Writer_OpenRouter_Provider ) {
return;
}
$last_query = '';
foreach ( array_reverse( $messages ) as $msg ) {
if ( 'user' === $msg['role'] ) {
$last_query = (string) $msg['content'];
break;
}
}
if ( empty( $last_query ) ) {
return;
}
$brave_search = WP_Agentic_Writer_Brave_Search_API::get_instance();
$results = $brave_search->search( $last_query, 3 );
if ( ! is_wp_error( $results ) && ! empty( $results ) ) {
$context_markdown = $brave_search->format_results_for_llm( $results, $last_query );
$injection_message = array(
'role' => 'system',
'content' => $context_markdown
);
$injected = false;
for( $i = count( $messages ) - 1; $i >= 0; $i-- ) {
if ( 'user' === $messages[ $i ]['role'] ) {
array_splice( $messages, $i, 0, array( $injection_message ) );
$injected = true;
break;
}
}
if ( ! $injected ) {
array_unshift( $messages, $injection_message );
}
}
}
/**
* Build web search option overrides.
*
@@ -1206,12 +1372,22 @@ CRITICAL LANGUAGE REQUIREMENT:
return $this->stream_generate_plan( $topic, $context, $post_id, $auto_execute, $article_length, $clarification_answers, $effective_language, $post_config, $chat_history );
}
// Get OpenRouter provider.
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
// Get provider for planning task.
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'planning' );
// Build prompt for plan generation.
$plan_language_instruction = $this->build_language_instruction( $effective_language, 'article plan (title, section headings, descriptions)' );
$system_prompt = "You are an expert content strategist and technical writer. Your task is to create a detailed article plan/outline based on the user's topic and context.
$system_prompt = "You are an Information Architect and SEO/GEO Strategist. Your task is to outline a high-information-density article based on the user's topic and context.
ANTI-ROBOT RULES:
- Never use generic intros or 'throat-clearing' fluff.
- Avoid academic, pompous, or 'expert' posturing.
- Headings must provide direct value or ask specific questions the article will answer.
GEO/SEO STRATEGY:
- Design the outline for Generative Engine Optimization (GEO): sections must flow logically to answer the user's core intent comprehensively.
- Suggest strategic use of tables, bullet points, and Q&A formats where they maximize information density.
- Incorporate secondary entities and related concepts naturally to show topical depth.
CRITICAL LANGUAGE REQUIREMENT:
{$plan_language_instruction}
@@ -1256,6 +1432,7 @@ Keep sections focused and actionable. Include H2 headings only. For technical ar
);
// Generate plan.
$this->maybe_inject_brave_search( $messages, $provider, $web_search_options );
$response = $provider->chat( $messages, array_merge( array( 'temperature' => 0.7 ), $web_search_options ), 'planning' );
if ( is_wp_error( $response ) ) {
@@ -1350,7 +1527,7 @@ Keep sections focused and actionable. Include H2 headings only. For technical ar
);
}
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'planning' );
$memory_context = $this->get_post_memory_context( $post_id );
$system_prompt = "You are an expert content strategist. Revise the provided outline based on the user's instruction.
@@ -1402,6 +1579,8 @@ Rules:
),
);
// Generate revised plan.
$this->maybe_inject_brave_search( $messages, $provider, $web_search_options );
$response = $provider->chat( $messages, array_merge( array( 'temperature' => 0.6 ), $web_search_options ), 'planning' );
if ( is_wp_error( $response ) ) {
@@ -1576,13 +1755,26 @@ Rules:
}
flush();
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'writing' );
$total_cost = 0;
$post_config = $this->sanitize_post_config( wp_parse_args( $post_config, $this->get_default_post_config() ) );
$post_config_context = $this->build_post_config_context( $post_config );
$web_search_options = $this->get_web_search_options( $post_config );
$effective_language = $this->resolve_language_preference( $post_config, $detected_language );
// Extract focus keyword for context anchoring
$focus_keyword = '';
if ( ! empty( $post_config['focus_keyword'] ) ) {
$focus_keyword = sanitize_text_field( $post_config['focus_keyword'] );
} elseif ( ! empty( $post_config['seo_focus_keyword'] ) ) {
$focus_keyword = sanitize_text_field( $post_config['seo_focus_keyword'] );
}
// Save focus keyword to post meta for persistence
if ( $post_id > 0 && ! empty( $focus_keyword ) ) {
update_post_meta( $post_id, '_wpaw_focus_keyword', $focus_keyword );
}
try {
// Note: Clarity check should be done BEFORE calling this streaming endpoint
// The frontend is responsible for checking clarity first via /check-clarity
@@ -1656,8 +1848,22 @@ Rules:
// Determine language instruction for plan generation
$plan_language_instruction = $this->build_language_instruction( $effective_language, 'article plan (title, section headings, descriptions)' );
$system_prompt = "You are an expert content strategist and technical writer. Your task is to create a detailed article plan/outline based on the user's topic and context.
// Build focus keyword anchor instruction
$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 clearly 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, treat them as ASPECTS of the primary topic \"{$focus_keyword}\"
";
}
$system_prompt = "You are an expert content strategist and technical writer. Your task is to create a detailed article plan/outline based on the user's topic and context.
{$focus_keyword_instruction}
CRITICAL LANGUAGE REQUIREMENT:
{$plan_language_instruction}
@@ -1706,6 +1912,7 @@ Keep sections focused and actionable. Include H2 headings only. For technical ar
error_log( 'WP Agentic Writer: Calling OpenRouter API for planning. Topic: ' . substr( $topic, 0, 100 ) );
error_log( 'WP Agentic Writer: Detected language: ' . $detected_language );
$this->maybe_inject_brave_search( $messages, $provider, $web_search_options );
$response = $provider->chat( $messages, array_merge( array( 'temperature' => 0.7 ), $web_search_options ), 'planning' );
error_log( 'WP Agentic Writer: OpenRouter API response received' );
@@ -2105,7 +2312,31 @@ Remember: You MUST include the ~~~ARTICLE~~~ divider to separate your conversati
// NOW parse the complete markdown content and send blocks
if ( ! empty( $markdown_content ) ) {
$markdown_blocks = WP_Agentic_Writer_Markdown_Parser::parse( $markdown_content );
// Extract image placeholders and generate IDs
$image_placeholders = array();
if ( preg_match_all( '/\[IMAGE:\s*(.+?)\]/i', $markdown_content, $matches ) ) {
$image_manager = WP_Agentic_Writer_Image_Manager::get_instance();
foreach ( $matches[1] as $index => $description ) {
$agent_image_id = 'img_' . $post_id . '_' . time() . '_' . ( $index + 1 );
$image_placeholders[] = array(
'agent_image_id' => $agent_image_id,
'description' => trim( $description ),
);
// Save to database
$image_manager->save_image_recommendation(
$post_id,
$agent_image_id,
'section_' . $section_id,
$heading,
trim( $description ),
trim( $description )
);
}
}
$markdown_blocks = WP_Agentic_Writer_Markdown_Parser::parse( $markdown_content, $image_placeholders );
foreach ( $markdown_blocks as $block ) {
echo "data: " . wp_json_encode(
@@ -2221,8 +2452,8 @@ Remember: You MUST include the ~~~ARTICLE~~~ divider to separate your conversati
}
}
// Get OpenRouter provider.
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
// Get provider for writing task.
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'writing' );
$image_instruction = "IMAGE SUGGESTIONS:
- Suggest where images would enhance understanding
@@ -2282,7 +2513,18 @@ Remember: You MUST include the ~~~ARTICLE~~~ divider to separate your conversati
}
// Build system prompt for article generation.
$system_prompt = "You are an expert technical writer and blogger. Your task is to write engaging, clear, and well-structured content based on the provided article plan.
$system_prompt = "You are an industry practitioner sharing insights with a colleague. Write engaging, high-information-density content based on the provided article plan.
ANTI-ROBOT RULES:
- BANNED WORDS: delve, furthermore, moreover, crucial, paramount, landscape, testament, in today's digital world, in conclusion.
- BANNED PATTERNS: Do not use transition words to start paragraphs. Do not summarize what you are about to say. Do not summarize what you just said.
- BURSTINESS: Mix very short, punchy sentences (3-5 words) with longer, descriptive ones. Avoid uniform sentence length.
- TONE: Conversational, direct, pragmatic. Do not sound like an academic 'expert' or textbook.
GEO/SEO STRATEGY:
- Answer the implicit user intent directly and immediately in the first paragraph.
- Maximize information density: high ratio of facts/insights to total word count. Remove filler adjectives.
- Use bullet points or numbered lists where they make data easier to scan.
CRITICAL LANGUAGE REQUIREMENT:
{$language_instruction}
@@ -2291,22 +2533,17 @@ CRITICAL LANGUAGE REQUIREMENT:
Follow these guidelines:
- Use the tone specified in POST CONFIG if provided; otherwise be conversational but professional
- Use clear examples and analogies
- For code blocks, use proper syntax highlighting
- Keep paragraphs concise (2-3 sentences)
- Use transitions between sections
- Write for the specified difficulty level
- Code formatting: Any code/config snippets MUST be in fenced code blocks with a language tag (e.g., ```php). Never place code inline in paragraphs.
- Embed secondary keywords naturally as concepts, without forcing exact matches
- For code blocks, use proper syntax highlighting (e.g., ```php)
- Code typography: Use plain ASCII quotes inside code. Do NOT use smart quotes.
- Write for the specified difficulty level
{$seo_instruction}
{$internal_links_instruction}
IMAGE SUGGESTIONS:
- Suggest where images would enhance understanding
- Place image suggestions on their own line using this format: [IMAGE: descriptive alt text]
- Be strategic: only suggest images where they add real value (diagrams, screenshots, visual examples)
- Good places for images: after introductions, before complex explanations, to show examples
- Maximum 1-2 image suggestions per section
- Suggest where images would enhance understanding (diagrams, screenshots)
- Place image suggestions on their own line: [IMAGE: descriptive alt text]
- Maximum 1 image per section
{$image_instruction}";
// Generate content for each section.
@@ -2435,7 +2672,7 @@ IMAGE SUGGESTIONS:
}
flush();
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'writing' );
$total_cost = 0;
$post_config = $this->sanitize_post_config( wp_parse_args( $post_config, $this->get_default_post_config() ) );
$post_config_context = $this->build_post_config_context( $post_config );
@@ -2527,7 +2764,18 @@ IMAGE SUGGESTIONS:
}
}
$system_prompt = "You are an expert technical writer and blogger. Your task is to write engaging, clear, and well-structured content based on the provided article plan.
$system_prompt = "You are an industry practitioner sharing insights with a colleague. Write engaging, high-information-density content based on the provided article plan.
ANTI-ROBOT RULES:
- BANNED WORDS: delve, furthermore, moreover, crucial, paramount, landscape, testament, in today's digital world, in conclusion.
- BANNED PATTERNS: Do not use transition words to start paragraphs. Do not summarize what you are about to say. Do not summarize what you just said.
- BURSTINESS: Mix very short, punchy sentences (3-5 words) with longer, descriptive ones. Avoid uniform sentence length.
- TONE: Conversational, direct, pragmatic. Do not sound like an academic 'expert' or textbook.
GEO/SEO STRATEGY:
- Answer the implicit user intent directly and immediately in the first paragraph.
- Maximize information density: high ratio of facts/insights to total word count. Remove filler adjectives.
- Use bullet points or numbered lists where they make data easier to scan.
CRITICAL LANGUAGE REQUIREMENT:
{$language_instruction}
@@ -2535,22 +2783,17 @@ CRITICAL LANGUAGE REQUIREMENT:
Follow these guidelines:
- Use the tone specified in POST CONFIG if provided; otherwise be conversational but professional
- Use clear examples and analogies
- For code blocks, use proper syntax highlighting
- Keep paragraphs concise (2-3 sentences)
- Use transitions between sections
- Write for the specified difficulty level
- Code formatting: Any code/config snippets MUST be in fenced code blocks with a language tag (e.g., ```php). Never place code inline in paragraphs.
- Embed secondary keywords naturally as concepts, without forcing exact matches
- For code blocks, use proper syntax highlighting (e.g., ```php)
- Code typography: Use plain ASCII quotes inside code. Do NOT use smart quotes.
- Write for the specified difficulty level
{$seo_instruction}
{$internal_links_instruction}
IMAGE SUGGESTIONS:
- Suggest where images would enhance understanding
- Place image suggestions on their own line using this format: [IMAGE: descriptive alt text]
- Be strategic: only suggest images where they add real value (diagrams, screenshots, visual examples)
- Good places for images: after introductions, before complex explanations, to show examples
- Maximum 1-2 image suggestions per section
- Suggest where images would enhance understanding (diagrams, screenshots)
- Place image suggestions on their own line: [IMAGE: descriptive alt text]
- Maximum 1 image per section
{$image_instruction}";
$section_index = 0;
@@ -2795,8 +3038,8 @@ IMAGE SUGGESTIONS:
);
}
// Get OpenRouter provider.
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
// Get provider for writing task.
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'writing' );
$messages = array(
array(
@@ -2944,7 +3187,7 @@ IMAGE SUGGESTIONS:
);
}
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'writing' );
// Get settings.
$settings = get_option( 'wp_agentic_writer_settings', array() );
@@ -2960,15 +3203,21 @@ IMAGE SUGGESTIONS:
'pov',
);
// If quiz is disabled, always return clear.
// If quiz is disabled, skip AI questions but still add MANDATORY config questions
if ( ! $enabled ) {
$result = array(
'is_clear' => true,
'confidence' => 1.0,
'questions' => array(),
);
// MANDATORY: Always add config questions (language, focus keyword)
$result['questions'] = $this->append_config_questions( $result['questions'], $post_config );
if ( ! empty( $result['questions'] ) ) {
$result['is_clear'] = false; // Force quiz for config questions
}
return new WP_REST_Response(
array(
'result' => array(
'is_clear' => true,
'confidence' => 1.0,
'questions' => array(),
),
'result' => $result,
'cost' => 0,
),
200
@@ -3079,9 +3328,11 @@ QUESTION TYPES:
CONFIDENCE CALCULATION:
- Start at 100% (1.0)
- Subtract 20% for each missing HIGH-PRIORITY category (topic_scope, target_platform, specific_focus)
- Subtract 15% for each missing HIGH-PRIORITY category (topic_scope, target_platform, specific_focus)
- Subtract 10% for each missing SECONDARY category (target_outcome, target_audience, content_depth)
- CRITICAL: If chat history exists with detailed discussion, ADD 20% confidence bonus (user already provided context)
- If confidence < {$threshold}, generate questions starting with HIGH-PRIORITY
- MANDATORY: Even if confidence >= threshold, ALWAYS ask at least 1-2 system config questions (language, SEO settings)
QUESTION GENERATION STRATEGY:
1. Always detect user language first and match it
@@ -3118,6 +3369,11 @@ No markdown, no explanation - just JSON.";
// Log error and use default questions instead of failing.
error_log( 'WP Agentic Writer: Clarity check API error - ' . $response->get_error_message() );
$result = $this->get_default_clarification_questions( $topic );
// MANDATORY: Always add config questions
$result['questions'] = $this->append_config_questions( $result['questions'] ?? array(), $post_config );
if ( ! empty( $result['questions'] ) ) {
$result['is_clear'] = false;
}
return new WP_REST_Response(
array(
'result' => $result,
@@ -3135,6 +3391,11 @@ No markdown, no explanation - just JSON.";
// Log parse error and use default questions instead of failing.
error_log( 'WP Agentic Writer: Failed to parse clarity check JSON' );
$result = $this->get_default_clarification_questions( $topic );
// MANDATORY: Always add config questions
$result['questions'] = $this->append_config_questions( $result['questions'] ?? array(), $post_config );
if ( ! empty( $result['questions'] ) ) {
$result['is_clear'] = false;
}
return new WP_REST_Response(
array(
'result' => $result,
@@ -3156,15 +3417,15 @@ No markdown, no explanation - just JSON.";
$response['cost'] ?? 0
);
// Always add configuration questions, even if no clarity questions
// MANDATORY: Always add configuration questions
if ( ! isset( $result['questions'] ) || ! is_array( $result['questions'] ) ) {
$result['questions'] = array();
}
$result['questions'] = $this->append_config_questions( $result['questions'], $post_config );
// If only config questions exist and clarity is clear, still show config
if ( empty( $result['questions'] ) === false && $result['is_clear'] === true ) {
$result['is_clear'] = false; // Force quiz to show for config questions
// CRITICAL: Always show quiz if config questions exist (system questions are MANDATORY)
if ( ! empty( $result['questions'] ) ) {
$result['is_clear'] = false; // Force quiz to show - config questions are mandatory
}
return new WP_REST_Response(
@@ -3312,7 +3573,7 @@ No markdown, no explanation - just JSON.";
return $this->stream_block_refine( $block_id, $block_type, $block_content, $refinement_request, $article_context, $post_id, $post_config );
}
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'refinement' );
// Build context from article structure.
$context_str = "\n\nArticle Context:\n";
@@ -3450,7 +3711,7 @@ Keep the same block type (paragraph, heading, list, etc.).";
}
flush();
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'refinement' );
$post_config = $this->sanitize_post_config( wp_parse_args( $post_config, $this->get_default_post_config() ) );
$post_config_context = $this->build_post_config_context( $post_config );
$stored_language = get_post_meta( $post_id, '_wpaw_detected_language', true );
@@ -3473,10 +3734,16 @@ Keep the same block type (paragraph, heading, list, etc.).";
$context_str .= "Next section: " . $article_context['nextBlock']['heading'] . "\n";
}
$system_prompt = "You are an expert content refiner. Your task is to rewrite and improve content based on the user's request.
$system_prompt = "You are a precise content editor. Your task is to refine the provided content based strictly on the user's request.
ANTI-ROBOT RULES:
- BANNED WORDS: delve, furthermore, moreover, crucial, paramount, landscape, testament.
- Do not add introductory throat-clearing sentences or summarizing conclusions unless explicitly requested.
- Increase specificity and information density. Do not just increase word count with conversational filler.
CRITICAL LANGUAGE REQUIREMENT:
{$language_instruction}
{$post_config_context}
{$context_str}
@@ -4057,7 +4324,7 @@ No markdown, no explanation - just JSON.";
);
}
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'writing' );
$post_config = $this->sanitize_post_config( wp_parse_args( $post_config, $this->get_default_post_config() ) );
$post_config_context = $this->build_post_config_context( $post_config );
$stored_language = get_post_meta( $post_id, '_wpaw_detected_language', true );
@@ -4225,7 +4492,12 @@ Blocks:
$context_str .= "Next section: " . $article_context['nextBlock']['heading'] . "\n";
}
$system_prompt = "You are an expert content refiner. Your task is to rewrite and improve content based on the user's request.
$system_prompt = "You are a precise content editor. Your task is to refine the provided content based strictly on the user's request.
ANTI-ROBOT RULES:
- BANNED WORDS: delve, furthermore, moreover, crucial, paramount, landscape, testament.
- Do not add introductory throat-clearing sentences or summarizing conclusions unless explicitly requested.
- Increase specificity and information density. Do not just increase word count with conversational filler.
CRITICAL LANGUAGE REQUIREMENT:
{$language_instruction}
@@ -5026,7 +5298,7 @@ Output format:
}
$language_instruction = $this->build_language_instruction( $effective_language, 'meta description' );
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'clarity' );
$prompt = "Generate a compelling meta description for SEO. Requirements:\n";
$prompt .= "- Length: MAXIMUM 155 characters (STRICT - count every character including spaces)\n";
@@ -5138,7 +5410,7 @@ Output format:
}
}
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'clarity' );
$prompt = "Generate a compelling meta description for SEO. Requirements:\n";
$prompt .= "- Length: MAXIMUM 155 characters (STRICT - count every character including spaces)\n";
@@ -5303,8 +5575,8 @@ PREFERENCES: [any specific requirements]
Conversation:
{$history_text}";
// Call AI with cheap model
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
// Call AI with clarity model for language detection
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'clarity' );
$messages = array(
array(
'role' => 'user',
@@ -5385,8 +5657,8 @@ User's message: \"{$last_message}\"
Respond with ONLY the intent code (e.g., \"create_outline\"). No explanation.";
// Call AI with cheap model
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
// Call AI with clarity model for intent detection
$provider = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( 'clarity' );
$messages = array(
array(
'role' => 'user',
@@ -5429,4 +5701,79 @@ Respond with ONLY the intent code (e.g., \"create_outline\"). No explanation.";
200
);
}
/**
* Handle get image recommendations request.
*
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error Response.
*/
public function handle_get_image_recommendations( $request ) {
$post_id = $request->get_param( 'post_id' );
$image_manager = WP_Agentic_Writer_Image_Manager::get_instance();
$images = $image_manager->get_image_recommendations( $post_id );
return new WP_REST_Response(
array( 'images' => $images ),
200
);
}
/**
* Handle generate image request.
*
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error Response.
*/
public function handle_generate_image( $request ) {
$post_id = $request->get_param( 'post_id' );
$agent_image_id = $request->get_param( 'agent_image_id' );
$prompt = $request->get_param( 'prompt' );
$variant_count = $request->get_param( 'variant_count' ) ?? 2;
$image_manager = WP_Agentic_Writer_Image_Manager::get_instance();
$variants = $image_manager->generate_image_variants(
$post_id,
$agent_image_id,
$prompt,
$variant_count
);
if ( is_wp_error( $variants ) ) {
return $variants;
}
return new WP_REST_Response(
array( 'variants' => $variants ),
200
);
}
/**
* Handle commit image request.
*
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error Response.
*/
public function handle_commit_image( $request ) {
$post_id = $request->get_param( 'post_id' );
$agent_image_id = $request->get_param( 'agent_image_id' );
$variant_id = $request->get_param( 'variant_id' );
$alt_text = $request->get_param( 'alt' );
$image_manager = WP_Agentic_Writer_Image_Manager::get_instance();
$result = $image_manager->commit_image_variant(
$post_id,
$agent_image_id,
$variant_id,
$alt_text
);
if ( is_wp_error( $result ) ) {
return $result;
}
return new WP_REST_Response( $result, 200 );
}
}