Add generate-from-idea button to Ideas browser (v0.6.0)
Each idea row now has a Generate button that triggers single-article generation via a background WP-Cron job. Skips the pipeline's collect/ analyze/score steps and goes straight to Article_Worker with the selected idea. Button shows real-time stage updates via per-idea status transients, then swaps to Edit/View links with cost on completion. Respects the generation lock to prevent concurrent runs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
18
CHANGELOG.md
18
CHANGELOG.md
@@ -5,6 +5,24 @@ All notable changes to PRAutoBlogger will be documented in this file.
|
||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project uses [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [0.6.0] — 2026-04-20
|
||||
|
||||
### Added
|
||||
- **Generate article from idea.** New "Generate" column in the Ideas browser.
|
||||
Click any idea's Generate button to produce an article directly, bypassing
|
||||
the scheduled pipeline's collect → analyze → score steps. The button shows
|
||||
real-time stage updates (drafting, editing, publishing) via per-idea status
|
||||
polling, then swaps to Edit/View links with cost on completion.
|
||||
- `Ideas_Browser::on_ajax_generate_from_idea()` — AJAX trigger that stores
|
||||
the idea and schedules a one-shot WP-Cron event.
|
||||
- `Ideas_Browser::on_cron_generate_from_idea()` — Background handler that
|
||||
runs Article_Worker for the selected idea.
|
||||
- `Ideas_Browser::on_ajax_idea_gen_status()` — Per-idea status polling.
|
||||
- `assets/js/ideas-browser.js` — Button click, AJAX, polling, and UI state.
|
||||
- Per-idea transients (`prab_idea_gen_{id}`) for independent status tracking.
|
||||
- Page-load polling resume for ideas that were mid-generation.
|
||||
- Generation lock respected — only one generation at a time.
|
||||
|
||||
## [0.5.3] — 2026-04-20
|
||||
|
||||
### Fixed
|
||||
|
||||
157
assets/js/ideas-browser.js
Normal file
157
assets/js/ideas-browser.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* PRAutoBlogger Ideas Browser — "Generate Article" button handler.
|
||||
*
|
||||
* Each idea row has a Generate button that triggers single-article generation
|
||||
* via AJAX → WP-Cron background job. Polls for status and shows stage updates
|
||||
* in the button cell, then swaps to a "View" link on completion.
|
||||
*
|
||||
* @see admin/class-ideas-browser.php — AJAX handlers.
|
||||
* @see templates/admin/ideas-browser.php — Button markup.
|
||||
*/
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
var config = window.prautobloggerAdmin || {};
|
||||
var pollTimers = {};
|
||||
|
||||
/**
|
||||
* Kick off generation for an idea and start polling.
|
||||
*
|
||||
* @param {jQuery} $cell The table cell containing the button.
|
||||
* @param {number} ideaId Analysis result row ID.
|
||||
*/
|
||||
function generateFromIdea($cell, ideaId) {
|
||||
$cell.html(
|
||||
'<span class="dashicons dashicons-update ab-spin"></span> ' +
|
||||
'<span class="prab-idea-stage">Starting…</span>'
|
||||
);
|
||||
|
||||
$.ajax({
|
||||
url: config.ajaxUrl,
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'prautoblogger_generate_from_idea',
|
||||
nonce: config.ideaGenNonce,
|
||||
idea_id: ideaId
|
||||
},
|
||||
timeout: 15000,
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
pollIdeaStatus($cell, ideaId);
|
||||
} else {
|
||||
showIdeaError($cell, ideaId, response.data && response.data.message || 'Failed to start.');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showIdeaError($cell, ideaId, 'Request failed.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the per-idea status endpoint until generation completes.
|
||||
*
|
||||
* @param {jQuery} $cell The table cell to update.
|
||||
* @param {number} ideaId Analysis result row ID.
|
||||
*/
|
||||
function pollIdeaStatus($cell, ideaId) {
|
||||
pollTimers[ideaId] = setInterval(function () {
|
||||
$.ajax({
|
||||
url: config.ajaxUrl,
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'prautoblogger_idea_gen_status',
|
||||
nonce: config.ideaGenNonce,
|
||||
idea_id: ideaId
|
||||
},
|
||||
timeout: 10000,
|
||||
success: function (response) {
|
||||
if (!response.success) return;
|
||||
var d = response.data;
|
||||
|
||||
if (d.status === 'running') {
|
||||
$cell.find('.prab-idea-stage').text(d.stage || 'Generating…');
|
||||
return;
|
||||
}
|
||||
|
||||
// Terminal — stop polling.
|
||||
clearInterval(pollTimers[ideaId]);
|
||||
delete pollTimers[ideaId];
|
||||
|
||||
if (d.status === 'complete') {
|
||||
showIdeaComplete($cell, ideaId, d);
|
||||
} else if (d.status === 'error') {
|
||||
showIdeaError($cell, ideaId, d.message || 'Generation failed.');
|
||||
} else {
|
||||
showIdeaError($cell, ideaId, 'Unknown status.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the completion state with a View link.
|
||||
*
|
||||
* @param {jQuery} $cell The table cell.
|
||||
* @param {number} ideaId Analysis result row ID.
|
||||
* @param {Object} data Server response data.
|
||||
*/
|
||||
function showIdeaComplete($cell, ideaId, data) {
|
||||
var html = '<span class="dashicons dashicons-yes-alt" style="color:#00a32a;"></span> ';
|
||||
html += '<span style="color:#00a32a;">Done</span>';
|
||||
|
||||
if (data.post_id) {
|
||||
var editUrl = config.adminUrl + 'post.php?post=' + data.post_id + '&action=edit';
|
||||
var viewUrl = config.adminUrl + '?p=' + data.post_id;
|
||||
html += ' — <a href="' + editUrl + '">Edit</a>';
|
||||
html += ' | <a href="' + viewUrl + '" target="_blank">View</a>';
|
||||
}
|
||||
|
||||
if (data.cost > 0) {
|
||||
html += '<div style="font-size:11px;color:#666;margin-top:2px;">$' + parseFloat(data.cost).toFixed(4) + '</div>';
|
||||
}
|
||||
|
||||
$cell.html(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an error state with a retry button.
|
||||
*
|
||||
* @param {jQuery} $cell The table cell.
|
||||
* @param {number} ideaId Analysis result row ID.
|
||||
* @param {string} message Error message.
|
||||
*/
|
||||
function showIdeaError($cell, ideaId, message) {
|
||||
$cell.html(
|
||||
'<span class="dashicons dashicons-warning" style="color:#d63638;"></span> ' +
|
||||
'<span style="color:#d63638; font-size:12px;">' + $('<span>').text(message).html() + '</span>' +
|
||||
'<br><button type="button" class="button button-small prab-gen-idea-btn" data-idea-id="' + ideaId + '" style="margin-top:4px;">Retry</button>'
|
||||
);
|
||||
}
|
||||
|
||||
// Click handler for Generate / Retry buttons (event delegation).
|
||||
$(document).on('click', '.prab-gen-idea-btn', function () {
|
||||
var $btn = $(this);
|
||||
if ($btn.prop('disabled')) return;
|
||||
|
||||
var ideaId = $btn.data('idea-id');
|
||||
var $cell = $btn.closest('td');
|
||||
|
||||
generateFromIdea($cell, ideaId);
|
||||
});
|
||||
|
||||
// On page load, resume polling for any ideas that are mid-generation.
|
||||
$(function () {
|
||||
$('.prab-idea-gen-cell[data-status="running"]').each(function () {
|
||||
var $cell = $(this);
|
||||
var ideaId = $cell.data('idea-id');
|
||||
$cell.html(
|
||||
'<span class="dashicons dashicons-update ab-spin"></span> ' +
|
||||
'<span class="prab-idea-stage">Resuming…</span>'
|
||||
);
|
||||
pollIdeaStatus($cell, ideaId);
|
||||
});
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
@@ -72,11 +72,13 @@ class PRAutoBlogger_Admin_Page {
|
||||
|
||||
wp_localize_script( 'prautoblogger-admin', 'prautobloggerAdmin', [
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'adminUrl' => admin_url(),
|
||||
'generateNonce' => wp_create_nonce( 'prautoblogger_generate_now' ),
|
||||
'imageNonce' => wp_create_nonce( 'prautoblogger_generate_image' ),
|
||||
'testNonce' => wp_create_nonce( 'prautoblogger_test_connection' ),
|
||||
'modelsNonce' => wp_create_nonce( 'prautoblogger_get_models' ),
|
||||
'reviewNonce' => wp_create_nonce( 'prautoblogger_review_queue' ),
|
||||
'ideaGenNonce' => wp_create_nonce( 'prautoblogger_idea_gen' ),
|
||||
'generatingText' => __( 'Generating...', 'prautoblogger' ),
|
||||
'generateText' => __( 'Generate Now', 'prautoblogger' ),
|
||||
'testingText' => __( 'Testing...', 'prautoblogger' ),
|
||||
@@ -84,6 +86,11 @@ class PRAutoBlogger_Admin_Page {
|
||||
// Image models for the picker — defined in the settings class to keep this file short.
|
||||
'imageModels' => PRAutoBlogger_Settings_Fields_Extended::get_image_models(),
|
||||
] );
|
||||
|
||||
// Ideas browser: generate-from-idea JS (only on the Ideas page).
|
||||
if ( 'prautoblogger_page_prautoblogger-ideas' === $hook_suffix ) {
|
||||
wp_enqueue_script( 'prautoblogger-ideas-browser', PRAUTOBLOGGER_PLUGIN_URL . 'assets/js/ideas-browser.js', [ 'jquery', 'prautoblogger-admin' ], PRAUTOBLOGGER_VERSION, true );
|
||||
}
|
||||
}
|
||||
|
||||
/** Render the settings page (delegates to template). */
|
||||
|
||||
@@ -2,30 +2,25 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Admin page listing analysis results (article ideas) from all pipeline runs.
|
||||
*
|
||||
* Shows every idea the content analyzer has surfaced: topic, type, relevance
|
||||
* score, frequency, source, and metadata (key points, keywords). Filterable
|
||||
* by type and source, with pagination. Gives the CEO a live view of the idea
|
||||
* pipeline without needing to trigger a generation run.
|
||||
* Admin page listing analysis results (article ideas) with per-idea generation.
|
||||
*
|
||||
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
|
||||
* Dependencies: WordPress $wpdb for direct table queries.
|
||||
* Dependencies: WordPress $wpdb, Article_Worker, Cost_Tracker, Generation_Lock.
|
||||
*
|
||||
* @see core/class-content-analyzer.php — Produces the rows this page displays.
|
||||
* @see models/class-analysis-result.php — Data model for analysis results.
|
||||
* @see core/class-article-worker.php — Generates articles from ideas.
|
||||
* @see templates/admin/ideas-browser.php — Renders the HTML.
|
||||
* @see ARCHITECTURE.md — Data flow step 2.
|
||||
*/
|
||||
class PRAutoBlogger_Ideas_Browser {
|
||||
|
||||
private const PER_PAGE = 30;
|
||||
|
||||
/**
|
||||
* Register the Ideas submenu page under PRAutoBlogger.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
/** Transient prefix for per-idea generation status. */
|
||||
private const STATUS_PREFIX = 'prab_idea_gen_';
|
||||
|
||||
/** How long to keep per-idea status (seconds). */
|
||||
private const STATUS_TTL = 600;
|
||||
|
||||
/** Register the Ideas submenu page under PRAutoBlogger. */
|
||||
public function on_register_menu(): void {
|
||||
add_submenu_page(
|
||||
'prautoblogger-settings',
|
||||
@@ -37,14 +32,7 @@ class PRAutoBlogger_Ideas_Browser {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the ideas browser page.
|
||||
*
|
||||
* Reads filter/pagination parameters from $_GET, queries the analysis_results
|
||||
* table, and includes the template.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
/** Render the ideas browser page. */
|
||||
public function render_page(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
@@ -52,9 +40,8 @@ class PRAutoBlogger_Ideas_Browser {
|
||||
|
||||
$paged = isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1;
|
||||
$type_filter = isset( $_GET['type'] ) ? sanitize_key( $_GET['type'] ) : '';
|
||||
$source_filter = isset( $_GET['source'] ) ? sanitize_key( $_GET['source'] ) : '';
|
||||
|
||||
$result = $this->query_ideas( $paged, $type_filter, $source_filter );
|
||||
$result = $this->query_ideas( $paged, $type_filter );
|
||||
$rows = $result['rows'];
|
||||
$total = $result['total'];
|
||||
$total_pages = (int) ceil( $total / self::PER_PAGE );
|
||||
@@ -64,16 +51,202 @@ class PRAutoBlogger_Ideas_Browser {
|
||||
}
|
||||
|
||||
/**
|
||||
* Query analysis results with optional filtering and pagination.
|
||||
* AJAX: trigger article generation from a specific idea.
|
||||
*
|
||||
* Side effects: database SELECT.
|
||||
* Loads the analysis result, stores it as a transient, sets a per-idea
|
||||
* status transient, and schedules a one-shot cron event so the actual
|
||||
* generation runs in a separate PHP process (Hostinger 120s limit).
|
||||
*
|
||||
* @param int $paged Page number (1-based).
|
||||
* @param string $type Filter by analysis_type (empty = all).
|
||||
* @param string $source Filter: 'llm_research' or 'reddit' based on source_ids content.
|
||||
* @return array{rows: array[], total: int}
|
||||
* Side effects: transient writes, cron scheduling.
|
||||
*/
|
||||
private function query_ideas( int $paged, string $type, string $source ): array {
|
||||
public function on_ajax_generate_from_idea(): void {
|
||||
check_ajax_referer( 'prautoblogger_idea_gen', 'nonce' );
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => 'Insufficient permissions.' ], 403 );
|
||||
return;
|
||||
}
|
||||
|
||||
$idea_id = absint( $_POST['idea_id'] ?? 0 );
|
||||
if ( $idea_id < 1 ) {
|
||||
wp_send_json_error( [ 'message' => 'Invalid idea ID.' ] );
|
||||
return;
|
||||
}
|
||||
|
||||
// Build Article_Idea from the analysis row.
|
||||
$idea_data = self::load_idea_data( $idea_id );
|
||||
if ( null === $idea_data ) {
|
||||
wp_send_json_error( [ 'message' => 'Idea not found.' ] );
|
||||
return;
|
||||
}
|
||||
|
||||
// Store idea for the cron handler and set initial "running" status.
|
||||
set_transient( self::STATUS_PREFIX . 'data_' . $idea_id, $idea_data, self::STATUS_TTL );
|
||||
set_transient( self::STATUS_PREFIX . $idea_id, [
|
||||
'status' => 'running',
|
||||
'stage' => __( 'Starting generation…', 'prautoblogger' ),
|
||||
'started' => time(),
|
||||
], self::STATUS_TTL );
|
||||
|
||||
// Schedule background cron — passes idea_id as argument.
|
||||
$hook = 'prautoblogger_generate_from_idea';
|
||||
if ( ! wp_next_scheduled( $hook, [ $idea_id ] ) ) {
|
||||
wp_schedule_single_event( time(), $hook, [ $idea_id ] );
|
||||
}
|
||||
spawn_cron();
|
||||
wp_remote_post(
|
||||
site_url( 'wp-cron.php?doing_wp_cron=' . sprintf( '%.22F', microtime( true ) ) ),
|
||||
[ 'timeout' => 0.01, 'blocking' => false, 'sslverify' => false ]
|
||||
);
|
||||
|
||||
wp_send_json_success( [ 'message' => 'Generation started.' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: return per-idea generation status for frontend polling.
|
||||
*
|
||||
* Side effects: none (reads transient only).
|
||||
*/
|
||||
public function on_ajax_idea_gen_status(): void {
|
||||
check_ajax_referer( 'prautoblogger_idea_gen', 'nonce' );
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => 'Insufficient permissions.' ], 403 );
|
||||
return;
|
||||
}
|
||||
|
||||
$idea_id = absint( $_POST['idea_id'] ?? 0 );
|
||||
$status = get_transient( self::STATUS_PREFIX . $idea_id );
|
||||
if ( ! is_array( $status ) ) {
|
||||
wp_send_json_success( [ 'status' => 'idle' ] );
|
||||
return;
|
||||
}
|
||||
|
||||
wp_send_json_success( $status );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cron handler: generate a single article from a stored idea.
|
||||
*
|
||||
* Runs in a background PHP process. Acquires the generation lock,
|
||||
* runs Article_Worker, and writes the result to the per-idea status transient.
|
||||
*
|
||||
* Side effects: LLM API calls, DB writes, post creation, cost logging.
|
||||
*
|
||||
* @param int $idea_id Analysis result row ID.
|
||||
*/
|
||||
public static function on_cron_generate_from_idea( int $idea_id ): void {
|
||||
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
@ignore_user_abort( true );
|
||||
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
@set_time_limit( 300 );
|
||||
|
||||
$key = self::STATUS_PREFIX . $idea_id;
|
||||
$idea_data = get_transient( self::STATUS_PREFIX . 'data_' . $idea_id );
|
||||
if ( ! is_array( $idea_data ) ) {
|
||||
set_transient( $key, [ 'status' => 'error', 'message' => 'Idea data expired.' ], self::STATUS_TTL );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! PRAutoBlogger_Generation_Lock::acquire() ) {
|
||||
set_transient( $key, [ 'status' => 'error', 'message' => 'Another generation is running.' ], self::STATUS_TTL );
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$idea = new PRAutoBlogger_Article_Idea( $idea_data );
|
||||
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
|
||||
$cost_tracker->set_run_id( wp_generate_uuid4() );
|
||||
|
||||
self::update_idea_stage( $idea_id, __( 'Generating article draft…', 'prautoblogger' ) );
|
||||
$worker = new PRAutoBlogger_Article_Worker( $cost_tracker );
|
||||
$result = $worker->generate( $idea );
|
||||
|
||||
// Amortize any research costs for this single-article run.
|
||||
PRAutoBlogger_Post_Assembler::amortize_research_costs( $cost_tracker->get_run_id() );
|
||||
|
||||
// Find the generated post ID for the "View" link.
|
||||
$post_id = self::find_post_by_run_id( $cost_tracker->get_run_id() );
|
||||
|
||||
set_transient( $key, [
|
||||
'status' => 'complete',
|
||||
'generated' => $result['generated'],
|
||||
'published' => $result['published'],
|
||||
'cost' => $result['cost'],
|
||||
'post_id' => $post_id,
|
||||
], self::STATUS_TTL );
|
||||
} catch ( \Throwable $e ) {
|
||||
PRAutoBlogger_Logger::instance()->error(
|
||||
sprintf( 'Idea generation %s for #%d: %s', get_class( $e ), $idea_id, $e->getMessage() ),
|
||||
'pipeline'
|
||||
);
|
||||
set_transient( $key, [ 'status' => 'error', 'message' => $e->getMessage() ], self::STATUS_TTL );
|
||||
}
|
||||
|
||||
PRAutoBlogger_Generation_Lock::release();
|
||||
delete_transient( self::STATUS_PREFIX . 'data_' . $idea_id );
|
||||
}
|
||||
|
||||
// ── Private helpers ─────────────────────────────────────────────────
|
||||
|
||||
/** Update the per-idea status transient with a stage message. */
|
||||
private static function update_idea_stage( int $idea_id, string $stage ): void {
|
||||
$key = self::STATUS_PREFIX . $idea_id;
|
||||
$current = get_transient( $key );
|
||||
if ( is_array( $current ) ) {
|
||||
$current['stage'] = $stage;
|
||||
set_transient( $key, $current, self::STATUS_TTL );
|
||||
}
|
||||
}
|
||||
|
||||
/** Find the most recent post created by a specific run_id. */
|
||||
private static function find_post_by_run_id( string $run_id ): ?int {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'prautoblogger_generation_log';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$post_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT DISTINCT post_id FROM {$table} WHERE run_id = %s AND post_id IS NOT NULL LIMIT 1",
|
||||
$run_id
|
||||
)
|
||||
);
|
||||
return null !== $post_id ? (int) $post_id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an analysis result row and map it to Article_Idea constructor data.
|
||||
*
|
||||
* @param int $idea_id Analysis result row ID.
|
||||
* @return array<string, mixed>|null Article_Idea-compatible array, or null.
|
||||
*/
|
||||
private static function load_idea_data( int $idea_id ): ?array {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'prautoblogger_analysis_results';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $idea_id ),
|
||||
ARRAY_A
|
||||
);
|
||||
if ( ! $row ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$meta = json_decode( $row['metadata_json'] ?? '{}', true ) ?: [];
|
||||
return [
|
||||
'topic' => $row['topic'],
|
||||
'article_type' => $row['analysis_type'],
|
||||
'suggested_title' => $meta['suggested_title'] ?? $row['topic'],
|
||||
'summary' => $row['summary'] ?? '',
|
||||
'score' => (float) $row['relevance_score'],
|
||||
'analysis_id' => (int) $row['id'],
|
||||
'source_ids' => json_decode( $row['source_ids_json'] ?? '[]', true ) ?: [],
|
||||
'key_points' => $meta['key_points'] ?? [],
|
||||
'target_keywords' => $meta['target_keywords'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
/** Query analysis results with optional filtering and pagination. */
|
||||
private function query_ideas( int $paged, string $type ): array {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'prautoblogger_analysis_results';
|
||||
$where = [];
|
||||
@@ -103,19 +276,10 @@ class PRAutoBlogger_Ideas_Browser {
|
||||
? $wpdb->get_results( $full_sql, ARRAY_A )
|
||||
: $wpdb->get_results( $wpdb->prepare( $full_sql, ...$params ), ARRAY_A );
|
||||
|
||||
return [
|
||||
'rows' => $rows ?: [],
|
||||
'total' => $total,
|
||||
];
|
||||
return [ 'rows' => $rows ?: [], 'total' => $total ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get counts per analysis_type for the filter badges.
|
||||
*
|
||||
* Side effects: database SELECT.
|
||||
*
|
||||
* @return array<string, int> Map of analysis_type => count.
|
||||
*/
|
||||
/** Get counts per analysis_type for the filter badges. */
|
||||
private function get_type_counts(): array {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'prautoblogger_analysis_results';
|
||||
|
||||
@@ -73,7 +73,8 @@ class PRAutoBlogger {
|
||||
$review_queue = new PRAutoBlogger_Review_Queue();
|
||||
add_action( 'admin_menu', [ $review_queue, 'on_register_menu' ] );
|
||||
|
||||
add_action( 'admin_menu', [ new PRAutoBlogger_Ideas_Browser(), 'on_register_menu' ] );
|
||||
$ideas_browser = new PRAutoBlogger_Ideas_Browser();
|
||||
add_action( 'admin_menu', [ $ideas_browser, 'on_register_menu' ] );
|
||||
add_action( 'admin_menu', [ new PRAutoBlogger_Log_Viewer(), 'on_register_menu' ] );
|
||||
|
||||
( new PRAutoBlogger_Post_List_Columns() )->register();
|
||||
@@ -107,6 +108,9 @@ class PRAutoBlogger {
|
||||
|
||||
$registry = $this->executor->get_model_registry();
|
||||
add_action( 'prautoblogger_refresh_model_registry', [ $registry, 'refresh' ] );
|
||||
|
||||
// Single-idea generation from the Ideas browser page.
|
||||
add_action( 'prautoblogger_generate_from_idea', [ 'PRAutoBlogger_Ideas_Browser', 'on_cron_generate_from_idea' ] );
|
||||
}
|
||||
|
||||
/** Register AJAX handlers for admin actions. */
|
||||
@@ -125,6 +129,10 @@ class PRAutoBlogger {
|
||||
add_action( 'wp_ajax_prautoblogger_reject_post', [ $review_queue, 'on_ajax_reject_post' ] );
|
||||
|
||||
add_action( 'wp_ajax_prautoblogger_clear_logs', [ new PRAutoBlogger_Log_Viewer(), 'on_ajax_clear_logs' ] );
|
||||
|
||||
$ideas = new PRAutoBlogger_Ideas_Browser();
|
||||
add_action( 'wp_ajax_prautoblogger_generate_from_idea', [ $ideas, 'on_ajax_generate_from_idea' ] );
|
||||
add_action( 'wp_ajax_prautoblogger_idea_gen_status', [ $ideas, 'on_ajax_idea_gen_status' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ declare(strict_types=1);
|
||||
* Plugin Name: PRAutoBlogger
|
||||
* Plugin URI: https://peptiderepo.com/prautoblogger
|
||||
* Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics.
|
||||
* Version: 0.5.3
|
||||
* Version: 0.6.0
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* Author: PeptideRepo
|
||||
@@ -35,7 +35,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
| and limits without magic strings.
|
||||
*/
|
||||
|
||||
define( 'PRAUTOBLOGGER_VERSION', '0.5.3' );
|
||||
define( 'PRAUTOBLOGGER_VERSION', '0.6.0' );
|
||||
define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
@@ -19,6 +19,10 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
|
||||
$base_url = admin_url( 'admin.php?page=prautoblogger-ideas' );
|
||||
?>
|
||||
<style>
|
||||
.ab-spin { animation: ab-spin 1s linear infinite; display: inline-block; }
|
||||
@keyframes ab-spin { 100% { transform: rotate(360deg); } }
|
||||
</style>
|
||||
<div class="wrap prautoblogger-ideas-browser">
|
||||
<h1><?php esc_html_e( 'PRAutoBlogger — Ideas', 'prautoblogger' ); ?></h1>
|
||||
<p class="description">
|
||||
@@ -50,35 +54,41 @@ $base_url = admin_url( 'admin.php?page=prautoblogger-ideas' );
|
||||
|
||||
<?php if ( empty( $rows ) ) : ?>
|
||||
<div class="prautoblogger-empty-queue">
|
||||
<p><?php esc_html_e( 'No ideas found. Ideas will appear here after the next generation run collects and analyzes source data.', 'prautoblogger' ); ?></p>
|
||||
<p><?php esc_html_e( 'No ideas found. Ideas will appear here after the next generation run.', 'prautoblogger' ); ?></p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<table class="widefat striped prautoblogger-ideas-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30%"><?php esc_html_e( 'Title / Topic', 'prautoblogger' ); ?></th>
|
||||
<th style="width:8%"><?php esc_html_e( 'Type', 'prautoblogger' ); ?></th>
|
||||
<th style="width:28%"><?php esc_html_e( 'Title / Topic', 'prautoblogger' ); ?></th>
|
||||
<th style="width:7%"><?php esc_html_e( 'Type', 'prautoblogger' ); ?></th>
|
||||
<th style="width:6%"><?php esc_html_e( 'Score', 'prautoblogger' ); ?></th>
|
||||
<th style="width:6%"><?php esc_html_e( 'Freq', 'prautoblogger' ); ?></th>
|
||||
<th style="width:35%"><?php esc_html_e( 'Key Points / Keywords', 'prautoblogger' ); ?></th>
|
||||
<th style="width:15%"><?php esc_html_e( 'Analyzed', 'prautoblogger' ); ?></th>
|
||||
<th style="width:5%"><?php esc_html_e( 'Freq', 'prautoblogger' ); ?></th>
|
||||
<th style="width:30%"><?php esc_html_e( 'Key Points / Keywords', 'prautoblogger' ); ?></th>
|
||||
<th style="width:10%"><?php esc_html_e( 'Analyzed', 'prautoblogger' ); ?></th>
|
||||
<th style="width:14%"><?php esc_html_e( 'Generate', 'prautoblogger' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $rows as $row ) : ?>
|
||||
<?php
|
||||
$meta = json_decode( $row['metadata_json'] ?? '{}', true ) ?: [];
|
||||
$suggested_title = $meta['suggested_title'] ?? '';
|
||||
$key_points = $meta['key_points'] ?? [];
|
||||
$keywords = $meta['target_keywords'] ?? [];
|
||||
$score_pct = round( (float) $row['relevance_score'] * 100 );
|
||||
$type_label = ucfirst( $row['analysis_type'] ?? 'unknown' );
|
||||
$type_class = 'prab-type-' . sanitize_html_class( $row['analysis_type'] );
|
||||
$meta = json_decode( $row['metadata_json'] ?? '{}', true ) ?: [];
|
||||
$suggested = $meta['suggested_title'] ?? '';
|
||||
$key_points = $meta['key_points'] ?? [];
|
||||
$keywords = $meta['target_keywords'] ?? [];
|
||||
$score_pct = round( (float) $row['relevance_score'] * 100 );
|
||||
$type_label = ucfirst( $row['analysis_type'] ?? 'unknown' );
|
||||
$type_class = 'prab-type-' . sanitize_html_class( $row['analysis_type'] );
|
||||
$idea_id = (int) $row['id'];
|
||||
|
||||
// Check if this idea is already mid-generation.
|
||||
$idea_status = get_transient( 'prab_idea_gen_' . $idea_id );
|
||||
$gen_state = is_array( $idea_status ) ? ( $idea_status['status'] ?? 'idle' ) : 'idle';
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?php echo esc_html( '' !== $suggested_title ? $suggested_title : $row['topic'] ); ?></strong>
|
||||
<?php if ( '' !== $suggested_title && $suggested_title !== $row['topic'] ) : ?>
|
||||
<strong><?php echo esc_html( '' !== $suggested ? $suggested : $row['topic'] ); ?></strong>
|
||||
<?php if ( '' !== $suggested && $suggested !== $row['topic'] ) : ?>
|
||||
<div style="color:#666; font-size:12px; margin-top:2px;">
|
||||
<?php echo esc_html( $row['topic'] ); ?>
|
||||
</div>
|
||||
@@ -95,9 +105,7 @@ $base_url = admin_url( 'admin.php?page=prautoblogger-ideas' );
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$bar_color = $score_pct >= 70 ? '#00a32a' : ( $score_pct >= 40 ? '#dba617' : '#d63638' );
|
||||
?>
|
||||
<?php $bar_color = $score_pct >= 70 ? '#00a32a' : ( $score_pct >= 40 ? '#dba617' : '#d63638' ); ?>
|
||||
<div style="display:flex; align-items:center; gap:6px;">
|
||||
<div style="width:40px; height:6px; background:#ddd; border-radius:3px; overflow:hidden;">
|
||||
<div style="width:<?php echo (int) $score_pct; ?>%; height:100%; background:<?php echo esc_attr( $bar_color ); ?>;"></div>
|
||||
@@ -130,6 +138,24 @@ $base_url = admin_url( 'admin.php?page=prautoblogger-ideas' );
|
||||
<td style="font-size:12px; color:#666;">
|
||||
<?php echo esc_html( wp_date( 'M j, g:i A', strtotime( $row['analyzed_at'] ) ) ); ?>
|
||||
</td>
|
||||
<td class="prab-idea-gen-cell"
|
||||
data-idea-id="<?php echo $idea_id; ?>"
|
||||
data-status="<?php echo esc_attr( $gen_state ); ?>">
|
||||
<?php if ( 'complete' === $gen_state && ! empty( $idea_status['post_id'] ) ) : ?>
|
||||
<span class="dashicons dashicons-yes-alt" style="color:#00a32a;"></span>
|
||||
<a href="<?php echo esc_url( admin_url( 'post.php?post=' . $idea_status['post_id'] . '&action=edit' ) ); ?>">Edit</a>
|
||||
| <a href="<?php echo esc_url( get_permalink( $idea_status['post_id'] ) ); ?>" target="_blank">View</a>
|
||||
<?php elseif ( 'running' === $gen_state ) : ?>
|
||||
<span class="dashicons dashicons-update ab-spin"></span>
|
||||
<span class="prab-idea-stage"><?php echo esc_html( $idea_status['stage'] ?? 'Generating…' ); ?></span>
|
||||
<?php else : ?>
|
||||
<button type="button"
|
||||
class="button button-small prab-gen-idea-btn"
|
||||
data-idea-id="<?php echo $idea_id; ?>">
|
||||
<?php esc_html_e( 'Generate', 'prautoblogger' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user