diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 07ada92..ed20d80 100755 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -27,12 +27,14 @@ peptide-search-ai/ │ ├── class-psa-cost-tracker.php API usage logging, monthly budget enforcement, cost estimation │ ├── class-psa-pubchem.php PubChem PUG REST API integration for molecular enrichment │ ├── class-psa-admin.php Settings page, usage dashboard, migration tools, re-enrichment actions +│ ├── class-psa-batch-enrichment.php AJAX-based batch re-enrichment (processes one peptide per request, no WP-Cron) │ └── class-psa-template.php Single peptide page rendering; quick-facts, extended data, badges │ ├── assets/ │ ├── js/ │ │ ├── peptide-search.js Frontend search, AJAX handler, result rendering (jQuery) -│ │ └── psa-directory.js Directory grid, category filters, detail modal (vanilla JS) +│ │ ├── psa-directory.js Directory grid, category filters, detail modal (vanilla JS) +│ │ └── psa-batch-enrich.js Admin batch enrichment UI (progress bar, AJAX polling loop) │ └── css/ │ ├── peptide-search.css Scoped styles for search UI, results, pending states, badges │ └── psa-directory.css Directory card grid, filter pills, modal styles, dark mode support diff --git a/assets/js/psa-batch-enrich.js b/assets/js/psa-batch-enrich.js new file mode 100755 index 0000000..20ab5a0 --- /dev/null +++ b/assets/js/psa-batch-enrich.js @@ -0,0 +1,172 @@ +/** + * Batch enrichment UI controller for the Peptide Search AI admin page. + * + * What: Drives the AJAX polling loop that enriches one peptide at a time with a progress bar. + * Who loads it: PSA_Admin::enqueue_batch_scripts() on the PSA settings page. + * Dependencies: jQuery (WP core), wp.ajax (WP admin AJAX helper), psaBatchEnrich localized data. + * + * @see includes/class-psa-batch-enrichment.php + * @see includes/class-psa-admin.php + */ +/* global jQuery, psaBatchEnrich */ +(function ($) { + 'use strict'; + + var isRunning = false; + var processed = 0; + var errors = 0; + var total = 0; + var remaining = 0; + + /** + * Initialize: bind the start button and fetch initial status. + */ + function init() { + var $btn = $('#psa-batch-enrich-start'); + if (!$btn.length) { + return; + } + + $btn.on('click', function (e) { + e.preventDefault(); + if (isRunning) { + return; + } + startEnrichment(); + }); + + // Fetch initial count on page load. + fetchStatus(); + } + + /** + * Fetch the current enrichment status (how many need enrichment). + */ + function fetchStatus() { + $.post(psaBatchEnrich.ajaxUrl, { + action: 'psa_batch_enrich_status', + _nonce: psaBatchEnrich.nonce + }, function (response) { + if (response.success) { + remaining = response.data.remaining; + total = response.data.remaining; // Total to process this run. + updateStatusText(response.data.remaining + ' peptides need enrichment'); + + if (response.data.remaining === 0) { + $('#psa-batch-enrich-start').prop('disabled', true).text('All enriched'); + } + } + }); + } + + /** + * Start the enrichment loop. + */ + function startEnrichment() { + isRunning = true; + processed = 0; + errors = 0; + + $('#psa-batch-enrich-start').prop('disabled', true).text('Running...'); + $('#psa-batch-enrich-progress').show(); + $('#psa-batch-enrich-log').empty().show(); + + updateProgress(0, total); + processNext(); + } + + /** + * Process one peptide, then loop. + */ + function processNext() { + if (!isRunning) { + return; + } + + $.post(psaBatchEnrich.ajaxUrl, { + action: 'psa_batch_enrich_next', + _nonce: psaBatchEnrich.nonce + }, function (response) { + if (!response.success) { + appendLog('Server error: ' + (response.data || 'Unknown'), 'error'); + finish(); + return; + } + + var data = response.data; + + if (data.status === 'done') { + appendLog('All peptides enriched!', 'success'); + finish(); + return; + } + + if (data.status === 'cap_reached') { + appendLog(data.message, 'warning'); + finish(); + return; + } + + processed++; + remaining = data.remaining; + + if (data.status === 'ok') { + appendLog('Enriched: ' + data.name, 'success'); + } else if (data.status === 'error') { + errors++; + appendLog('Failed: ' + data.name + ' — ' + data.message, 'error'); + } + + updateProgress(processed, processed + remaining); + + // Brief delay to avoid hammering the server — 2 seconds between requests. + setTimeout(processNext, 2000); + + }).fail(function (xhr) { + appendLog('Request failed (HTTP ' + xhr.status + '). Retrying in 10s...', 'warning'); + setTimeout(processNext, 10000); + }); + } + + /** + * Update the progress bar. + */ + function updateProgress(done, total) { + var pct = total > 0 ? Math.round((done / total) * 100) : 0; + $('#psa-batch-enrich-bar').css('width', pct + '%'); + $('#psa-batch-enrich-pct').text(pct + '% (' + done + '/' + total + ')'); + } + + /** + * Update the status text below the button. + */ + function updateStatusText(text) { + $('#psa-batch-enrich-status').text(text); + } + + /** + * Append a line to the log. + */ + function appendLog(message, type) { + var color = type === 'error' ? '#dc3232' : type === 'warning' ? '#dba617' : '#46b450'; + var icon = type === 'error' ? '✗' : type === 'warning' ? '⚠' : '✓'; + $('#psa-batch-enrich-log').prepend( + '
' + + icon + ' ' + $('').text(message).html() + + '
' + ); + } + + /** + * End the run. + */ + function finish() { + isRunning = false; + $('#psa-batch-enrich-start').prop('disabled', false).text('Re-enrich Peptides'); + + var summary = 'Done. ' + processed + ' processed, ' + errors + ' errors, ' + remaining + ' remaining.'; + updateStatusText(summary); + } + + $(document).ready(init); +})(jQuery); diff --git a/includes/class-psa-admin.php b/includes/class-psa-admin.php index 13a31df..4b23ca7 100755 --- a/includes/class-psa-admin.php +++ b/includes/class-psa-admin.php @@ -23,6 +23,7 @@ class PSA_Admin { add_action( 'admin_menu', array( __CLASS__, 'add_menu_pages' ) ); add_action( 'admin_init', array( __CLASS__, 'register_settings' ) ); add_action( 'admin_init', array( __CLASS__, 'handle_admin_actions' ) ); + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_batch_scripts' ) ); } /** @@ -298,6 +299,34 @@ 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 // ------------------------------------------------------------------------- @@ -386,7 +415,6 @@ class PSA_Admin { 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' ); - $reenrich_url = wp_nonce_url( admin_url( 'options-general.php?page=peptide-search-ai&psa_action=reenrich' ), 'psa_reenrich' ); ?>

@@ -418,10 +446,19 @@ class PSA_Admin { - - - -

+ + +

+ + + diff --git a/includes/class-psa-batch-enrichment.php b/includes/class-psa-batch-enrichment.php new file mode 100755 index 0000000..688a036 --- /dev/null +++ b/includes/class-psa-batch-enrichment.php @@ -0,0 +1,198 @@ + $total_published, + 'remaining' => count( $needs_enrichment ), + 'completed' => $total_published - count( $needs_enrichment ), + ) + ); + } + + /** + * AJAX: Process one peptide — set to draft, generate, republish. + * + * Response JSON on success: { post_id: int, name: string, status: 'ok', remaining: int } + * Response JSON on skip: { status: 'done', remaining: 0 } + * Response JSON on error: { post_id: int, name: string, status: 'error', message: string, remaining: int } + * + * Side effects: Updates post meta via AI generation, logs API cost. + */ + public static function ajax_process_next(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( 'Unauthorized', 403 ); + } + + check_ajax_referer( 'psa_batch_enrich', '_nonce' ); + + // Respect daily generation cap. + $daily_key = 'psa_daily_gen_' . gmdate( 'Y-m-d' ); + $daily_count = (int) get_transient( $daily_key ); + if ( $daily_count >= PSA_Config::DAILY_GENERATION_CAP ) { + wp_send_json_success( + array( + 'status' => 'cap_reached', + 'message' => sprintf( + 'Daily generation cap reached (%d/%d). Try again tomorrow.', + $daily_count, + PSA_Config::DAILY_GENERATION_CAP + ), + 'remaining' => count( self::get_unenriched_ids() ), + ) + ); + } + + $ids = self::get_unenriched_ids(); + if ( empty( $ids ) ) { + wp_send_json_success( + array( + 'status' => 'done', + 'remaining' => 0, + ) + ); + } + + $post_id = $ids[0]; + $post = get_post( $post_id ); + if ( ! $post ) { + wp_send_json_error( 'Post not found: ' . $post_id, 404 ); + } + + $peptide_name = $post->post_title; + $original_status = $post->post_status; + + // Temporarily set to draft + pending so background_generate processes it. + wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'draft', + ) + ); + update_post_meta( $post_id, 'psa_source', 'pending' ); + update_post_meta( $post_id, 'psa_generation_started', time() ); + + // Call the generator directly (synchronous — no cron dependency). + PSA_AI_Generator::background_generate( $post_id, $peptide_name ); + + // Bump daily counter. + $daily_count = (int) get_transient( $daily_key ); + set_transient( $daily_key, $daily_count + 1, DAY_IN_SECONDS ); + + // Check outcome: did generation succeed? + $error = get_post_meta( $post_id, 'psa_generation_error', true ); + $refreshed_post = get_post( $post_id ); + + // If still in draft (generation failed or auto_publish is 'draft'), force republish. + if ( $refreshed_post && 'draft' === $refreshed_post->post_status ) { + wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'publish', + ) + ); + } + + $remaining = count( self::get_unenriched_ids() ); + + if ( ! empty( $error ) ) { + wp_send_json_success( + array( + 'status' => 'error', + 'post_id' => $post_id, + 'name' => $peptide_name, + 'message' => $error, + 'remaining' => $remaining, + ) + ); + } + + wp_send_json_success( + array( + 'status' => 'ok', + 'post_id' => $post_id, + 'name' => $peptide_name, + 'remaining' => $remaining, + ) + ); + } + + /** + * Get IDs of published peptides missing the half_life extended field. + * + * @return int[] Post IDs. + */ + private static function get_unenriched_ids(): array { + return 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' => '', + ), + ), + ) + ); + } + + /** + * Get total published peptide count. + * + * @return int + */ + private static function get_total_published_count(): int { + $counts = wp_count_posts( 'peptide' ); + return (int) ( $counts->publish ?? 0 ); + } +} diff --git a/peptide-search-ai.php b/peptide-search-ai.php index 53cafa9..f8d85f7 100755 --- a/peptide-search-ai.php +++ b/peptide-search-ai.php @@ -3,7 +3,7 @@ * Plugin Name: Peptide Search AI * Plugin URI: https://example.com/peptide-search-ai * Description: Searchable peptide database with AI-powered auto-population and browsable directory. - * Version: 4.3.0 + * Version: 4.4.0 * Author: Terence * License: GPL v2 or later * Text Domain: peptide-search-ai @@ -15,7 +15,7 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } -define( 'PSA_VERSION', '4.3.0' ); +define( 'PSA_VERSION', '4.4.0' ); define( 'PSA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PSA_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'PSA_PLUGIN_FILE', __FILE__ ); @@ -30,6 +30,7 @@ require_once PSA_PLUGIN_DIR . 'includes/class-psa-kb-builder.php'; require_once PSA_PLUGIN_DIR . 'includes/class-psa-ai-generator.php'; require_once PSA_PLUGIN_DIR . 'includes/class-psa-pubchem.php'; require_once PSA_PLUGIN_DIR . 'includes/class-psa-admin.php'; +require_once PSA_PLUGIN_DIR . 'includes/class-psa-batch-enrichment.php'; require_once PSA_PLUGIN_DIR . 'includes/class-psa-template.php'; require_once PSA_PLUGIN_DIR . 'includes/class-psa-directory.php'; @@ -39,6 +40,7 @@ function psa_init() { PSA_Search::init(); PSA_AI_Generator::init(); PSA_Admin::init(); + PSA_Batch_Enrichment::init(); PSA_Template::init(); PSA_Directory::init(); }