sidebar = $sidebar; } /** * Handle execute article request. * * @since 0.1.0 * @param WP_REST_Request $request REST request. * @return WP_REST_Response|WP_Error Response. */ public function handle_execute_article($request) { // Check rate limit. $rate_limit = WPAW_Rate_Limiter::check("execute_article"); if (is_wp_error($rate_limit)) { return $rate_limit; } $params = $request->get_json_params(); $post_id = $params["postId"] ?? 0; $session_id = $this->sidebar->resolve_or_create_session_id( $params["sessionId"] ?? "", $post_id, ); $stream = $params["stream"] ?? false; $recommended_title = ""; $chat_history = $params["chatHistory"] ?? []; $post_config = $this->sidebar->resolve_post_config_from_request( $params, $post_id, ); $post_config_context = $this->sidebar->build_post_config_context($post_config); $stored_language = get_post_meta( $post_id, "_wpaw_detected_language", true, ); $detected_language = $params["detectedLanguage"] ?? $stored_language; $effective_language = $this->sidebar->resolve_language_preference( $post_config, $detected_language, ); // Auto-save post and link conversation if needed (only for post_id = 0) if (empty($post_id) && !empty($session_id)) { $post_id = $this->sidebar->ensure_conversation_linked_to_post( $session_id, $post_id, ); } // Check post permission if post_id is provided. if ($post_id > 0 && !$this->sidebar->check_post_permission($post_id)) { return new WP_Error( "forbidden", __( "You do not have permission to edit this post.", "wp-agentic-writer", ), ["status" => 403], ); } // Get plan from post meta. $plan = get_post_meta($post_id, "_wpaw_plan", true); if (empty($plan)) { return new WP_Error( "no_plan", __( "No plan found. Please generate a plan first.", "wp-agentic-writer", ), ["status" => 400], ); } if ($stream) { // For streaming, link conversation to post BEFORE getting plan from meta if (empty($post_id) && !empty($session_id)) { $post_id = $this->sidebar->ensure_conversation_linked_to_post( $session_id, $post_id, ); } // Now get plan after potentially having a valid post_id $plan = get_post_meta($post_id, "_wpaw_plan", true); if (empty($plan)) { echo "data: " . wp_json_encode([ "type" => "error", "message" => "No plan found. Please generate a plan first.", ]) . "\n\n"; flush(); return; } $this->sidebar->stream_execute_article( $plan, $post_id, $post_config, $effective_language, $session_id, ); exit(); } $plan = $this->sidebar->ensure_plan_sections_with_tasks($plan); // MEMANTO: Plan execution implies approval. do_action("wpaw_memanto_plan_approved", $post_id, $plan); // Update post title from the plan title when available. if (!empty($plan["title"])) { $recommended_title = sanitize_text_field($plan["title"]); if ($post_id > 0) { $post = get_post($post_id); if ($post && current_user_can("edit_post", $post_id)) { if (empty($post->post_title)) { wp_update_post([ "ID" => $post_id, "post_title" => $recommended_title, ]); } } } } // Get provider for writing task. $provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( "writing", ); $provider = $provider_result->provider; $image_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 - Example: [IMAGE: Screenshot of the plugin settings panel showing the API key field]"; if (empty($post_config["include_images"])) { $image_instruction = "IMAGE SUGGESTIONS: - Do NOT include any image suggestions or [IMAGE: ...] placeholders."; } $language_instruction = $this->sidebar->build_language_instruction( $effective_language, "article content", ); // Build chat history context for continuity $chat_history_context = ""; if (!empty($chat_history) && is_array($chat_history)) { $chat_history_context = "\n\n--- CONVERSATION CONTEXT ---\n"; foreach ($chat_history as $msg) { $role = isset($msg["role"]) ? ucfirst($msg["role"]) : "Unknown"; $content = isset($msg["content"]) ? $msg["content"] : ""; if ( !empty($content) && "system" !== strtolower($msg["role"] ?? "") ) { $chat_history_context .= "{$role}: {$content}\n\n"; } } $chat_history_context .= "--- END CONVERSATION CONTEXT ---\n"; $chat_history_context .= "Use the above conversation to understand the user's intent and preferences for this article."; } // Build SEO instructions if SEO is enabled $seo_instruction = ""; $internal_links_instruction = ""; if ( !empty($post_config["seo_enabled"]) && !empty($post_config["seo_focus_keyword"]) ) { $focus_keyword = $post_config["seo_focus_keyword"]; $seo_instruction = "\n\nSEO OPTIMIZATION REQUIREMENTS (CRITICAL - MUST FOLLOW): - Focus Keyword: \"{$focus_keyword}\" - MANDATORY: Include the exact focus keyword \"{$focus_keyword}\" in the article title (preferably at the beginning) - MANDATORY: Use the focus keyword in the FIRST paragraph (within the first 100 words) - Use the focus keyword 5-8 times naturally throughout the article - Include the focus keyword in: * At least 2 H2 or H3 subheadings * The conclusion paragraph - Include 2-3 authoritative outbound links to reputable sources (Wikipedia, official documentation, industry leaders) - When suggesting images, include the focus keyword or related terms in the alt text - Keep the article title under 60 characters"; // Get internal link suggestions $internal_links = $this->sidebar->suggest_internal_links( $post_id, $focus_keyword, 3, ); if (!empty($internal_links)) { $internal_links_instruction = "\n\nINTERNAL LINKS (optional - use where contextually relevant):\n"; foreach ($internal_links as $link) { $internal_links_instruction .= "- [{$link["title"]}]({$link["url"]})\n"; } $internal_links_instruction .= "Naturally incorporate 1-2 of these internal links where they add value to the reader. Use descriptive anchor text, not 'click here'."; } } // Build system prompt for article generation. $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} {$post_config_context} {$chat_history_context} Follow these guidelines: - Use the tone specified in POST CONFIG if provided; otherwise be conversational but professional - 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 (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. $blocks = []; $total_cost = 0; $sections_to_write = []; foreach ($plan["sections"] as $index => $section) { $status = $section["status"] ?? "pending"; if ("done" === $status) { continue; } $sections_to_write[$index] = $section; } foreach ($sections_to_write as $section) { $heading = $section["heading"] ?? ($section["title"] ?? ""); $section_prompt = $heading ? "Write the \"{$heading}\" section.\n\n" : "Write the next section.\n\n"; $section_prompt .= "Content requirements:\n"; if (!empty($section["content"]) && is_array($section["content"])) { foreach ($section["content"] as $item) { if (!empty($item["content"])) { $section_prompt .= "- {$item["content"]}\n"; } } } $messages = [ [ "role" => "system", "content" => $system_prompt, ], [ "role" => "user", "content" => $section_prompt, ], ]; $response = $provider->chat( $messages, ["temperature" => 0.8], "execution", ); if (is_wp_error($response)) { return new WP_Error( "execution_error", $response->get_error_message(), ["status" => 500], ); } // Add section blocks. if ($heading) { $blocks[] = [ "type" => "heading", "content" => $heading, "level" => 2, ]; } $section_blocks = WP_Agentic_Writer_Markdown_Parser::parse( $response["content"], ); if (!empty($section_blocks)) { $first_block = $section_blocks[0]; if ( isset($first_block["blockName"]) && "core/heading" === $first_block["blockName"] ) { $first_heading = $first_block["attrs"]["content"] ?? ""; if ( $heading && $first_heading && 0 === strcasecmp(trim($first_heading), trim($heading)) ) { array_shift($section_blocks); } } foreach ($section_blocks as $block) { $blocks[] = $block; } } else { $blocks[] = [ "type" => "paragraph", "content" => $response["content"], ]; } $total_cost += $response["cost"]; // MEMANTO: Remember section written. $section_id = $section["id"] ?? sanitize_title($heading); do_action( "wpaw_memanto_section_written", $post_id, $section_id, $heading, ); } if (!empty($sections_to_write)) { foreach (array_keys($sections_to_write) as $section_index) { $plan["sections"][$section_index]["status"] = "done"; } if ($post_id > 0) { update_post_meta($post_id, "_wpaw_plan", $plan); } } // Track total cost. $this->sidebar->track_ai_cost( $post_id, $this->sidebar->get_provider_execution_model($provider, "execution"), "execution", 0, 0, $total_cost, $provider_result, "", "success", ); return new WP_REST_Response( [ "blocks" => $blocks, "cost" => $total_cost, "recommended_title" => $recommended_title, "provider_metadata" => $this->sidebar->build_provider_metadata( $provider_result, $this->sidebar->get_provider_execution_model($provider, "execution"), ), ], 200, ); } /** * Handle reformat blocks request. * * @since 0.1.0 * @param WP_REST_Request $request REST request. * @return WP_REST_Response|WP_Error Response. */ public function handle_reformat_blocks($request) { $params = $request->get_json_params(); $blocks = $params["blocks"] ?? []; $post_id = $params["postId"] ?? 0; $recommended_title = ""; $title_updated = false; if (empty($blocks) || !is_array($blocks)) { return new WP_Error( "no_blocks", __("Blocks are required to reformat.", "wp-agentic-writer"), ["status" => 400], ); } // Check post permission if post_id is provided. if ($post_id > 0 && !$this->sidebar->check_post_permission($post_id)) { return new WP_Error( "forbidden", __( "You do not have permission to edit this post.", "wp-agentic-writer", ), ["status" => 403], ); } $results = []; if ($post_id > 0) { $plan = get_post_meta($post_id, "_wpaw_plan", true); if (is_array($plan) && !empty($plan["title"])) { $recommended_title = sanitize_text_field($plan["title"]); } } foreach ($blocks as $block) { $client_id = $block["clientId"] ?? ($block["attrs"]["clientId"] ?? ""); $block_type = $block["name"] ?? ($block["blockName"] ?? "core/paragraph"); $block_attrs = $block["attributes"] ?? ($block["attrs"] ?? []); if (empty($client_id)) { continue; } if ("core/paragraph" !== $block_type) { continue; } $content = $this->sidebar->extract_block_content_from_attrs( $block_type, $block_attrs, ); if ("" === trim((string) $content)) { continue; } $parsed_blocks = WP_Agentic_Writer_Markdown_Parser::parse($content); if (empty($parsed_blocks)) { continue; } $results[] = [ "clientId" => $client_id, "blocks" => $parsed_blocks, ]; } if ($post_id > 0 && "" !== $recommended_title) { $post = get_post($post_id); if ($post && current_user_can("edit_post", $post_id)) { if (empty($post->post_title)) { wp_update_post([ "ID" => $post_id, "post_title" => $recommended_title, ]); $title_updated = true; } } } return new WP_REST_Response( [ "results" => $results, "recommended_title" => $recommended_title, "title_updated" => $title_updated, ], 200, ); } /** * Handle save section blocks request. * * @since 0.1.0 * @param WP_REST_Request $request REST request. * @return WP_REST_Response|WP_Error Response. */ public function handle_save_section_blocks($request) { $params = $request->get_json_params(); $post_id = intval($params["postId"] ?? 0); $section_id = sanitize_text_field($params["sectionId"] ?? ""); $block_ids = $params["blockIds"] ?? []; if ($post_id <= 0 || empty($section_id) || !is_array($block_ids)) { return new WP_Error( "invalid_section_blocks", __( "Invalid section block mapping request.", "wp-agentic-writer", ), ["status" => 400], ); } if (!$this->sidebar->check_post_permission($post_id)) { return new WP_Error( "forbidden", __( "You do not have permission to edit this post.", "wp-agentic-writer", ), ["status" => 403], ); } $block_ids = array_values( array_filter(array_map("sanitize_text_field", $block_ids)), ); $mapping = get_post_meta($post_id, "_wpaw_section_blocks", true); if (!is_array($mapping)) { $mapping = []; } $mapping[$section_id] = $block_ids; update_post_meta($post_id, "_wpaw_section_blocks", $mapping); return new WP_REST_Response( [ "success" => true, "sectionId" => $section_id, "blockCount" => count($block_ids), ], 200, ); } /** * Handle get section blocks request. * * @since 0.1.0 * @param WP_REST_Request $request REST request. * @return WP_REST_Response|WP_Error Response. */ public function handle_get_section_blocks($request) { $post_id = intval($request["post_id"] ?? 0); if ($post_id <= 0) { return new WP_Error( "invalid_post", __("Invalid post ID.", "wp-agentic-writer"), ["status" => 400], ); } if (!$this->sidebar->check_post_permission($post_id)) { return new WP_Error( "forbidden", __( "You do not have permission to access this post.", "wp-agentic-writer", ), ["status" => 403], ); } $mapping = get_post_meta($post_id, "_wpaw_section_blocks", true); if (!is_array($mapping)) { $mapping = []; } return new WP_REST_Response( [ "sectionBlocks" => $mapping, ], 200, ); } }