sidebar = $sidebar; } /** * Handle check clarity request. * * @since 0.1.0 * @param WP_REST_Request $request REST request. * @return WP_REST_Response|WP_Error Response. */ public function handle_check_clarity($request) { $params = $request->get_json_params(); $topic = $params["topic"] ?? ""; $answers = $params["answers"] ?? []; $post_id = $params["postId"] ?? 0; $mode = $params["mode"] ?? "generation"; $chat_history = $params["chatHistory"] ?? []; if (empty($topic)) { return new WP_Error( "no_topic", __("Topic is required.", "wp-agentic-writer"), ["status" => 400], ); } // Check post permission BEFORE reading post data. if ($post_id > 0 && !$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], ); } // Only read post config after permission check. $post_config = $this->sidebar->resolve_post_config_from_request( $params, $post_id, ); $post_config_context = $this->sidebar->build_post_config_context($post_config); $preferred_language = $this->sidebar->resolve_language_preference( $post_config, "", ); $language_hint = ""; if ("auto" !== ($post_config["language"] ?? "auto")) { $language_hint = "\n\nPreferred language: {$preferred_language}. Ask questions in that language."; } $provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( "writing", ); $provider = $provider_result->provider; // Get settings. $settings = get_option("wp_agentic_writer_settings", []); $enabled = $settings["enable_clarification_quiz"] ?? true; $threshold = $settings["clarity_confidence_threshold"] ?? "0.6"; $required_categories = $settings["required_context_categories"] ?? [ "target_outcome", "target_audience", "tone", "content_depth", "expertise_level", ]; // If quiz is disabled, skip AI questions but still add MANDATORY config questions if (!$enabled) { $result = [ "is_clear" => true, "confidence" => 1.0, "questions" => [], ]; // 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( [ "result" => $result, "cost" => 0, ], 200, ); } // Build context from answers if available. $context = ""; if (!empty($answers)) { $context = "\n\nPrevious answers:\n"; foreach ($answers as $answer) { $context .= "- {$answer["question"]}: {$answer["answer"]}\n"; } } // Build chat history context for continuity. $chat_history_context = ""; if (!empty($chat_history) && is_array($chat_history)) { $chat_history_context = "\n\n--- CONVERSATION HISTORY (IMPORTANT - use this context!) ---\n"; foreach ($chat_history as $msg) { $role = isset($msg["role"]) ? ucfirst($msg["role"]) : "Unknown"; $content = isset($msg["content"]) ? $msg["content"] : ""; if (!empty($content)) { $chat_history_context .= "{$role}: {$content}\n\n"; } } $chat_history_context .= "--- END CONVERSATION HISTORY ---\n"; $chat_history_context .= "\nIMPORTANT: The user's current request \"" . $topic . "\" is a CONTINUATION of the above conversation. Extract topic/context from the chat history. If the conversation already discussed a specific topic, the user likely wants to create an outline for THAT topic. Do NOT ask \"what topic?\" if it's already clear from the conversation."; } $memory_context = $this->sidebar->get_post_memory_context($post_id); $followup_hint = ""; if ("refinement" === $mode && !empty($memory_context)) { $followup_hint = "\n\nThis is a follow-up request to an existing article. Use the post memory below to avoid asking generic questions already covered unless the request is ambiguous within that context."; } // Also treat chat history as follow-up context. if (!empty($chat_history_context)) { $followup_hint .= "\n\nThis request continues from a previous chat conversation. Use the conversation history to understand what the user wants."; } $system_prompt = "You are an expert editor who determines if an article request has sufficient context to write effectively. IMPORTANT RULES: 1. WE ARE WRITING A WORDPRESS BLOG POST/ARTICLE. NEVER ask what media format or content type the user wants (e.g., do not ask if it's for social media, pamphlets, emails, etc.). Assume it is always a web article/blog post. 2. DETECT LANGUAGE: Identify the user's language (Indonesian, English, etc.) and write ALL questions in that SAME language 3. PRIORITIZE TOPIC CONTEXT: Ask about the topic/scope/platform FIRST before writing-style questions 4. USE OPEN-TEXT for complex questions that need detailed explanations 5. USE MULTIPLE CHOICE only for simple binary/selection questions EVALUATION CATEGORIES (in priority order): **TOPIC-SPECIFIC CONTEXT (Most Important):** 1. topic_scope - What specific aspects should be covered? (e.g., for \"page builder\": platforms covered? WordPress only? Comparison? Features? Use cases?) 2. target_platform - Which platform/tool? (WordPress/Shopify/Webflow/Generic/Multiple/etc) 3. specific_focus - What angle? (Technical tutorial/Business benefits/Comparison/Getting started/Best practices) 4. missing_info - What key details are unclear? **WRITING CONTEXT (Secondary):** 5. target_outcome - What should this achieve? (Education/Marketing/Sales/Comparison/Tutorial/Opinion) 6. target_audience - Who reads this? (Beginners/Developers/Business owners/Marketers/General audience) 7. content_depth - How detailed? (Quick overview/Standard guide/Comprehensive/Technical deep-dive) QUESTION TYPES: 1. **single_choice** - For simple selections (one answer): Use for: platform, outcome, audience type, depth level Example: { 'id': 'q1', 'category': 'target_platform', 'question': 'Platform apa yang ingin dibahas? (What platform to focus on?)', 'type': 'single_choice', 'options': [ { 'value': 'WordPress only', 'default': true }, { 'value': 'Comparison: WordPress vs others', 'default': false }, { 'value': 'General page builders (multiple platforms)', 'default': false }, { 'value': 'Specific platform (mention below)', 'default': false } ] } 2. **multiple_choice** - For selecting multiple items: Use for: topics to cover, platforms to compare Example: { 'id': 'q2', 'category': 'topic_scope', 'question': 'Apa yang harus dibahas? (What to cover?)', 'type': 'multiple_choice', 'options': [ { 'value': 'Benefits/advantages', 'default': true }, { 'value': 'How to choose', 'default': true }, { 'value': 'Popular plugins/platforms', 'default': false }, { 'value': 'Use cases/examples', 'default': false }, { 'value': 'Technical details', 'default': false } ] } 3. **open_text** - For detailed explanations (RECOMMENDED for complex topics): Use for: scope clarification, specific requirements, custom details Example: { 'id': 'q3', 'category': 'topic_scope', 'question': 'Jelaskan lebih detail apa yang ingin Anda bahas tentang page builder. Apakah ada platform spesifik? Fokus ke mana? (Explain in detail what you want to cover about page builders. Any specific platform? What focus?)', 'type': 'open_text', 'placeholder': 'Contoh: Fokus ke WordPress plugin seperti Elementor, Divi, Brizy. Jelaskan kelebihan masing-masing...', 'max_length': 500 } CONFIDENCE CALCULATION: - Start at 100% (1.0) - 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 2. If topic is vague (e.g., \"page builder\" without platform), ask open_text about scope 3. If multiple possible platforms, ask single_choice to narrow down 4. If multiple topics could apply, ask multiple_choice 5. Only ask writing-style questions if topic context is already clear Return ONLY valid JSON: { 'is_clear': true/false, 'confidence': 0.0-1.0, 'detected_language': 'indonesian'|'english'|'other', 'missing_categories': ['topic_scope', 'target_platform'], 'questions': [ ... ] } No markdown, no explanation - just JSON."; $messages = [ [ "role" => "system", "content" => $system_prompt, ], [ "role" => "user", "content" => "Topic: {$topic}\n\nRequired Categories: " . implode(", ", $required_categories) . "\n\nEvaluate this request and determine which context is missing.{$chat_history_context}{$context}{$post_config_context}{$memory_context}{$followup_hint}{$language_hint}", ], ]; $response = $provider->chat( $messages, ["temperature" => 0.7], "planning", ); if (is_wp_error($response)) { // Track failed attempt for observability. $this->sidebar->track_ai_cost( $post_id, WPAW_Model_Registry::get_default_model("clarity"), "clarity_check", 0, 0, 0, $provider_result, "", "error", ); // 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"] ?? [], $post_config, ); if (!empty($result["questions"])) { $result["is_clear"] = false; } return new WP_REST_Response( [ "result" => $result, "cost" => 0, ], 200, ); } // Extract JSON from response. $content = $response["content"]; $result = $this->sidebar->extract_json($content); if (null === $result) { // Track parse failure for observability. $this->sidebar->track_ai_cost( $post_id, $response["model"] ?? "unknown", "clarity_check", $response["input_tokens"] ?? 0, $response["output_tokens"] ?? 0, $response["cost"] ?? 0, $provider_result, "", "error", ); // 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"] ?? [], $post_config, ); if (!empty($result["questions"])) { $result["is_clear"] = false; } return new WP_REST_Response( [ "result" => $result, "cost" => 0, ], 200, ); } // Track cost (always track for debugging). $post_id = $params["postId"] ?? 0; $this->sidebar->track_ai_cost( $post_id, $response["model"] ?? "", "clarity_check", $response["input_tokens"] ?? 0, $response["output_tokens"] ?? 0, $response["cost"] ?? 0, $provider_result, "", "success", ); // MANDATORY: Always add configuration questions if (!isset($result["questions"]) || !is_array($result["questions"])) { $result["questions"] = []; } $result["questions"] = $this->append_config_questions( $result["questions"], $post_config, ); // 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( [ "result" => $result, "cost" => $response["cost"] ?? 0, "provider_metadata" => $this->sidebar->build_provider_metadata( $provider_result, $response["model"] ?? "", ), ], 200, ); } /** * Check clarity before article generation. * * @since 0.1.0 * @param string $topic User topic. * @param array $answers Previous answers. * @param mixed $provider OpenRouter provider. * @return array Clarity check result with is_clear and questions. */ public function check_clarity_before_generation( $topic, $answers, $provider, ) { // Get settings. $settings = get_option("wp_agentic_writer_settings", []); $enabled = $settings["enable_clarification_quiz"] ?? true; $threshold = $settings["clarity_confidence_threshold"] ?? "0.6"; $required_categories = $settings["required_context_categories"] ?? [ "target_outcome", "target_audience", "tone", "content_depth", "expertise_level", ]; // If quiz is disabled, always return clear. if (!$enabled) { return ["is_clear" => true, "confidence" => 1.0, "questions" => []]; } // Build context from answers if available. $context = ""; if (!empty($answers)) { $context = "\n\nPrevious answers:\n"; foreach ($answers as $answer) { $context .= "- {$answer["question"]}: {$answer["answer"]}\n"; } } $system_prompt = "You are an expert editor who determines if an article request has sufficient context to write effectively. Evaluate the user's request and determine which context categories are clear: CATEGORIES TO EVALUATE: 1. target_outcome - What should this content achieve? (education/marketing/sales/entertainment/brand_awareness) 2. target_audience - Who is reading this? (demographics, role, knowledge level) 3. tone - How should we sound? (formal/casual/technical/friendly/professional/conversational) 4. content_depth - How comprehensive? (quick_overview/standard_guide/detailed_analysis/comprehensive) 5. expertise_level - Reader's knowledge? (beginner/intermediate/advanced/expert) 6. content_type - What format? (tutorial/how_to/opinion/comparison/listicle/case_study/news_analysis) 7. pov - Whose perspective? (first_person/third_person/expert_voice/neutral) For each MISSING category, generate a clarifying question using PREDEFINED OPTIONS. Use 'single_choice' or 'multiple_choice' types - NEVER 'open_text'. QUESTION STRUCTURE: { 'id': 'q1', 'category': 'target_outcome', 'question': 'What is the primary goal of this content?', 'type': 'single_choice', 'options': [ { 'value': 'Education - Teach something new', 'default': true }, { 'value': 'Marketing - Promote a product/service', 'default': false }, { 'value': 'Sales - Drive conversions/signups', 'default': false }, { 'value': 'Entertainment - Engage and entertain', 'default': false }, { 'value': 'Brand Awareness - Build authority/trust', 'default': false } ] } CONFIDENCE CALCULATION: - Start at 100% (1.0) - Subtract 15% for each missing required category - If confidence < {$threshold}, generate questions for ALL missing categories Return ONLY valid JSON with this structure: { 'is_clear': true/false, 'confidence': 0.0-1.0, 'missing_categories': ['category1', 'category2'], 'questions': [ ... ] } No markdown, no explanation - just JSON."; $messages = [ [ "role" => "system", "content" => $system_prompt, ], [ "role" => "user", "content" => "Topic: {$topic}\n\nRequired Categories: " . implode(", ", $required_categories) . "\n\nEvaluate this request and determine which context is missing.{$context}", ], ]; $response = $provider->chat( $messages, ["temperature" => 0.7], "planning", ); if (is_wp_error($response)) { // Log error and use default questions instead of skipping. error_log( "WP Agentic Writer: Clarity check API error - " . $response->get_error_message(), ); return $this->get_default_clarification_questions($topic); } // Extract JSON from response. $content = $response["content"]; $result = $this->sidebar->extract_json($content); if (null === $result) { // Log parse error and use default questions instead of skipping. error_log("WP Agentic Writer: Failed to parse clarity check JSON"); return $this->get_default_clarification_questions($topic); } return $result; } /** * Append configuration questions to clarity quiz. * * @since 0.1.0 * @param array $questions Existing questions. * @param array $post_config Post configuration. * @return array Updated questions with config prompts. */ public function append_config_questions($questions, $post_config) { $detected_language = $post_config["language"] ?? "auto"; $is_indonesian = "Indonesian" === $detected_language; // Get preferred languages from settings $settings = get_option("wp_agentic_writer_settings", []); $preferred_languages = array_merge( $settings["preferred_languages"] ?? [ "auto", "English", "Indonesian", ], $settings["custom_languages"] ?? [], ); // Build language options from site preferences $language_options = []; foreach ($preferred_languages as $lang) { $language_options[] = [ "value" => $lang, "default" => "auto" === $lang, ]; } // Language selection question (FIRST) $questions[] = [ "id" => "config_language", "category" => "config", "question" => $is_indonesian ? "🌍 Pilih Bahasa Artikel (Select Article Language)" : "🌍 Select Article Language", "type" => "single_choice", "options" => $language_options, ]; // Single consolidated config question with all fields $questions[] = [ "id" => "config_all", "category" => "config", "question" => $is_indonesian ? "⚙️ Konfigurasi Artikel (Article Configuration)" : "⚙️ Article Configuration", "type" => "config_form", "fields" => [ [ "id" => "web_search", "label" => $is_indonesian ? "🔍 Pencarian Web (Web Search)" : "🔍 Web Search", "description" => $is_indonesian ? 'Aktifkan untuk data terkini (~$0.02/pencarian)' : 'Enable for current data (~$0.02/search)', "type" => "toggle", "default" => false, ], [ "id" => "seo", "label" => $is_indonesian ? "📊 Optimasi SEO (SEO Optimization)" : "📊 SEO Optimization", "description" => $is_indonesian ? "Optimalkan artikel untuk mesin pencari" : "Optimize article for search engines", "type" => "toggle", "default" => true, ], [ "id" => "focus_keyword", "label" => $is_indonesian ? "🎯 Kata Kunci Fokus (Focus Keyword)" : "🎯 Focus Keyword", "placeholder" => $is_indonesian ? "Contoh: wordpress plugin" : "Example: wordpress plugin", "type" => "text", "max_length" => 100, "conditional" => "seo", "default" => $post_config["seo_focus_keyword"] ?? "", "description" => !empty($post_config["seo_focus_keyword"]) ? ($is_indonesian ? "💡 Disarankan AI - edit jika perlu" : "💡 AI-suggested - edit if needed") : "", ], [ "id" => "secondary_keywords", "label" => $is_indonesian ? "🔑 Kata Kunci Sekunder (Secondary Keywords)" : "🔑 Secondary Keywords", "placeholder" => $is_indonesian ? "Pisahkan dengan koma" : "Comma-separated", "type" => "text", "max_length" => 200, "conditional" => "seo", "default" => $post_config["seo_secondary_keywords"] ?? "", "description" => !empty( $post_config["seo_secondary_keywords"] ) ? ($is_indonesian ? "💡 Disarankan AI - edit jika perlu" : "💡 AI-suggested - edit if needed") : "", ], ], ]; return $questions; } /** * Get default clarification questions when AI fails. * * @since 0.1.0 * @param string $topic User's topic. * @return array Clarification result with default questions. */ public function get_default_clarification_questions($topic) { $settings = get_option("wp_agentic_writer_settings", []); $required_categories = $settings["required_context_categories"] ?? [ "target_outcome", "target_audience", "tone", "content_depth", "expertise_level", ]; $questions = []; $question_id = 1; $question_templates = [ "target_outcome" => [ "category" => "target_outcome", "question" => "What is the primary goal of this content?", "type" => "single_choice", "options" => [ [ "value" => "Education - Teach something new", "default" => true, ], [ "value" => "Marketing - Promote a product/service", "default" => false, ], [ "value" => "Sales - Drive conversions", "default" => false, ], [ "value" => "Entertainment - Engage readers", "default" => false, ], [ "value" => "Brand Awareness - Build authority", "default" => false, ], ], ], "target_audience" => [ "category" => "target_audience", "question" => "Who is the primary audience for this content?", "type" => "single_choice", "options" => [ [ "value" => "General public / Beginners", "default" => true, ], [ "value" => "Professionals in the field", "default" => false, ], ["value" => "Potential customers", "default" => false], ["value" => "Existing customers/users", "default" => false], ["value" => "Industry peers / Experts", "default" => false], ], ], "tone" => [ "category" => "tone", "question" => "What tone should this content have?", "type" => "single_choice", "options" => [ [ "value" => "Professional & Authoritative", "default" => true, ], [ "value" => "Friendly & Conversational", "default" => false, ], ["value" => "Technical & Detailed", "default" => false], ["value" => "Casual & Entertaining", "default" => false], ["value" => "Formal & Academic", "default" => false], ], ], "content_depth" => [ "category" => "content_depth", "question" => "How comprehensive should this content be?", "type" => "single_choice", "options" => [ [ "value" => "Quick overview (500-800 words)", "default" => false, ], [ "value" => "Standard guide (800-1500 words)", "default" => true, ], [ "value" => "Detailed analysis (1500-2500 words)", "default" => false, ], [ "value" => "Comprehensive deep-dive (2500+ words)", "default" => false, ], ], ], "expertise_level" => [ "category" => "expertise_level", "question" => 'What is the target audience\'s expertise level?', "type" => "single_choice", "options" => [ [ "value" => "Beginner - No prior knowledge", "default" => true, ], [ "value" => "Intermediate - Basic understanding", "default" => false, ], [ "value" => "Advanced - Deep technical knowledge", "default" => false, ], [ "value" => "Expert - Industry professional", "default" => false, ], ], ], "content_type" => [ "category" => "content_type", "question" => "What type of content works best for this topic?", "type" => "single_choice", "options" => [ ["value" => "Tutorial / How-to guide", "default" => true], ["value" => "Opinion / Commentary", "default" => false], ["value" => "Comparison / Review", "default" => false], ["value" => "Listicle / Tips", "default" => false], ["value" => "Case study", "default" => false], ["value" => "News analysis", "default" => false], ], ], "pov" => [ "category" => "pov", "question" => "From what perspective should this be written?", "type" => "single_choice", "options" => [ [ "value" => 'Third person (objective, "it", "they")', "default" => true, ], [ "value" => 'First person (personal, "I", "my")', "default" => false, ], [ "value" => "Expert voice (authoritative, experienced)", "default" => false, ], ["value" => "Neutral / Unbiased", "default" => false], ], ], ]; foreach ($required_categories as $category) { if (isset($question_templates[$category])) { $q = $question_templates[$category]; $q["id"] = "q" . $question_id++; $questions[] = $q; } } return [ "is_clear" => false, "confidence" => 0.0, "missing_categories" => $required_categories, "questions" => $questions, ]; } }