array( __CLASS__, 'sanitize_settings' ), ) ); // OpenRouter Section. add_settings_section( 'psa_ai_section', __( 'OpenRouter API Settings', 'peptide-search-ai' ), array( __CLASS__, 'render_section_description' ), 'peptide-search-ai' ); add_settings_field( 'api_key', __( 'API Key', 'peptide-search-ai' ), array( __CLASS__, 'render_api_key_field' ), 'peptide-search-ai', 'psa_ai_section' ); add_settings_field( 'ai_model', __( 'Generation Model', 'peptide-search-ai' ), array( __CLASS__, 'render_model_field' ), 'peptide-search-ai', 'psa_ai_section' ); add_settings_field( 'validation_model', __( 'Validation Model', 'peptide-search-ai' ), array( __CLASS__, 'render_validation_model_field' ), 'peptide-search-ai', 'psa_ai_section' ); // Behavior Section. add_settings_section( 'psa_behavior_section', __( 'Behavior Settings', 'peptide-search-ai' ), null, 'peptide-search-ai' ); add_settings_field( 'auto_publish', __( 'Auto-Publish', 'peptide-search-ai' ), array( __CLASS__, 'render_publish_field' ), 'peptide-search-ai', 'psa_behavior_section' ); add_settings_field( 'use_pubchem', __( 'PubChem Enrichment', 'peptide-search-ai' ), array( __CLASS__, 'render_pubchem_field' ), 'peptide-search-ai', 'psa_behavior_section' ); add_settings_field( 'monthly_budget', __( 'Monthly API Budget (USD)', 'peptide-search-ai' ), array( __CLASS__, 'render_budget_field' ), 'peptide-search-ai', 'psa_behavior_section' ); } /** * Sanitize settings input. * * @param array $input Raw input from the form. * @return array Sanitized settings. */ public static function sanitize_settings( array $input ): array { $api_key_input = $input['api_key'] ?? ''; $api_key_value = ''; if ( ! empty( $api_key_input ) && '••••••••' !== $api_key_input ) { $sanitized = sanitize_text_field( $api_key_input ); $encrypted = PSA_Encryption::encrypt( $sanitized ); $api_key_value = ( false !== $encrypted ) ? $encrypted : ''; } else { $existing = self::get_setting( 'api_key', '' ); $api_key_value = $existing; } return array( 'api_key' => $api_key_value, 'ai_model' => sanitize_text_field( $input['ai_model'] ?? '' ), 'validation_model' => sanitize_text_field( $input['validation_model'] ?? '' ), 'auto_publish' => sanitize_text_field( $input['auto_publish'] ?? 'draft' ), 'use_pubchem' => ! empty( $input['use_pubchem'] ) ? '1' : '0', 'monthly_budget' => max( 0, floatval( $input['monthly_budget'] ?? PSA_Config::DEFAULT_MONTHLY_BUDGET ) ), ); } /** * Handle admin actions (migration, re-enrich) via GET params with nonce verification. */ public static function handle_admin_actions(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['psa_action'] ) || ! current_user_can( 'manage_options' ) ) { return; } $action = sanitize_text_field( wp_unslash( $_GET['psa_action'] ) ); $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; if ( 'migrate_categories' === $action && wp_verify_nonce( $nonce, 'psa_migrate_categories' ) ) { self::run_category_migration(); add_action( 'admin_notices', function () { echo '

' . esc_html__( 'Category migration completed.', 'peptide-search-ai' ) . '

'; } ); } if ( 'reenrich' === $action && wp_verify_nonce( $nonce, 'psa_reenrich' ) ) { $queued = self::queue_reenrichment(); add_action( 'admin_notices', function () use ( $queued ) { echo '

' . esc_html( sprintf( /* translators: %d = number of peptides queued */ __( '%d peptides queued for re-enrichment.', 'peptide-search-ai' ), $queued ) ) . '

'; } ); } } // ------------------------------------------------------------------------- // Migration: assign categories to existing peptides // ------------------------------------------------------------------------- /** * One-time migration: assign peptide_category terms to existing peptide posts. * * Reads the free-text psa_category meta from each published peptide and maps * it to a taxonomy term. Safe to run multiple times (idempotent). */ private static function run_category_migration(): void { $posts = get_posts( array( 'post_type' => 'peptide', 'post_status' => array( 'publish', 'draft' ), 'posts_per_page' => -1, 'fields' => 'ids', ) ); foreach ( $posts as $post_id ) { // Skip posts that already have a category term assigned. $terms = wp_get_object_terms( $post_id, 'peptide_category' ); if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) { continue; } $category = get_post_meta( $post_id, 'psa_category', true ); $label = get_post_meta( $post_id, 'psa_category_label', true ); $text = ! empty( $category ) ? $category : $label; if ( empty( $text ) ) { continue; } // Use the same mapping logic as the AI generator. $slug = self::map_category_to_slug( $text ); if ( ! empty( $slug ) ) { wp_set_object_terms( $post_id, $slug, 'peptide_category' ); } } } /** * Map free-text category label to taxonomy slug. * * @param string $text Category label from AI or meta. * @return string Taxonomy term slug, or empty if no match. */ private static function map_category_to_slug( string $text ): string { $map = array( 'gh secretagogues' => 'growth-hormone', 'growth hormone' => 'growth-hormone', 'healing & repair' => 'tissue-repair', 'healing and repair' => 'tissue-repair', 'tissue repair' => 'tissue-repair', 'tissue healing' => 'tissue-repair', 'cytoprotective' => 'tissue-repair', 'melanocortin peptides' => 'dermatological', 'melanocortin' => 'dermatological', 'dermatological' => 'dermatological', 'metabolic & anti-aging' => 'metabolic', 'metabolic' => 'metabolic', 'anti-aging' => 'aging-research', 'aging research' => 'aging-research', 'immunology' => 'immunology', 'immune' => 'immunology', 'nootropic' => 'immunology', 'neuroprotective' => 'immunology', 'lipid metabolism' => 'lipid-metabolism', 'endocrine' => 'endocrine', ); $lower = strtolower( trim( $text ) ); if ( isset( $map[ $lower ] ) ) { return $map[ $lower ]; } // Fuzzy match. foreach ( PSA_Post_Type::DEFAULT_CATEGORIES as $slug => $name ) { if ( stripos( $text, $name ) !== false || stripos( $text, str_replace( '-', ' ', $slug ) ) !== false ) { return $slug; } } return ''; } // ------------------------------------------------------------------------- // Re-enrichment: queue background regeneration for posts missing new fields // ------------------------------------------------------------------------- /** * Queue re-enrichment for published peptides missing the new extended fields. * * Schedules WP-Cron events for each peptide that needs regeneration. * Respects the daily generation cap to avoid cost overruns. * * @return int Number of peptides queued. */ private static function queue_reenrichment(): int { $posts = get_posts( array( 'post_type' => 'peptide', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', // Only select posts missing the half_life field (proxy for "needs extended data"). 'meta_query' => array( 'relation' => 'OR', array( 'key' => 'psa_half_life', 'compare' => 'NOT EXISTS', ), array( 'key' => 'psa_half_life', 'value' => '', ), ), ) ); $daily_key = 'psa_daily_gen_' . gmdate( 'Y-m-d' ); $daily_count = (int) get_transient( $daily_key ); $queued = 0; foreach ( $posts as $post_id ) { // Respect daily cap. if ( ( $daily_count + $queued ) >= PSA_Config::DAILY_GENERATION_CAP ) { break; } $post = get_post( $post_id ); if ( ! $post ) { continue; } // Mark as pending so the generator picks it up, then schedule. update_post_meta( $post_id, 'psa_source', 'pending' ); wp_update_post( array( 'ID' => $post_id, 'post_status' => 'draft' ) ); update_post_meta( $post_id, 'psa_generation_started', time() ); $args = array( $post_id, $post->post_title ); if ( ! wp_next_scheduled( 'psa_generate_peptide_background', $args ) ) { // Stagger jobs 30 seconds apart to avoid rate limit hammering. wp_schedule_single_event( time() + ( $queued * 30 ), 'psa_generate_peptide_background', $args ); } $queued++; } // Bump daily counter. set_transient( $daily_key, $daily_count + $queued, DAY_IN_SECONDS ); // Kick off the cron runner. if ( $queued > 0 ) { spawn_cron(); } return $queued; } // ------------------------------------------------------------------------- // Field renderers // ------------------------------------------------------------------------- public static function render_section_description(): void { ?>

OpenRouter to access AI models.', 'peptide-search-ai' ), esc_url( 'https://openrouter.ai/' ) ); ?>

.




[peptide_search] —

[peptide_directory] —


GET /wp-json/peptides/v1/search?q=BPC-157 —

GET /wp-json/peptide-search-ai/v1/compounds?search=&category=&page=1 —

$

created_at ); ?> model ); ?> total_tokens ) ); ?> $estimated_cost_usd, 4 ) ); ?> request_type ); ?> peptide_name ); ?> success ) ? '✓' : '✗'; ?>