diff --git a/admin/class-peptide-news-admin.php b/admin/class-peptide-news-admin.php
index 177dbd8..7ccffa8 100644
--- a/admin/class-peptide-news-admin.php
+++ b/admin/class-peptide-news-admin.php
@@ -568,6 +568,13 @@ class Peptide_News_Admin {
echo '';
echo '';
+ // Generate AI Summaries button.
+ echo '
';
+ echo '';
+ echo '';
+
?>
prefix . 'peptide_news_articles';
@@ -370,6 +374,63 @@ class Peptide_News_LLM {
}
}
+ /**
+ * AJAX handler: bulk-generate AI summaries for articles that are missing them.
+ *
+ * Processes up to 50 articles per request. Returns the count of articles
+ * processed and remaining so the admin UI can loop until done.
+ *
+ * @since 2.0.1
+ */
+ public static function ajax_generate_summaries() {
+ check_ajax_referer( 'peptide_news_admin', 'nonce' );
+
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( 'Unauthorized', 403 );
+ }
+
+ if ( ! self::is_enabled() ) {
+ wp_send_json_error( 'AI Analysis is not enabled. Please enable it and set an OpenRouter API key in Peptide News → Settings.' );
+ }
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'peptide_news_articles';
+
+ // Count how many still need processing.
+ $remaining = (int) $wpdb->get_var(
+ "SELECT COUNT(*) FROM {$table}
+ WHERE is_active = 1
+ AND ( ai_summary = '' OR ai_summary IS NULL )"
+ );
+
+ if ( $remaining < 1 ) {
+ wp_send_json_success( array(
+ 'processed' => 0,
+ 'remaining' => 0,
+ 'message' => 'All articles already have AI summaries.',
+ ) );
+ }
+
+ // Process a generous batch — the 120-second timeout guard inside
+ // process_unanalyzed() will stop early if needed. Override the
+ // per-cycle limit since this is a manual admin action.
+ $batch_size = min( 50, $remaining );
+ $processed = self::process_unanalyzed( $batch_size, true );
+
+ // Recount remaining after this batch.
+ $still_remaining = (int) $wpdb->get_var(
+ "SELECT COUNT(*) FROM {$table}
+ WHERE is_active = 1
+ AND ( ai_summary = '' OR ai_summary IS NULL )"
+ );
+
+ wp_send_json_success( array(
+ 'processed' => $processed,
+ 'remaining' => $still_remaining,
+ 'message' => sprintf( '%d article(s) summarized, %d remaining.', $processed, $still_remaining ),
+ ) );
+ }
+
/**
* Clear all article transient caches.
*/
diff --git a/includes/class-peptide-news.php b/includes/class-peptide-news.php
index 7dee51b..25a0cc3 100644
--- a/includes/class-peptide-news.php
+++ b/includes/class-peptide-news.php
@@ -81,6 +81,7 @@ class Peptide_News {
$this->loader->add_action( 'peptide_news_cron_fetch', $fetcher, 'fetch_all_sources' );
$this->loader->add_filter( 'cron_schedules', $fetcher, 'add_custom_cron_schedules' );
$this->loader->add_action( 'wp_ajax_peptide_news_backfill_sources', $fetcher, 'ajax_backfill_sources' );
+ $this->loader->add_action( 'wp_ajax_peptide_news_generate_summaries', 'Peptide_News_LLM', 'ajax_generate_summaries' );
}
/**