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();
}