From adc55c7c45d4e7de6c344dc85ad162b9b5c85b72 Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Thu, 9 Apr 2026 10:46:00 +0700 Subject: [PATCH] fix: convert space indentation to tabs for WPCS compliance All 499 PHPCS errors were 'Tabs must be used to indent lines; spaces are not allowed'. Converted 4-space indentation to tabs throughout class-psa-ai-generator.php. --- includes/class-psa-ai-generator.php | 978 ++++++++++++++-------------- 1 file changed, 489 insertions(+), 489 deletions(-) diff --git a/includes/class-psa-ai-generator.php b/includes/class-psa-ai-generator.php index 39e4ac0..83c24a7 100644 --- a/includes/class-psa-ai-generator.php +++ b/includes/class-psa-ai-generator.php @@ -8,144 +8,144 @@ */ if ( ! defined( 'ABSPATH' ) ) { - exit; + exit; } class PSA_AI_Generator { - /** - * OpenRouter API base URL. - */ - const API_URL = 'https://openrouter.ai/api/v1/chat/completions'; + /** + * OpenRouter API base URL. + */ + const API_URL = 'https://openrouter.ai/api/v1/chat/completions'; - public static function init() { - add_action( 'psa_generate_peptide_background', array( __CLASS__, 'background_generate' ), 10, 2 ); - } + public static function init() { + add_action( 'psa_generate_peptide_background', array( __CLASS__, 'background_generate' ), 10, 2 ); + } - /** - * Validate whether a search term is a legitimate peptide name. - * Uses a lightweight AI call to check before committing to full generation. - * - * @param string $name The name to validate. - * @return array|WP_Error { is_valid: bool, canonical_name: string, reason: string } - */ - public static function validate_peptide_name( $name ) { - $options = self::get_settings(); - $api_key = $options['api_key']; - $model = $options['validation_model']; + /** + * Validate whether a search term is a legitimate peptide name. + * Uses a lightweight AI call to check before committing to full generation. + * + * @param string $name The name to validate. + * @return array|WP_Error { is_valid: bool, canonical_name: string, reason: string } + */ + public static function validate_peptide_name( $name ) { + $options = self::get_settings(); + $api_key = $options['api_key']; + $model = $options['validation_model']; - if ( empty( $api_key ) ) { - return new WP_Error( 'no_api_key', 'OpenRouter API key is not configured.' ); - } + if ( empty( $api_key ) ) { + return new WP_Error( 'no_api_key', 'OpenRouter API key is not configured.' ); + } - // Check transient cache first (avoid repeated API calls for same term). - $cache_key = 'psa_validate_' . md5( strtolower( trim( $name ) ) ); - $cached = get_transient( $cache_key ); - if ( false !== $cached ) { - return $cached; - } + // Check transient cache first (avoid repeated API calls for same term). + $cache_key = 'psa_validate_' . md5( strtolower( trim( $name ) ) ); + $cached = get_transient( $cache_key ); + if ( false !== $cached ) { + return $cached; + } - $prompt = self::build_validation_prompt( $name ); - // Allow filtering of validation prompt before sending. - $prompt = apply_filters( 'psa_validation_prompt', $prompt, $name ); - $response = self::call_openrouter( + $prompt = self::build_validation_prompt( $name ); + // Allow filtering of validation prompt before sending. + $prompt = apply_filters( 'psa_validation_prompt', $prompt, $name ); + $response = self::call_openrouter( $api_key, $model ?: 'google/gemini-2.0-flash-001', $prompt, PSA_Config::VALIDATION_MAX_TOKENS ); - if ( is_wp_error( $response ) ) { - return $response; - } + if ( is_wp_error( $response ) ) { + return $response; + } - $data = self::parse_ai_response( $response ); - if ( is_wp_error( $data ) ) { - return array( - 'is_valid' => false, - 'canonical_name' => $name, - 'reason' => 'Could not verify this peptide name.', - ); - } + $data = self::parse_ai_response( $response ); + if ( is_wp_error( $data ) ) { + return array( + 'is_valid' => false, + 'canonical_name' => $name, + 'reason' => 'Could not verify this peptide name.', + ); + } - $result = array( - 'is_valid' => ! empty( $data['is_valid'] ) && true === $data['is_valid'], - 'canonical_name' => sanitize_text_field( $data['canonical_name'] ?? $name ), - 'reason' => sanitize_text_field( $data['reason'] ?? '' ), - ); + $result = array( + 'is_valid' => ! empty( $data['is_valid'] ) && true === $data['is_valid'], + 'canonical_name' => sanitize_text_field( $data['canonical_name'] ?? $name ), + 'reason' => sanitize_text_field( $data['reason'] ?? '' ), + ); - // Cache for 24 hours (using PSA_Config for consistency). - set_transient( $cache_key, $result, DAY_IN_SECONDS ); // Validation cache TTL + // Cache for 24 hours (using PSA_Config for consistency). + set_transient( $cache_key, $result, DAY_IN_SECONDS ); // Validation cache TTL - return $result; - } + return $result; + } - /** - * Background generation handler (called via WP-Cron or synchronously). - * - * @param int $post_id The placeholder post ID. - * @param string $peptide_name The peptide to research. - */ - public static function background_generate( $post_id, $peptide_name ) { - $post = get_post( $post_id ); - if ( ! $post || 'draft' !== $post->post_status || 'peptide' !== $post->post_type ) { - error_log( 'PSA: background_generate skipped — post ' . $post_id . ' not a draft peptide.' ); - return; - } + /** + * Background generation handler (called via WP-Cron or synchronously). + * + * @param int $post_id The placeholder post ID. + * @param string $peptide_name The peptide to research. + */ + public static function background_generate( $post_id, $peptide_name ) { + $post = get_post( $post_id ); + if ( ! $post || 'draft' !== $post->post_status || 'peptide' !== $post->post_type ) { + error_log( 'PSA: background_generate skipped — post ' . $post_id . ' not a draft peptide.' ); + return; + } - // Only process posts that are still in 'pending' state. - $source = get_post_meta( $post_id, 'psa_source', true ); - if ( 'pending' !== $source ) { - error_log( 'PSA: background_generate skipped — post ' . $post_id . ' source is "' . $source . '", not "pending".' ); - return; - } + // Only process posts that are still in 'pending' state. + $source = get_post_meta( $post_id, 'psa_source', true ); + if ( 'pending' !== $source ) { + error_log( 'PSA: background_generate skipped — post ' . $post_id . ' source is "' . $source . '", not "pending".' ); + return; + } - $options = self::get_settings(); - $auto_publish = $options['auto_publish']; + $options = self::get_settings(); + $auto_publish = $options['auto_publish']; - error_log( 'PSA: Generating content for "' . $peptide_name . '" (post ' . $post_id . ') using model: ' . ( $options['ai_model'] ?: 'google/gemini-2.5-flash' ) ); + error_log( 'PSA: Generating content for "' . $peptide_name . '" (post ' . $post_id . ') using model: ' . ( $options['ai_model'] ?: 'google/gemini-2.5-flash' ) ); - // Allow developers to hook before generation starts. - do_action( 'psa_before_generation', $post_id, $peptide_name ); + // Allow developers to hook before generation starts. + do_action( 'psa_before_generation', $post_id, $peptide_name ); - // Generate the full content. - $result = self::generate_peptide_content( $peptide_name ); - if ( is_wp_error( $result ) ) { - $err_msg = $result->get_error_message(); - error_log( 'PSA: Generation FAILED for "' . $peptide_name . '": ' . $err_msg ); - update_post_meta( $post_id, 'psa_generation_error', $err_msg ); - return; - } + // Generate the full content. + $result = self::generate_peptide_content( $peptide_name ); + if ( is_wp_error( $result ) ) { + $err_msg = $result->get_error_message(); + error_log( 'PSA: Generation FAILED for "' . $peptide_name . '": ' . $err_msg ); + update_post_meta( $post_id, 'psa_generation_error', $err_msg ); + return; + } - error_log( 'PSA: AI content generated successfully for "' . $peptide_name . '". Fields: ' . implode( ', ', array_keys( $result ) ) ); + error_log( 'PSA: AI content generated successfully for "' . $peptide_name . '". Fields: ' . implode( ', ', array_keys( $result ) ) ); - // FIX: Use 'overview' field (which the prompt returns) with fallback to 'description'. - $post_content = $result['overview'] ?? $result['description'] ?? ''; + // FIX: Use 'overview' field (which the prompt returns) with fallback to 'description'. + $post_content = $result['overview'] ?? $result['description'] ?? ''; - // Safeguard: if post_content is still empty after generation, log a warning. - if ( empty( trim( $post_content ) ) ) { - error_log( 'PSA: WARNING — AI returned valid JSON but post content (overview/description) is empty for "' . $peptide_name . '". Available keys: ' . implode( ', ', array_keys( $result ) ) ); - update_post_meta( $post_id, 'psa_generation_error', 'AI returned empty overview/description content. Available fields: ' . implode( ', ', array_keys( $result ) ) ); - return; - } + // Safeguard: if post_content is still empty after generation, log a warning. + if ( empty( trim( $post_content ) ) ) { + error_log( 'PSA: WARNING — AI returned valid JSON but post content (overview/description) is empty for "' . $peptide_name . '". Available keys: ' . implode( ', ', array_keys( $result ) ) ); + update_post_meta( $post_id, 'psa_generation_error', 'AI returned empty overview/description content. Available fields: ' . implode( ', ', array_keys( $result ) ) ); + return; + } - // Try PubChem enrichment. - $pubchem_data = null; - if ( ! empty( $options['use_pubchem'] ) ) { - $pubchem_result = PSA_PubChem::lookup( $peptide_name ); - if ( is_wp_error( $pubchem_result ) ) { - // Log PubChem API failures. - error_log( 'PSA: PubChem lookup failed for "' . $peptide_name . '": ' . $pubchem_result->get_error_message() ); - } elseif ( is_array( $pubchem_result ) ) { - $pubchem_data = $pubchem_result; - error_log( 'PSA: PubChem data found for "' . $peptide_name . '" (CID: ' . ( $pubchem_data['cid'] ?? 'unknown' ) . ')' ); - } - } + // Try PubChem enrichment. + $pubchem_data = null; + if ( ! empty( $options['use_pubchem'] ) ) { + $pubchem_result = PSA_PubChem::lookup( $peptide_name ); + if ( is_wp_error( $pubchem_result ) ) { + // Log PubChem API failures. + error_log( 'PSA: PubChem lookup failed for "' . $peptide_name . '": ' . $pubchem_result->get_error_message() ); + } elseif ( is_array( $pubchem_result ) ) { + $pubchem_data = $pubchem_result; + error_log( 'PSA: PubChem data found for "' . $peptide_name . '" (CID: ' . ( $pubchem_data['cid'] ?? 'unknown' ) . ')' ); + } + } - // Update the placeholder with real content. - $post_status = ( 'publish' === $auto_publish ) ? 'publish' : 'draft'; + // Update the placeholder with real content. + $post_status = ( 'publish' === $auto_publish ) ? 'publish' : 'draft'; - $update_result = wp_update_post( + $update_result = wp_update_post( array( 'ID' => $post_id, 'post_title' => sanitize_text_field( $result['name'] ?? $peptide_name ), @@ -155,144 +155,144 @@ class PSA_AI_Generator { true ); - if ( is_wp_error( $update_result ) ) { - error_log( 'PSA: wp_update_post FAILED for post ' . $post_id . ': ' . $update_result->get_error_message() ); - update_post_meta( $post_id, 'psa_generation_error', 'Failed to update post: ' . $update_result->get_error_message() ); - return; - } + if ( is_wp_error( $update_result ) ) { + error_log( 'PSA: wp_update_post FAILED for post ' . $post_id . ': ' . $update_result->get_error_message() ); + update_post_meta( $post_id, 'psa_generation_error', 'Failed to update post: ' . $update_result->get_error_message() ); + return; + } - // Save all meta fields — prefer PubChem data for molecular properties. - self::save_peptide_meta( $post_id, $result, $pubchem_data ); + // Save all meta fields — prefer PubChem data for molecular properties. + self::save_peptide_meta( $post_id, $result, $pubchem_data ); - // Create a matching Knowledge Base article. - self::create_kb_article( $post_id, $result, $peptide_name ); + // Create a matching Knowledge Base article. + self::create_kb_article( $post_id, $result, $peptide_name ); - // Clean up generation tracking. - delete_post_meta( $post_id, 'psa_generation_started' ); - delete_post_meta( $post_id, 'psa_generation_error' ); - update_post_meta( $post_id, 'psa_generation_completed', time() ); + // Clean up generation tracking. + delete_post_meta( $post_id, 'psa_generation_started' ); + delete_post_meta( $post_id, 'psa_generation_error' ); + update_post_meta( $post_id, 'psa_generation_completed', time() ); - // Allow developers to hook after generation completes. - do_action( 'psa_after_generation', $post_id, $result, $peptide_name ); + // Allow developers to hook after generation completes. + do_action( 'psa_after_generation', $post_id, $result, $peptide_name ); - error_log( 'PSA: Successfully completed generation for "' . $peptide_name . '" (post ' . $post_id . ', status: ' . $post_status . ')' ); - } + error_log( 'PSA: Successfully completed generation for "' . $peptide_name . '" (post ' . $post_id . ', status: ' . $post_status . ')' ); + } - /** - * Generate full peptide content via AI. - * - * @param string $peptide_name The peptide to research. - * @return array|WP_Error Parsed peptide data or error. - */ - public static function generate_peptide_content( $peptide_name ) { - $options = self::get_settings(); - $api_key = $options['api_key']; - $model = $options['ai_model']; + /** + * Generate full peptide content via AI. + * + * @param string $peptide_name The peptide to research. + * @return array|WP_Error Parsed peptide data or error. + */ + public static function generate_peptide_content( $peptide_name ) { + $options = self::get_settings(); + $api_key = $options['api_key']; + $model = $options['ai_model']; - if ( empty( $api_key ) ) { - return new WP_Error( 'no_api_key', 'OpenRouter API key is not configured.' ); - } + if ( empty( $api_key ) ) { + return new WP_Error( 'no_api_key', 'OpenRouter API key is not configured.' ); + } - $prompt = self::build_generation_prompt( $peptide_name ); - // Allow filtering of generation prompt before sending. - $prompt = apply_filters( 'psa_generation_prompt', $prompt, $peptide_name ); - $response = self::call_openrouter( + $prompt = self::build_generation_prompt( $peptide_name ); + // Allow filtering of generation prompt before sending. + $prompt = apply_filters( 'psa_generation_prompt', $prompt, $peptide_name ); + $response = self::call_openrouter( $api_key, $model ?: 'google/gemini-2.5-flash', $prompt, PSA_Config::GENERATION_MAX_TOKENS ); - if ( is_wp_error( $response ) ) { - return $response; - } + if ( is_wp_error( $response ) ) { + return $response; + } - return self::parse_ai_response( $response ); - } + return self::parse_ai_response( $response ); + } - // ------------------------------------------------------------------------- - // Private helpers - // ------------------------------------------------------------------------- + // ------------------------------------------------------------------------- + // Private helpers + // ------------------------------------------------------------------------- - /** - * Get plugin settings with defaults. - * - * @return array - */ - private static function get_settings() { - $defaults = array( - 'api_key' => '', - 'ai_model' => '', - 'validation_model' => '', - 'auto_publish' => 'draft', - 'use_pubchem' => '1', - ); + /** + * Get plugin settings with defaults. + * + * @return array + */ + private static function get_settings() { + $defaults = array( + 'api_key' => '', + 'ai_model' => '', + 'validation_model' => '', + 'auto_publish' => 'draft', + 'use_pubchem' => '1', + ); - $settings = wp_parse_args( get_option( 'psa_settings', array() ), $defaults ); + $settings = wp_parse_args( get_option( 'psa_settings', array() ), $defaults ); - // Prefer wp-config.php constant over database value for security. - if ( defined( 'PSA_OPENROUTER_KEY' ) && PSA_OPENROUTER_KEY ) { - $settings['api_key'] = PSA_OPENROUTER_KEY; - } + // Prefer wp-config.php constant over database value for security. + if ( defined( 'PSA_OPENROUTER_KEY' ) && PSA_OPENROUTER_KEY ) { + $settings['api_key'] = PSA_OPENROUTER_KEY; + } - // Allow filtering of settings before use. - $settings = apply_filters( 'psa_ai_settings', $settings ); + // Allow filtering of settings before use. + $settings = apply_filters( 'psa_ai_settings', $settings ); - return $settings; - } + return $settings; + } - /** - * Save meta fields for a peptide post. - * - * @param int $post_id The post ID. - * @param array $ai_data AI-generated data. - * @param array|null $pubchem_data PubChem data (or null). - */ - private static function save_peptide_meta( $post_id, $ai_data, $pubchem_data ) { - $meta = array( - 'psa_sequence' => $ai_data['sequence'] ?? '', - 'psa_molecular_weight' => ! empty( $pubchem_data['molecular_weight'] ) - ? $pubchem_data['molecular_weight'] - : ( $ai_data['molecular_weight'] ?? '' ), - 'psa_molecular_formula' => ! empty( $pubchem_data['molecular_formula'] ) - ? $pubchem_data['molecular_formula'] - : ( $ai_data['molecular_formula'] ?? '' ), - 'psa_aliases' => $ai_data['aliases'] ?? '', - 'psa_mechanism' => $ai_data['mechanism'] ?? '', - 'psa_overview' => $ai_data['overview'] ?? $ai_data['description'] ?? '', - 'psa_category_label' => $ai_data['category_label'] ?? '', - 'psa_origin' => $ai_data['origin'] ?? '', - 'psa_research_apps' => $ai_data['research_benefits'] ?? ( $ai_data['research_applications'] ?? '' ), - 'psa_safety_profile' => $ai_data['safety_side_effects'] ?? ( $ai_data['safety_profile'] ?? '' ), - 'psa_dosage_info' => $ai_data['administration_dosing'] ?? ( $ai_data['dosage_info'] ?? '' ), - 'psa_legal_regulatory' => $ai_data['legal_regulatory'] ?? '', - 'psa_references' => $ai_data['references'] ?? '', - 'psa_category' => $ai_data['category'] ?? '', - 'psa_source' => ! empty( $pubchem_data ) ? 'pubchem' : 'ai-generated', - 'psa_pubchem_cid' => $pubchem_data['cid'] ?? '', - ); + /** + * Save meta fields for a peptide post. + * + * @param int $post_id The post ID. + * @param array $ai_data AI-generated data. + * @param array|null $pubchem_data PubChem data (or null). + */ + private static function save_peptide_meta( $post_id, $ai_data, $pubchem_data ) { + $meta = array( + 'psa_sequence' => $ai_data['sequence'] ?? '', + 'psa_molecular_weight' => ! empty( $pubchem_data['molecular_weight'] ) + ? $pubchem_data['molecular_weight'] + : ( $ai_data['molecular_weight'] ?? '' ), + 'psa_molecular_formula' => ! empty( $pubchem_data['molecular_formula'] ) + ? $pubchem_data['molecular_formula'] + : ( $ai_data['molecular_formula'] ?? '' ), + 'psa_aliases' => $ai_data['aliases'] ?? '', + 'psa_mechanism' => $ai_data['mechanism'] ?? '', + 'psa_overview' => $ai_data['overview'] ?? $ai_data['description'] ?? '', + 'psa_category_label' => $ai_data['category_label'] ?? '', + 'psa_origin' => $ai_data['origin'] ?? '', + 'psa_research_apps' => $ai_data['research_benefits'] ?? ( $ai_data['research_applications'] ?? '' ), + 'psa_safety_profile' => $ai_data['safety_side_effects'] ?? ( $ai_data['safety_profile'] ?? '' ), + 'psa_dosage_info' => $ai_data['administration_dosing'] ?? ( $ai_data['dosage_info'] ?? '' ), + 'psa_legal_regulatory' => $ai_data['legal_regulatory'] ?? '', + 'psa_references' => $ai_data['references'] ?? '', + 'psa_category' => $ai_data['category'] ?? '', + 'psa_source' => ! empty( $pubchem_data ) ? 'pubchem' : 'ai-generated', + 'psa_pubchem_cid' => $pubchem_data['cid'] ?? '', + ); - // Allow filtering of meta before save. - $meta = apply_filters( 'psa_peptide_meta', $meta, $post_id, $ai_data, $pubchem_data ); + // Allow filtering of meta before save. + $meta = apply_filters( 'psa_peptide_meta', $meta, $post_id, $ai_data, $pubchem_data ); - foreach ( $meta as $key => $value ) { - update_post_meta( $post_id, $key, $value ); - } - } + foreach ( $meta as $key => $value ) { + update_post_meta( $post_id, $key, $value ); + } + } - /** - * Build the lightweight validation prompt. - * - * Uses wp_json_encode() to safely encode the peptide name within the prompt. - * This ensures special characters and quotes are properly escaped before sending to the AI. - * The JSON-encoded format also helps the AI parser recognize the input clearly. - * - * @param string $name The peptide name to validate. - * @return string Prompt text ready to send to OpenRouter API. - */ - private static function build_validation_prompt( $name ) { - $json_name = wp_json_encode( $name ); - return <<get_error_code() ) { - return $result; - } + // If not a rate limit error, return immediately. + if ( ! is_wp_error( $result ) || 'rate_limited' !== $result->get_error_code() ) { + return $result; + } - // Rate limited — retry with exponential backoff. - if ( $attempt < $max_retries ) { - $delay = $base_delay * pow( 2, $attempt - 1 ); // 5s, 10s, 20s - error_log( 'PSA: Rate limited on attempt ' . $attempt . '/' . $max_retries . '. Retrying in ' . $delay . 's...' ); - sleep( $delay ); - } - } + // Rate limited — retry with exponential backoff. + if ( $attempt < $max_retries ) { + $delay = $base_delay * pow( 2, $attempt - 1 ); // 5s, 10s, 20s + error_log( 'PSA: Rate limited on attempt ' . $attempt . '/' . $max_retries . '. Retrying in ' . $delay . 's...' ); + sleep( $delay ); + } + } - error_log( 'PSA: All ' . $max_retries . ' attempts failed due to rate limiting.' ); - return new WP_Error( 'rate_limited', 'OpenRouter rate limit reached after ' . $max_retries . ' retries. Consider adding credits at openrouter.ai or switching to a non-free model.' ); - } + error_log( 'PSA: All ' . $max_retries . ' attempts failed due to rate limiting.' ); + return new WP_Error( 'rate_limited', 'OpenRouter rate limit reached after ' . $max_retries . ' retries. Consider adding credits at openrouter.ai or switching to a non-free model.' ); + } - /** - * Single OpenRouter API call (no retry). - * - * @param string $api_key The OpenRouter API key. - * @param string $model The model identifier. - * @param string $prompt The prompt text. - * @param int $max_tokens Maximum tokens to generate. - * @return string|WP_Error Response text or error. - */ - private static function call_openrouter_once( $api_key, $model, $prompt, $max_tokens = 4096 ) { - $site_url = get_bloginfo( 'url' ); - $site_name = get_bloginfo( 'name' ); + /** + * Single OpenRouter API call (no retry). + * + * @param string $api_key The OpenRouter API key. + * @param string $model The model identifier. + * @param string $prompt The prompt text. + * @param int $max_tokens Maximum tokens to generate. + * @return string|WP_Error Response text or error. + */ + private static function call_openrouter_once( $api_key, $model, $prompt, $max_tokens = 4096 ) { + $site_url = get_bloginfo( 'url' ); + $site_name = get_bloginfo( 'name' ); - $response = wp_remote_post( + $response = wp_remote_post( self::API_URL, array( 'timeout' => 120, @@ -442,82 +442,82 @@ PROMPT; ) ); - if ( is_wp_error( $response ) ) { - error_log( 'PSA: OpenRouter connection error: ' . $response->get_error_message() ); - return new WP_Error( 'api_error', 'Failed to connect to OpenRouter API: ' . $response->get_error_message() ); - } + if ( is_wp_error( $response ) ) { + error_log( 'PSA: OpenRouter connection error: ' . $response->get_error_message() ); + return new WP_Error( 'api_error', 'Failed to connect to OpenRouter API: ' . $response->get_error_message() ); + } - $code = wp_remote_retrieve_response_code( $response ); - $raw_body = wp_remote_retrieve_body( $response ); - $body = json_decode( $raw_body, true ); + $code = wp_remote_retrieve_response_code( $response ); + $raw_body = wp_remote_retrieve_body( $response ); + $body = json_decode( $raw_body, true ); - error_log( 'PSA: OpenRouter response code: ' . $code . ' for model: ' . $model ); + error_log( 'PSA: OpenRouter response code: ' . $code . ' for model: ' . $model ); - if ( 200 !== $code ) { - if ( 429 === $code ) { - error_log( 'PSA: OpenRouter rate limited (429)' ); - return new WP_Error( 'rate_limited', 'OpenRouter rate limit reached. Please try again in a few moments.' ); - } - if ( 402 === $code ) { - error_log( 'PSA: OpenRouter insufficient credits (402)' ); - return new WP_Error( 'insufficient_credits', 'OpenRouter account has insufficient credits. Please add funds at openrouter.ai.' ); - } - $msg = $body['error']['message'] ?? ( 'Unknown error (HTTP ' . $code . ')' ); - error_log( 'PSA: OpenRouter API error (' . $code . '): ' . $msg ); - error_log( 'PSA: OpenRouter raw response: ' . substr( $raw_body, 0, 500 ) ); - return new WP_Error( 'api_error', 'OpenRouter API error: ' . $msg ); - } + if ( 200 !== $code ) { + if ( 429 === $code ) { + error_log( 'PSA: OpenRouter rate limited (429)' ); + return new WP_Error( 'rate_limited', 'OpenRouter rate limit reached. Please try again in a few moments.' ); + } + if ( 402 === $code ) { + error_log( 'PSA: OpenRouter insufficient credits (402)' ); + return new WP_Error( 'insufficient_credits', 'OpenRouter account has insufficient credits. Please add funds at openrouter.ai.' ); + } + $msg = $body['error']['message'] ?? ( 'Unknown error (HTTP ' . $code . ')' ); + error_log( 'PSA: OpenRouter API error (' . $code . '): ' . $msg ); + error_log( 'PSA: OpenRouter raw response: ' . substr( $raw_body, 0, 500 ) ); + return new WP_Error( 'api_error', 'OpenRouter API error: ' . $msg ); + } - if ( empty( $body['choices'][0]['message']['content'] ) ) { - error_log( 'PSA: OpenRouter returned empty content. Raw: ' . substr( $raw_body, 0, 500 ) ); - return new WP_Error( 'api_error', 'OpenRouter API returned an empty response.' ); - } + if ( empty( $body['choices'][0]['message']['content'] ) ) { + error_log( 'PSA: OpenRouter returned empty content. Raw: ' . substr( $raw_body, 0, 500 ) ); + return new WP_Error( 'api_error', 'OpenRouter API returned an empty response.' ); + } - return $body['choices'][0]['message']['content']; - } + return $body['choices'][0]['message']['content']; + } - /** - * Parse AI response text into structured data. - * - * @param string $response_text Raw response text. - * @return array|WP_Error Parsed data or error. - */ - private static function parse_ai_response( $response_text ) { - // Strip any ... tags (some models like DeepSeek include reasoning). - $cleaned = preg_replace( '/.*?<\/think>/s', '', $response_text ); + /** + * Parse AI response text into structured data. + * + * @param string $response_text Raw response text. + * @return array|WP_Error Parsed data or error. + */ + private static function parse_ai_response( $response_text ) { + // Strip any ... tags (some models like DeepSeek include reasoning). + $cleaned = preg_replace( '/.*?<\/think>/s', '', $response_text ); - // Strip markdown code fences if present. - $cleaned = preg_replace( '/^```(?:json)?\s*/m', '', $cleaned ); - $cleaned = preg_replace( '/\s*```\s*$/m', '', $cleaned ); - $cleaned = trim( $cleaned ); + // Strip markdown code fences if present. + $cleaned = preg_replace( '/^```(?:json)?\s*/m', '', $cleaned ); + $cleaned = preg_replace( '/\s*```\s*$/m', '', $cleaned ); + $cleaned = trim( $cleaned ); - $data = json_decode( $cleaned, true ); - if ( JSON_ERROR_NONE !== json_last_error() ) { - error_log( 'PSA: JSON parse error: ' . json_last_error_msg() . '. Raw (first 500 chars): ' . substr( $response_text, 0, 500 ) ); - return new WP_Error( - 'parse_error', - 'Failed to parse AI response as JSON: ' . json_last_error_msg() - ); - } + $data = json_decode( $cleaned, true ); + if ( JSON_ERROR_NONE !== json_last_error() ) { + error_log( 'PSA: JSON parse error: ' . json_last_error_msg() . '. Raw (first 500 chars): ' . substr( $response_text, 0, 500 ) ); + return new WP_Error( + 'parse_error', + 'Failed to parse AI response as JSON: ' . json_last_error_msg() + ); + } - return $data; - } + return $data; + } - /** - * Create a matching Echo Knowledge Base article with full structured content. - * - * @param int $peptide_post_id The peptide post ID. - * @param array $ai_data The parsed AI data. - * @param string $peptide_name The peptide name. - */ - private static function create_kb_article( $peptide_post_id, $ai_data, $peptide_name ) { - if ( ! post_type_exists( 'epkb_post_type_1' ) ) { - error_log( 'PSA: Echo Knowledge Base not active — skipping KB article.' ); - return; - } + /** + * Create a matching Echo Knowledge Base article with full structured content. + * + * @param int $peptide_post_id The peptide post ID. + * @param array $ai_data The parsed AI data. + * @param string $peptide_name The peptide name. + */ + private static function create_kb_article( $peptide_post_id, $ai_data, $peptide_name ) { + if ( ! post_type_exists( 'epkb_post_type_1' ) ) { + error_log( 'PSA: Echo Knowledge Base not active — skipping KB article.' ); + return; + } - $title = sanitize_text_field( $ai_data['name'] ?? $peptide_name ); - $existing = get_posts( + $title = sanitize_text_field( $ai_data['name'] ?? $peptide_name ); + $existing = get_posts( array( 'post_type' => 'epkb_post_type_1', 'title' => $title, @@ -526,77 +526,77 @@ PROMPT; ) ); - if ( ! empty( $existing ) ) { - error_log( 'PSA: KB article already exists for "' . $peptide_name . '".' ); - return; - } + if ( ! empty( $existing ) ) { + error_log( 'PSA: KB article already exists for "' . $peptide_name . '".' ); + return; + } - // Build header meta line. - $cat_label = ! empty( $ai_data['category_label'] ) ? $ai_data['category_label'] : ''; - $origin = ! empty( $ai_data['origin'] ) ? $ai_data['origin'] : ''; - $sequence = ! empty( $ai_data['sequence'] ) ? $ai_data['sequence'] : ''; + // Build header meta line. + $cat_label = ! empty( $ai_data['category_label'] ) ? $ai_data['category_label'] : ''; + $origin = ! empty( $ai_data['origin'] ) ? $ai_data['origin'] : ''; + $sequence = ! empty( $ai_data['sequence'] ) ? $ai_data['sequence'] : ''; - $header = ''; - if ( $cat_label ) { - $header .= 'Category: ' . esc_html( $cat_label ); - } - if ( $origin ) { - $header .= ' | Origin: ' . esc_html( $origin ); - } - if ( $sequence ) { - $header .= ' | Sequence: ' . esc_html( $sequence ); - } + $header = ''; + if ( $cat_label ) { + $header .= 'Category: ' . esc_html( $cat_label ); + } + if ( $origin ) { + $header .= ' | Origin: ' . esc_html( $origin ); + } + if ( $sequence ) { + $header .= ' | Sequence: ' . esc_html( $sequence ); + } - // Build full article content. - $c = $header ? '

' . $header . '

' . "\n\n" : ''; + // Build full article content. + $c = $header ? '

' . $header . '

' . "\n\n" : ''; - // Overview. - $c .= '

Overview

' . "\n"; - $c .= self::paragraphs( $ai_data['overview'] ?? $ai_data['description'] ?? '' ); + // Overview. + $c .= '

Overview

' . "\n"; + $c .= self::paragraphs( $ai_data['overview'] ?? $ai_data['description'] ?? '' ); - // Mechanism of Action. - if ( ! empty( $ai_data['mechanism'] ) ) { - $c .= '

Mechanism of Action

' . "\n"; - $c .= self::paragraphs( $ai_data['mechanism'] ); - } + // Mechanism of Action. + if ( ! empty( $ai_data['mechanism'] ) ) { + $c .= '

Mechanism of Action

' . "\n"; + $c .= self::paragraphs( $ai_data['mechanism'] ); + } - // Research & Potential Benefits (with h3 subheadings). - if ( ! empty( $ai_data['research_benefits'] ) || ! empty( $ai_data['research_applications'] ) ) { - $c .= '

Research & Potential Benefits

' . "\n"; - $c .= '

The following potential benefits are based on animal studies and preclinical research. Human clinical trial data is extremely limited.

' . "\n"; - $research = $ai_data['research_benefits'] ?? $ai_data['research_applications'] ?? ''; - $c .= self::research_with_subheadings( $research ); - } + // Research & Potential Benefits (with h3 subheadings). + if ( ! empty( $ai_data['research_benefits'] ) || ! empty( $ai_data['research_applications'] ) ) { + $c .= '

Research & Potential Benefits

' . "\n"; + $c .= '

The following potential benefits are based on animal studies and preclinical research. Human clinical trial data is extremely limited.

' . "\n"; + $research = $ai_data['research_benefits'] ?? $ai_data['research_applications'] ?? ''; + $c .= self::research_with_subheadings( $research ); + } - // Administration & Dosing. - if ( ! empty( $ai_data['administration_dosing'] ) || ! empty( $ai_data['dosage_info'] ) ) { - $c .= '

Administration & Dosing (Research Context)

' . "\n"; - $c .= '

Important: The following dosing information is derived from animal studies and extrapolated research. This peptide is not approved for human use. This information is provided for educational purposes only.

' . "\n"; - $c .= self::paragraphs( $ai_data['administration_dosing'] ?? $ai_data['dosage_info'] ?? '' ); - } + // Administration & Dosing. + if ( ! empty( $ai_data['administration_dosing'] ) || ! empty( $ai_data['dosage_info'] ) ) { + $c .= '

Administration & Dosing (Research Context)

' . "\n"; + $c .= '

Important: The following dosing information is derived from animal studies and extrapolated research. This peptide is not approved for human use. This information is provided for educational purposes only.

' . "\n"; + $c .= self::paragraphs( $ai_data['administration_dosing'] ?? $ai_data['dosage_info'] ?? '' ); + } - // Safety & Side Effects. - if ( ! empty( $ai_data['safety_side_effects'] ) || ! empty( $ai_data['safety_profile'] ) ) { - $c .= '

Safety & Side Effects

' . "\n"; - $c .= self::paragraphs( $ai_data['safety_side_effects'] ?? $ai_data['safety_profile'] ?? '' ); - } + // Safety & Side Effects. + if ( ! empty( $ai_data['safety_side_effects'] ) || ! empty( $ai_data['safety_profile'] ) ) { + $c .= '

Safety & Side Effects

' . "\n"; + $c .= self::paragraphs( $ai_data['safety_side_effects'] ?? $ai_data['safety_profile'] ?? '' ); + } - // Legal & Regulatory Status. - if ( ! empty( $ai_data['legal_regulatory'] ) ) { - $c .= '

Legal & Regulatory Status

' . "\n"; - $c .= self::paragraphs( $ai_data['legal_regulatory'] ); - } + // Legal & Regulatory Status. + if ( ! empty( $ai_data['legal_regulatory'] ) ) { + $c .= '

Legal & Regulatory Status

' . "\n"; + $c .= self::paragraphs( $ai_data['legal_regulatory'] ); + } - // References. - if ( ! empty( $ai_data['references'] ) ) { - $c .= '

Notable Research References

' . "\n"; - $c .= self::paragraphs( $ai_data['references'] ); - } + // References. + if ( ! empty( $ai_data['references'] ) ) { + $c .= '

Notable Research References

' . "\n"; + $c .= self::paragraphs( $ai_data['references'] ); + } - // Medical Disclaimer. - $c .= "\n" . '

Medical Disclaimer: The information on this page is provided for educational and research purposes only. This peptide is not approved for human therapeutic use. Nothing on this page constitutes medical advice. Always consult a licensed healthcare professional before using any peptide, supplement, or research chemical.

'; + // Medical Disclaimer. + $c .= "\n" . '

Medical Disclaimer: The information on this page is provided for educational and research purposes only. This peptide is not approved for human therapeutic use. Nothing on this page constitutes medical advice. Always consult a licensed healthcare professional before using any peptide, supplement, or research chemical.

'; - $kb_post_id = wp_insert_post( + $kb_post_id = wp_insert_post( array( 'post_title' => $title, 'post_content' => wp_kses_post( $c ), @@ -606,87 +606,87 @@ PROMPT; true ); - if ( is_wp_error( $kb_post_id ) ) { - error_log( 'PSA: Failed to create KB article: ' . $kb_post_id->get_error_message() ); - return; - } + if ( is_wp_error( $kb_post_id ) ) { + error_log( 'PSA: Failed to create KB article: ' . $kb_post_id->get_error_message() ); + return; + } - // Assign KB category. - $category_map = array( - 'GH Secretagogues' => 'gh-secretagogues', - 'Healing & Repair' => 'healing-repair', - 'Melanocortin Peptides' => 'melanocortin-peptides', - 'Metabolic & Anti-Aging' => 'metabolic-anti-aging', - 'Nootropic & Neuroprotective' => 'nootropic-neuroprotective', - ); + // Assign KB category. + $category_map = array( + 'GH Secretagogues' => 'gh-secretagogues', + 'Healing & Repair' => 'healing-repair', + 'Melanocortin Peptides' => 'melanocortin-peptides', + 'Metabolic & Anti-Aging' => 'metabolic-anti-aging', + 'Nootropic & Neuroprotective' => 'nootropic-neuroprotective', + ); - $cat_name = trim( $ai_data['category'] ?? '' ); - if ( isset( $category_map[ $cat_name ] ) ) { - $term = get_term_by( 'slug', $category_map[ $cat_name ], 'epkb_post_type_1_category' ); - if ( $term ) { - wp_set_object_terms( $kb_post_id, array( (int) $term->term_id ), 'epkb_post_type_1_category' ); - } - } + $cat_name = trim( $ai_data['category'] ?? '' ); + if ( isset( $category_map[ $cat_name ] ) ) { + $term = get_term_by( 'slug', $category_map[ $cat_name ], 'epkb_post_type_1_category' ); + if ( $term ) { + wp_set_object_terms( $kb_post_id, array( (int) $term->term_id ), 'epkb_post_type_1_category' ); + } + } - update_post_meta( $peptide_post_id, 'psa_kb_article_id', $kb_post_id ); - update_post_meta( $kb_post_id, 'psa_peptide_post_id', $peptide_post_id ); + update_post_meta( $peptide_post_id, 'psa_kb_article_id', $kb_post_id ); + update_post_meta( $kb_post_id, 'psa_peptide_post_id', $peptide_post_id ); - error_log( 'PSA: Created KB article #' . $kb_post_id . ' for "' . $peptide_name . '"' ); - } + error_log( 'PSA: Created KB article #' . $kb_post_id . ' for "' . $peptide_name . '"' ); + } - /** - * Convert text to HTML paragraphs. - */ - private static function paragraphs( $text ) { - if ( empty( $text ) ) { - return ''; - } - $text = str_replace( "\r\n", "\n", $text ); - $parts = preg_split( '/\n{2,}/', trim( $text ) ); - $out = ''; - foreach ( $parts as $p ) { - $p = trim( $p ); - if ( '' === $p ) { - continue; - } - $out .= '

' . wp_kses_post( $p ) . '

' . "\n"; - } - return $out; - } + /** + * Convert text to HTML paragraphs. + */ + private static function paragraphs( $text ) { + if ( empty( $text ) ) { + return ''; + } + $text = str_replace( "\r\n", "\n", $text ); + $parts = preg_split( '/\n{2,}/', trim( $text ) ); + $out = ''; + foreach ( $parts as $p ) { + $p = trim( $p ); + if ( '' === $p ) { + continue; + } + $out .= '

' . wp_kses_post( $p ) . '

' . "\n"; + } + return $out; + } - /** - * Parse research text into h3 subheadings + paragraphs. - */ - private static function research_with_subheadings( $text ) { - if ( empty( $text ) ) { - return ''; - } - $text = str_replace( "\r\n", "\n", trim( $text ) ); - $blocks = preg_split( '/\n{2,}/', $text ); - $out = ''; - foreach ( $blocks as $block ) { - $block = trim( $block ); - if ( '' === $block ) { - continue; - } - // If block is short (under 80 chars) and has no period, treat as a subheading. - if ( strlen( $block ) < 80 && strpos( $block, '.' ) === false ) { - $out .= '

' . esc_html( $block ) . '

' . "\n"; - } else { - // Check if first line is a subheading followed by content. - $first_nl = strpos( $block, "\n" ); - if ( $first_nl !== false ) { - $first_line = trim( substr( $block, 0, $first_nl ) ); - $rest = trim( substr( $block, $first_nl + 1 ) ); - if ( strlen( $first_line ) < 80 && strpos( $first_line, '.' ) === false ) { - $out .= '

' . esc_html( $first_line ) . '

' . "\n"; - $out .= '

' . wp_kses_post( $rest ) . '

' . "\n"; - continue; - } - } - $out .= '

' . wp_kses_post( $block ) . '

' . "\n"; - } - } - return $out; - } + /** + * Parse research text into h3 subheadings + paragraphs. + */ + private static function research_with_subheadings( $text ) { + if ( empty( $text ) ) { + return ''; + } + $text = str_replace( "\r\n", "\n", trim( $text ) ); + $blocks = preg_split( '/\n{2,}/', $text ); + $out = ''; + foreach ( $blocks as $block ) { + $block = trim( $block ); + if ( '' === $block ) { + continue; + } + // If block is short (under 80 chars) and has no period, treat as a subheading. + if ( strlen( $block ) < 80 && strpos( $block, '.' ) === false ) { + $out .= '

' . esc_html( $block ) . '

' . "\n"; + } else { + // Check if first line is a subheading followed by content. + $first_nl = strpos( $block, "\n" ); + if ( $first_nl !== false ) { + $first_line = trim( substr( $block, 0, $first_nl ) ); + $rest = trim( substr( $block, $first_nl + 1 ) ); + if ( strlen( $first_line ) < 80 && strpos( $first_line, '.' ) === false ) { + $out .= '

' . esc_html( $first_line ) . '

' . "\n"; + $out .= '

' . wp_kses_post( $rest ) . '

' . "\n"; + continue; + } + } + $out .= '

' . wp_kses_post( $block ) . '

' . "\n"; + } + } + return $out; + } }