From 4a67958a68a71c20778c6d8a20613865cc1aaefb Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Fri, 17 Apr 2026 02:39:33 +0700 Subject: [PATCH] =?UTF-8?q?refactor:=20extract=20PSA=5FAdmin=5FPage=20from?= =?UTF-8?q?=20PSA=5FAdmin=20(560=E2=86=92290+221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rendering methods (settings page, field renderers, usage summary, batch script enqueue) moved to PSA_Admin_Page. PSA_Admin retains registration, sanitization, migration, re-enrichment, and get_setting (now public). Backward-compatible proxy methods on PSA_Admin. Co-Authored-By: Claude Opus 4.6 --- includes/class-psa-admin-page.php | 221 +++++++++++++++ includes/class-psa-admin.php | 456 ++++++------------------------ peptide-search-ai.php | 1 + 3 files changed, 315 insertions(+), 363 deletions(-) create mode 100644 includes/class-psa-admin-page.php diff --git a/includes/class-psa-admin-page.php b/includes/class-psa-admin-page.php new file mode 100644 index 0000000..4250995 --- /dev/null +++ b/includes/class-psa-admin-page.php @@ -0,0 +1,221 @@ + admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'psa_batch_enrich' ), + ) ); + } + + // ── Field renderers ───────────────────────────────────────────────── + + public static function render_section_description(): void { + printf( + '

' . __( 'This plugin uses OpenRouter to access AI models.', 'peptide-search-ai' ) . '

', + esc_url( 'https://openrouter.ai/' ) + ); + } + + public static function render_api_key_field(): void { + if ( defined( 'PSA_OPENROUTER_KEY' ) && PSA_OPENROUTER_KEY ) { + echo ' '; + echo '' . esc_html__( 'Configured via wp-config.php', 'peptide-search-ai' ) . ''; + echo '

' . esc_html__( 'The API key is securely set using the PSA_OPENROUTER_KEY constant.', 'peptide-search-ai' ) . '

'; + } else { + $value = PSA_Admin::get_setting( 'api_key', '' ); + $display_value = ! empty( $value ) ? '••••••••' : ''; + printf( + '', + esc_attr( $display_value ) + ); + printf( + '

%s %s.

', + esc_html__( 'Your OpenRouter API key.', 'peptide-search-ai' ), + esc_html__( 'Get one here', 'peptide-search-ai' ) + ); + } + } + + public static function render_model_field(): void { + $value = PSA_Admin::get_setting( 'ai_model', '' ); + printf( + '', + esc_attr( $value ), + esc_attr__( 'Leave blank for default', 'peptide-search-ai' ) + ); + echo '

' . esc_html__( 'Model for full content generation. Default: google/gemini-2.5-flash', 'peptide-search-ai' ) . '

'; + } + + public static function render_validation_model_field(): void { + $value = PSA_Admin::get_setting( 'validation_model', '' ); + printf( + '', + esc_attr( $value ), + esc_attr__( 'Leave blank for default', 'peptide-search-ai' ) + ); + echo '

' . esc_html__( 'Lighter model for name validation. Default: google/gemini-2.0-flash-001', 'peptide-search-ai' ) . '

'; + } + + public static function render_publish_field(): void { + $value = PSA_Admin::get_setting( 'auto_publish', 'draft' ); + echo ''; + } + + public static function render_pubchem_field(): void { + $value = PSA_Admin::get_setting( 'use_pubchem', '1' ); + printf( + '', + checked( $value, '1', false ), + esc_html__( 'Cross-reference with PubChem for verified molecular data', 'peptide-search-ai' ) + ); + } + + public static function render_budget_field(): void { + $value = PSA_Admin::get_setting( 'monthly_budget', PSA_Config::DEFAULT_MONTHLY_BUDGET ); + printf( + '', + esc_attr( $value ) + ); + echo '

' . esc_html__( 'Set to 0 for unlimited.', 'peptide-search-ai' ) . '

'; + } + + // ── Settings page ─────────────────────────────────────────────────── + + public static function render_settings_page(): void { + $migrate_url = wp_nonce_url( admin_url( 'options-general.php?page=peptide-search-ai&psa_action=migrate_categories' ), 'psa_migrate_categories' ); + ?> +
+

+ +
+ +
+ +
+

+ + +
+

+ + + + + + + + + +
+ +

+
+ + +

+ + +
+ +
+

+

[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 —

+
+ +
+

+

+ $ + 0 ) : ?> + ⚠ includes estimates + +
+
+ +

+
+ +

+ + + + + + + + + + + + + + + token_source ) && 'estimated' === $log->token_source ); ?> + > + + + + + + + + + + +
created_at ); ?>model ); ?>~' : ''; ?>total_tokens ) ); ?>' : ''; ?>~$' : '$'; ?>estimated_cost_usd, 4 ) ); ?>' : ''; ?>request_type ); ?>peptide_name ); ?>success ) ? '✓' : '✗'; ?>
+ array( __CLASS__, 'sanitize_settings' ), - ) - ); + register_setting( 'psa_settings_group', 'psa_settings', array( + 'sanitize_callback' => 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' ); + add_settings_section( 'psa_ai_section', __( 'OpenRouter API Settings', 'peptide-search-ai' ), array( 'PSA_Admin_Page', 'render_section_description' ), 'peptide-search-ai' ); + add_settings_field( 'api_key', __( 'API Key', 'peptide-search-ai' ), array( 'PSA_Admin_Page', 'render_api_key_field' ), 'peptide-search-ai', 'psa_ai_section' ); + add_settings_field( 'ai_model', __( 'Generation Model', 'peptide-search-ai' ), array( 'PSA_Admin_Page', 'render_model_field' ), 'peptide-search-ai', 'psa_ai_section' ); + add_settings_field( 'validation_model', __( 'Validation Model', 'peptide-search-ai' ), array( 'PSA_Admin_Page', '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' ); + add_settings_field( 'auto_publish', __( 'Auto-Publish', 'peptide-search-ai' ), array( 'PSA_Admin_Page', 'render_publish_field' ), 'peptide-search-ai', 'psa_behavior_section' ); + add_settings_field( 'use_pubchem', __( 'PubChem Enrichment', 'peptide-search-ai' ), array( 'PSA_Admin_Page', 'render_pubchem_field' ), 'peptide-search-ai', 'psa_behavior_section' ); + add_settings_field( 'monthly_budget', __( 'Monthly API Budget (USD)', 'peptide-search-ai' ), array( 'PSA_Admin_Page', 'render_budget_field' ), 'peptide-search-ai', 'psa_behavior_section' ); } /** * Sanitize settings input. * - * @param array $input Raw input from the form. + * @param array $input Raw form input. * @return array Sanitized settings. */ public static function sanitize_settings( array $input ): array { @@ -79,23 +70,20 @@ class PSA_Admin { $encrypted = PSA_Encryption::encrypt( $sanitized ); $api_key_value = ( false !== $encrypted ) ? $encrypted : ''; } else { - $existing = self::get_setting( 'api_key', '' ); - $api_key_value = $existing; + $api_key_value = self::get_setting( 'api_key', '' ); } 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 ) ), + '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. - */ + /** 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' ) ) { @@ -107,53 +95,36 @@ class PSA_Admin { 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' ) . '

'; - } - ); + 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 - ) - ) . '

'; - } - ); + add_action( 'admin_notices', function () use ( $queued ) { + echo '

' . esc_html( sprintf( + __( '%d peptides queued for re-enrichment.', 'peptide-search-ai' ), $queued + ) ) . '

'; + } ); } } - // ------------------------------------------------------------------------- - // Migration: assign categories to existing peptides - // ------------------------------------------------------------------------- + // ── Category migration ────────────────────────────────────────────── /** - * 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). + * Assign peptide_category terms to existing posts from free-text meta. + * Idempotent — safe to run multiple times. */ 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', - ) - ); + $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; @@ -167,7 +138,6 @@ class PSA_Admin { 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' ); @@ -210,7 +180,7 @@ class PSA_Admin { return $map[ $lower ]; } - // Fuzzy match. + // Fuzzy match against registered default categories. foreach ( PSA_Post_Type::DEFAULT_CATEGORIES as $slug => $name ) { if ( stripos( $text, $name ) !== false || stripos( $text, str_replace( '-', ' ', $slug ) ) !== false ) { return $slug; @@ -220,46 +190,32 @@ class PSA_Admin { return ''; } - // ------------------------------------------------------------------------- - // Re-enrichment: queue background regeneration for posts missing new fields - // ------------------------------------------------------------------------- + // ── Re-enrichment ─────────────────────────────────────────────────── /** - * Queue re-enrichment for published peptides missing the new extended fields. - * - * Schedules WP-Cron events for each peptide that needs regeneration. + * Queue re-enrichment for published peptides missing extended fields. * 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' => '', - ), - ), - ) - ); + $posts = get_posts( array( + 'post_type' => 'peptide', + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'fields' => 'ids', + '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; } @@ -269,29 +225,19 @@ class PSA_Admin { 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', - ) - ); + 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. + // Stagger 30s 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(); } @@ -299,262 +245,46 @@ class PSA_Admin { return $queued; } - /** - * Enqueue batch enrichment script on the PSA settings page only. - * - * @param string $hook The admin page hook suffix. - */ - public static function enqueue_batch_scripts( string $hook ): void { - if ( 'settings_page_peptide-search-ai' !== $hook ) { - return; - } - - wp_enqueue_script( - 'psa-batch-enrich', - PSA_PLUGIN_URL . 'assets/js/psa-batch-enrich.js', - array( 'jquery' ), - PSA_VERSION, - true - ); - - wp_localize_script( - 'psa-batch-enrich', - 'psaBatchEnrich', - array( - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'psa_batch_enrich' ), - ) - ); - } - - // ------------------------------------------------------------------------- - // 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 —

-
- -
-

-

- $ - 0 ) : ?> - ⚠ includes estimates - -
-
- -

-
- - -

- - - - - - - - - - - - - - - token_source ) && 'estimated' === $log->token_source ); ?> - > - - - - - - - - - - -
created_at ); ?>model ); ?> - - ~total_tokens ) ); ?> - - total_tokens ) ); ?> - - - - ~$estimated_cost_usd, 4 ) ); ?> - - $estimated_cost_usd, 4 ) ); ?> - - request_type ); ?>peptide_name ); ?>success ) ? '✓' : '✗'; ?>
- -