__( 'Invalid post or insufficient permissions.', 'ai-answer-toolkit' ) ) ); } $post = get_post( $post_id ); if ( ! $post ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - Post not found: ' . $post_id ); wp_send_json_error( array( 'message' => __( 'Post not found.', 'ai-answer-toolkit' ) ) ); } // Check if post has meaningful content before generating suggestions $content = trim( wp_strip_all_tags( $post->post_content ) ); $title = trim( $post->post_title ); if ( empty( $content ) && empty( $title ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - No content found for post: ' . $post_id ); wp_send_json_error( array( 'message' => __( 'Please add content to your post before generating AI suggestions.', 'ai-answer-toolkit' ) ) ); } // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - Starting generation for post: ' . $post_id ); // Get AI suggestions $suggestions = self::generate_summary_suggestions( $post ); if ( is_wp_error( $suggestions ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - Error: ' . $suggestions->get_error_message() ); wp_send_json_error( array( 'message' => $suggestions->get_error_message(), 'error_code' => $suggestions->get_error_code() ) ); } // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - Success! Generated ' . count( $suggestions ) . ' suggestions' ); wp_send_json_success( array( 'suggestions' => $suggestions ) ); } /** * AJAX handler to apply AI suggestion. */ public static function ajax_apply_suggestion() { check_ajax_referer( 'aitk_ai_suggestions', 'nonce' ); $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; $summary = isset( $_POST['summary'] ) ? wp_kses_post( wp_unslash( $_POST['summary'] ) ) : ''; if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) { wp_send_json_error( array( 'message' => __( 'Invalid post or insufficient permissions.', 'ai-answer-toolkit' ) ) ); } // Save the suggestion as manual summary update_post_meta( $post_id, '_aitk_summary', $summary ); // Fire action hook do_action( 'ai_answer_toolkit_ai_summary_applied', $post_id, $summary ); // Recalculate score $score_data = AI_Answer_Toolkit_Score::calculate_score( $post_id ); wp_send_json_success( array( 'message' => __( 'AI suggestion applied successfully!', 'ai-answer-toolkit' ), 'score_data' => $score_data, ) ); } /** * AJAX handler to get FAQ suggestions. */ public static function ajax_get_faq_suggestions() { check_ajax_referer( 'aitk_ai_suggestions', 'nonce' ); $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) { wp_send_json_error( array( 'message' => __( 'Invalid post or insufficient permissions.', 'ai-answer-toolkit' ) ) ); } $post = get_post( $post_id ); if ( ! $post ) { wp_send_json_error( array( 'message' => __( 'Post not found.', 'ai-answer-toolkit' ) ) ); } // Get FAQ suggestions $suggestions = self::generate_faq_suggestions( $post ); wp_send_json_success( array( 'suggestions' => $suggestions ) ); } /** * Generate AI summary suggestions. * * @param WP_Post $post Post object. * @return array|WP_Error Array of suggestions or error. */ public static function generate_summary_suggestions( $post ) { $api_key = get_option( 'ai_answer_toolkit_openai_api_key' ); if ( empty( $api_key ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - No API key configured' ); $settings_url = admin_url( 'admin.php?page=ai-answer-toolkit-settings' ); $message = sprintf( __( // translators: %s is the URL to the plugin settings page. 'OpenAI API key not configured. Please add it in plugin settings.', 'ai-answer-toolkit' ), esc_url( $settings_url ) ); return new WP_Error( 'no_api_key', $message ); } // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - API key found: ' . substr( $api_key, 0, 10 ) . '...' ); // Get post content $content = wp_strip_all_tags( $post->post_content ); $content = preg_replace( '/\s+/', ' ', $content ); $content = trim( substr( $content, 0, 3000 ) ); // Limit to 3000 chars // Build prompt $prompt = sprintf( "Create 3 different AEO-optimized summaries for the following article. Each summary should be 300-500 characters, comprehensive yet concise, and optimized for AI answer engines like ChatGPT, Perplexity, and Google SGE (AI Overview).\n\nTitle: %s\n\nContent: %s\n\nProvide 3 variations with different angles or focus areas. Format as JSON array with 'text' and 'focus' keys. Example: [{\"text\": \"Your summary here\", \"focus\": \"Overview\"}]", $post->post_title, $content ); // Build request $request_args = array( 'model' => 'gpt-3.5-turbo', 'messages' => array( array( 'role' => 'system', 'content' => 'You are an expert in Answer Engine Optimization (AEO) and creating content optimized for AI search engines.', ), array( 'role' => 'user', 'content' => $prompt, ), ), 'temperature' => 0.7, 'max_tokens' => 800, ); // Allow filtering $request_args = apply_filters( 'ai_answer_toolkit_ai_api_request', $request_args, $post->ID ); // Make API call // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - Making API call to OpenAI...' ); $response = wp_remote_post( self::API_ENDPOINT, array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( $request_args ), 'timeout' => 30, ) ); if ( is_wp_error( $response ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - WP_Error: ' . $response->get_error_message() ); return $response; } $response_code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - API Response Code: ' . $response_code ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - API Response Body: ' . substr( $body, 0, 500 ) ); // Check for API errors if ( $response_code !== 200 ) { $error_message = __( 'OpenAI API Error: ', 'ai-answer-toolkit' ); if ( ! empty( $data['error']['message'] ) ) { $error_message .= $data['error']['message']; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - OpenAI Error Message: ' . $data['error']['message'] ); if ( ! empty( $data['error']['type'] ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( 'AEO AI Suggestions - OpenAI Error Type: ' . $data['error']['type'] ); } } else { // translators: %d is the HTTP response code. $error_message .= sprintf( __( 'HTTP %d', 'ai-answer-toolkit' ), $response_code ); } // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r error_log( 'AEO AI Suggestions - Full API Error Response: ' . print_r( $data, true ) ); return new WP_Error( 'api_error', $error_message ); } if ( empty( $data['choices'][0]['message']['content'] ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r error_log( 'AEO AI Suggestions - Unexpected API response structure: ' . print_r( $data, true ) ); return new WP_Error( 'api_error', __( 'Failed to get AI suggestions. Unexpected API response format.', 'ai-answer-toolkit' ) ); } // Parse response $ai_response = $data['choices'][0]['message']['content']; $suggestions = self::parse_ai_suggestions( $ai_response ); // Allow filtering $suggestions = apply_filters( 'ai_answer_toolkit_ai_suggestions', $suggestions, $post->ID ); return $suggestions; } /** * Parse AI suggestions from response. * * @param string $response AI response. * @return array Suggestions. */ private static function parse_ai_suggestions( $response ) { // Clean up response - remove markdown code blocks if present $response = preg_replace( '/```json\s*/', '', $response ); $response = preg_replace( '/```\s*/', '', $response ); $response = trim( $response ); // Try to parse as JSON first $json = json_decode( $response, true ); if ( is_array( $json ) && ! empty( $json ) ) { // Ensure each item has 'text' and 'focus' keys $suggestions = array(); foreach ( array_slice( $json, 0, 3 ) as $item ) { if ( is_array( $item ) && isset( $item['text'] ) ) { $suggestions[] = array( 'text' => $item['text'], 'focus' => isset( $item['focus'] ) ? $item['focus'] : 'General', ); } elseif ( is_string( $item ) ) { // Handle case where array contains strings instead of objects $suggestions[] = array( 'text' => $item, 'focus' => 'General', ); } } if ( ! empty( $suggestions ) ) { return $suggestions; } } // Fallback: parse as text $suggestions = array(); $lines = explode( "\n", $response ); foreach ( $lines as $line ) { $line = trim( $line ); if ( empty( $line ) || strlen( $line ) < 100 ) { continue; } // Remove numbering and formatting $line = preg_replace( '/^[\d\.\)\-\*]+\s*/', '', $line ); $line = trim( $line, '"' ); if ( ! empty( $line ) ) { $suggestions[] = array( 'text' => $line, 'focus' => 'General', ); } if ( count( $suggestions ) >= 3 ) { break; } } return $suggestions; } /** * Generate FAQ suggestions from content. * * @param WP_Post $post Post object. * @return array FAQ suggestions. */ public static function generate_faq_suggestions( $post ) { $suggestions = array(); // Parse content for questions $content = $post->post_content; $blocks = parse_blocks( $content ); $suggestions = self::extract_potential_faqs( $blocks ); // If we have API key, enhance with AI $api_key = get_option( 'ai_answer_toolkit_openai_api_key' ); if ( ! empty( $api_key ) && count( $suggestions ) < 3 ) { $ai_faqs = self::generate_ai_faq_suggestions( $post ); if ( ! is_wp_error( $ai_faqs ) ) { $suggestions = array_merge( $suggestions, $ai_faqs ); } } return array_slice( $suggestions, 0, 5 ); } /** * Extract potential FAQs from blocks. * * @param array $blocks Gutenberg blocks. * @return array FAQ suggestions. */ private static function extract_potential_faqs( $blocks ) { $faqs = array(); for ( $i = 0; $i < count( $blocks ); $i++ ) { $block = $blocks[ $i ]; if ( empty( $block['blockName'] ) ) { continue; } // Look for headings with questions if ( $block['blockName'] === 'core/heading' ) { $question = wp_strip_all_tags( $block['innerHTML'] ); if ( ! empty( $question ) && substr( trim( $question ), -1 ) === '?' ) { // Look for answer in next paragraph if ( isset( $blocks[ $i + 1 ] ) && $blocks[ $i + 1 ]['blockName'] === 'core/paragraph' ) { $answer = wp_strip_all_tags( $blocks[ $i + 1 ]['innerHTML'] ); if ( ! empty( $answer ) ) { $faqs[] = array( 'question' => trim( $question ), 'answer' => trim( $answer ), 'source' => 'content', ); } } } } // Recursively check inner blocks if ( ! empty( $block['innerBlocks'] ) ) { $inner_faqs = self::extract_potential_faqs( $block['innerBlocks'] ); $faqs = array_merge( $faqs, $inner_faqs ); } } return $faqs; } /** * Generate AI-powered FAQ suggestions. * * @param WP_Post $post Post object. * @return array|WP_Error FAQ suggestions or error. */ private static function generate_ai_faq_suggestions( $post ) { $api_key = get_option( 'ai_answer_toolkit_openai_api_key' ); if ( empty( $api_key ) ) { return new WP_Error( 'no_api_key', __( 'OpenAI API key not configured.', 'ai-answer-toolkit' ) ); } // Get post content $content = wp_strip_all_tags( $post->post_content ); $content = preg_replace( '/\s+/', ' ', $content ); $content = trim( substr( $content, 0, 2000 ) ); // Build prompt $prompt = sprintf( "Based on the following article, suggest 3 common questions readers might ask and provide concise answers (50-100 words each). Format as JSON array with 'question' and 'answer' keys.\n\nTitle: %s\n\nContent: %s", $post->post_title, $content ); // Build request $request_args = array( 'model' => 'gpt-3.5-turbo', 'messages' => array( array( 'role' => 'system', 'content' => 'You are an expert at creating FAQ content for websites.', ), array( 'role' => 'user', 'content' => $prompt, ), ), 'temperature' => 0.7, 'max_tokens' => 400, ); // Make API call $response = wp_remote_post( self::API_ENDPOINT, array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( $request_args ), 'timeout' => 30, ) ); if ( is_wp_error( $response ) ) { return $response; } $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( empty( $data['choices'][0]['message']['content'] ) ) { return new WP_Error( 'api_error', __( 'Failed to get FAQ suggestions.', 'ai-answer-toolkit' ) ); } // Parse response $ai_response = $data['choices'][0]['message']['content']; $json = json_decode( $ai_response, true ); if ( is_array( $json ) ) { foreach ( $json as &$faq ) { $faq['source'] = 'ai'; } return $json; } return array(); } /** * Get score improvement hints. * * @param int $post_id Post ID. * @param array $score_data Score data. * @return array Hints. */ public static function get_improvement_hints( $post_id, $score_data ) { $tips = array(); $post = get_post( $post_id ); $score = isset( $score_data['score'] ) ? (int) $score_data['score'] : 0; $breakdown = isset( $score_data['breakdown'] ) && is_array( $score_data['breakdown'] ) ? $score_data['breakdown'] : array(); $severity_icon_map = array( 'critical' => 'dashicons-dismiss', 'warning' => 'dashicons-warning', 'info' => 'dashicons-info-outline', 'success' => 'dashicons-yes-alt', ); $add_tip = function( $tip ) use ( &$tips, $severity_icon_map ) { $defaults = array( 'id' => '', 'severity' => 'info', 'priority' => 50, 'score_impact' => 0, 'impact_text' => '', 'message' => '', 'ai_why' => '', 'category' => __( 'Overall AEO Quality', 'ai-answer-toolkit' ), 'action_target' => '', ); $tip = wp_parse_args( $tip, $defaults ); $tip['severity'] = in_array( $tip['severity'], array( 'critical', 'warning', 'info', 'success' ), true ) ? $tip['severity'] : 'info'; if ( empty( $tip['impact_text'] ) ) { // translators: %d is the number of points that can be gained. $tip['impact_text'] = sprintf( __( 'Fixing this can increase your score by +%d points', 'ai-answer-toolkit' ), (int) $tip['score_impact'] ); } // Backwards compatibility with existing UI. $tip['type'] = $tip['severity']; $tip['icon'] = isset( $severity_icon_map[ $tip['severity'] ] ) ? $severity_icon_map[ $tip['severity'] ] : 'dashicons-info-outline'; $tips[] = $tip; }; // Summary Optimization. $has_any_summary = AI_Answer_Toolkit_Summary::has_summary( $post_id ); $summary = AI_Answer_Toolkit_Summary::get_final_summary( $post_id ); $summary_length = strlen( $summary ); if ( ! $has_any_summary ) { $add_tip( array( 'id' => 'manual_summary_missing', 'severity' => 'critical', 'priority' => 1, 'score_impact' => 40, 'impact_text' => __( 'Fixing this can increase your score by +40 points', 'ai-answer-toolkit' ), 'message' => __( 'Add a manual summary to increase your score by 40 points.', 'ai-answer-toolkit' ), 'ai_why' => __( 'AI engines rely on concise summaries when generating direct answers and previews.', 'ai-answer-toolkit' ), 'category' => __( 'Summary Optimization', 'ai-answer-toolkit' ), 'action_target' => 'summary_editor', ) ); } if ( $summary_length > 0 && $summary_length < 300 ) { $needed = 300 - $summary_length; $add_tip( array( 'id' => 'summary_length_short', 'severity' => 'warning', 'priority' => 5, 'score_impact' => 20, 'impact_text' => __( 'Fixing this can increase your score by +0 to +20 points', 'ai-answer-toolkit' ), // translators: %d is the number of characters needed to reach optimal summary length. 'message' => sprintf( __( 'Expand your summary by %d characters to reach the optimal range (300-500).', 'ai-answer-toolkit' ), $needed ), 'ai_why' => __( 'Longer, information-dense summaries help AI systems extract context and reduce ambiguity.', 'ai-answer-toolkit' ), 'category' => __( 'Summary Optimization', 'ai-answer-toolkit' ), 'action_target' => 'summary_editor', ) ); } elseif ( $summary_length > 500 ) { $excess = $summary_length - 500; $add_tip( array( 'id' => 'summary_length_long', 'severity' => 'warning', 'priority' => 6, 'score_impact' => 20, 'impact_text' => __( 'Fixing this can increase your score by +0 to +20 points', 'ai-answer-toolkit' ), // translators: %d is the number of characters to remove for optimal summary length. 'message' => sprintf( __( 'Shorten your summary by %d characters for optimal length (300-500).', 'ai-answer-toolkit' ), $excess ), 'ai_why' => __( 'Overlong summaries can dilute key facts and reduce answer precision for AI engines.', 'ai-answer-toolkit' ), 'category' => __( 'Summary Optimization', 'ai-answer-toolkit' ), 'action_target' => 'summary_editor', ) ); } // FAQ & Q&A Structure. $faq_count = AI_Answer_Toolkit_Score::get_faq_count( $post_id ); if ( $faq_count === 0 ) { $add_tip( array( 'id' => 'faq_missing', 'severity' => 'warning', 'priority' => 10, 'score_impact' => 20, 'impact_text' => __( 'Fixing this can increase your score by +0 to +20 points', 'ai-answer-toolkit' ), 'message' => __( 'Add 1-3 FAQ items to improve your score by up to 20 points.', 'ai-answer-toolkit' ), 'ai_why' => __( 'Question/answer formatting helps AI engines extract direct answers and improves snippet eligibility.', 'ai-answer-toolkit' ), 'category' => __( 'FAQ & Q&A Structure', 'ai-answer-toolkit' ), 'action_target' => 'faq_section', ) ); } elseif ( $faq_count < 3 ) { $add_tip( array( 'id' => 'faq_incomplete', 'severity' => 'info', 'priority' => 20, 'score_impact' => 20, 'impact_text' => __( 'Fixing this can increase your score by +0 to +20 points', 'ai-answer-toolkit' ), // translators: %d is the number of FAQ items needed to reach the optimal count. 'message' => sprintf( __( 'Add %d more FAQ items to maximize your FAQ score.', 'ai-answer-toolkit' ), 3 - $faq_count ), 'ai_why' => __( 'More focused FAQs can increase coverage of user intents that AI engines match to queries.', 'ai-answer-toolkit' ), 'category' => __( 'FAQ & Q&A Structure', 'ai-answer-toolkit' ), 'action_target' => 'faq_section', ) ); } // Service Metadata. if ( $post && $post->post_type === 'service' ) { $area_served = get_post_meta( $post_id, '_aitk_service_area', true ); $service_type = get_post_meta( $post_id, '_aitk_service_type', true ); if ( empty( $area_served ) || empty( $service_type ) ) { $add_tip( array( 'id' => 'service_metadata_missing', 'severity' => 'warning', 'priority' => 15, 'score_impact' => 20, 'impact_text' => __( 'Fixing this can increase your score by +20 points', 'ai-answer-toolkit' ), 'message' => __( 'Complete service metadata (area served, service type) to gain 20 points.', 'ai-answer-toolkit' ), 'ai_why' => __( 'Service metadata helps AI systems understand what you offer and match your content to local or service-based queries.', 'ai-answer-toolkit' ), 'category' => __( 'Service Metadata', 'ai-answer-toolkit' ), 'action_target' => 'service_fields', ) ); } } // Content Structure (AI-focused) using existing analysis (no score impact yet). if ( class_exists( 'AI_Answer_Toolkit_Content_Guidance' ) ) { $guidance = AI_Answer_Toolkit_Content_Guidance::analyze_content( $post_id ); if ( isset( $guidance['ai_questions']['count'] ) && (int) $guidance['ai_questions']['count'] === 0 ) { $add_tip( array( 'id' => 'content_ai_questions_missing', 'severity' => 'info', 'priority' => 30, 'score_impact' => 0, 'impact_text' => __( 'Improves AI answer extraction (no direct score impact yet)', 'ai-answer-toolkit' ), 'message' => __( 'Add question-style headings (e.g., “What is…?”, “How does…?”) to help AI engines extract answers.', 'ai-answer-toolkit' ), 'ai_why' => __( 'AI engines frequently map questions to headings and pull the next paragraph as the answer.', 'ai-answer-toolkit' ), 'category' => __( 'Content Structure', 'ai-answer-toolkit' ), ) ); } } // Enhanced Positive Reinforcement. if ( $score >= 80 ) { $passed = array(); if ( $has_any_summary ) { $passed[] = __( 'Summary present', 'ai-answer-toolkit' ); } if ( $summary_length >= 300 && $summary_length <= 500 ) { $passed[] = __( 'Summary length optimal', 'ai-answer-toolkit' ); } if ( $faq_count >= 1 ) { $passed[] = __( 'FAQ coverage started', 'ai-answer-toolkit' ); } if ( $post && $post->post_type === 'service' ) { $area_served = get_post_meta( $post_id, '_aitk_service_area', true ); $service_type = get_post_meta( $post_id, '_aitk_service_type', true ); if ( ! empty( $area_served ) && ! empty( $service_type ) ) { $passed[] = __( 'Service metadata complete', 'ai-answer-toolkit' ); } } $add_tip( array( 'id' => 'quality_good', 'severity' => 'success', 'priority' => 999, 'score_impact' => 0, 'impact_text' => __( 'Great work — maintain this quality', 'ai-answer-toolkit' ), 'message' => __( 'This content is well-structured for AI answer engines.', 'ai-answer-toolkit' ), 'ai_why' => implode( ' · ', $passed ), 'category' => __( 'Overall AEO Quality', 'ai-answer-toolkit' ), ) ); } // Remove duplicates by id. $deduped = array(); foreach ( $tips as $tip ) { if ( empty( $tip['id'] ) ) { continue; } $deduped[ $tip['id'] ] = $tip; } $tips = array_values( $deduped ); // Sort by priority (asc). usort( $tips, function( $a, $b ) { $ap = isset( $a['priority'] ) ? (int) $a['priority'] : 9999; $bp = isset( $b['priority'] ) ? (int) $b['priority'] : 9999; return $ap <=> $bp; } ); $tips = apply_filters( 'ai_answer_toolkit_improvement_tips', $tips, $post_id, $score_data ); return $tips; } } AI_Answer_Toolkit_AI_Suggestions::init();