diff --git a/CHANGELOG.md b/CHANGELOG.md old mode 100755 new mode 100644 index d3db336..0df268b --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ 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.11.0] — 2026-04-23 + +### Added +- **OpenRouter model picker for text models.** Admin-facing dropdown field type (`model_select`) with searchable list, capability filtering, and estimated cost preview. Replaces free-text model slug inputs on the three text model settings (analysis, writing, editor). Includes daily cron refresh of the OpenRouter registry cache and manual "Refresh Model List" button on the AI Models tab. + +- **Cost preview in model picker.** Shows estimated cost per generation based on historical token usage for the 30 days prior. Maps each setting to its constituent pipeline stages (writing model = outline + draft + polish). + +- **AJAX refresh endpoint** (`prautoblogger_refresh_models`) for manual model registry refresh with nonce + capability gating (`manage_options`). + +- **New public method on Cost Tracker:** `get_avg_tokens_for_stages(array $stages, int $days = 30): array` returns average input/output token counts for a stage list, used by cost preview. + +### Changed +- **AI Models tab now renders a "Refresh Model List" button** next to the panel title. Triggers `prautoblogger_refresh_models` AJAX endpoint. + ## [0.10.1] — 2026-04-23 ### Fixed diff --git a/CONVENTIONS.md b/CONVENTIONS.md old mode 100755 new mode 100644 index 9a53e8c..9cb417f --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -234,6 +234,60 @@ Frontend components use `wp.element` (WordPress-bundled React) — no JSX, no bu 9. **No `echo` of raw data** — always escape with `esc_html()`, `esc_attr()`, etc. 10. **Nonce on every form/AJAX** — `wp_nonce_field()` / `check_ajax_referer()` + +## How To: Add an OpenRouter model picker field (v0.11.0+) + +Use the `model_select` field type to let admins choose OpenRouter models with a searchable dropdown, capability filtering, and estimated cost preview based on usage history. + +### 1. Declare the field in `class-settings-fields.php` + +```php +[ + 'id' => 'my_plugin_model', + 'label' => __( 'My Model Setting', 'my-plugin' ), + 'type' => 'model_select', + 'section' => 'my_plugin_settings', + 'default' => 'anthropic/claude-3.5-haiku', + 'capability' => 'text→text', // see ARCHITECTURE.md §18 for all capability strings + 'description' => __( 'Choose a model...', 'my-plugin' ), + 'badge' => __( 'Quality', 'my-plugin' ), +], +``` + +### 2. Map the setting to pipeline stages (for cost preview) + +If you want cost preview to work, add your setting ID and stage list to the `get_stages_for_setting()` method in `includes/admin/fields/class-open-router-model-field.php`: + +```php +private static function get_stages_for_setting( string $field_id ): array { + $map = [ + 'prautoblogger_analysis_model' => [ 'analysis' ], + 'prautoblogger_writing_model' => [ 'outline', 'draft', 'polish' ], + 'prautoblogger_editor_model' => [ 'review' ], + 'my_new_model_setting' => [ 'my_stage' ], // ADD HERE + ]; + return $map[ $field_id ] ?? []; +} +``` + +The cost preview queries `get_avg_tokens_for_stages()` on the Cost Tracker to show estimated cost per generation based on the last 30 days of usage. + +### 3. That's it + +The field renderer, AJAX refresh, model picker JS, and capability filtering are already wired. The admin will see a "Refresh Model List" button on the AI Models tab (or whichever tab contains the picker field). + +### Capability strings (locked in v0.11.0) + +- `text→text` — text input, text output (default for analysis/writing/editor) +- `text+image→text` — vision models +- `text+audio→text` — audio input models +- `text→image` — image generation +- `text→audio` — voice synthesis +- `text→video` — video generation +- `text→embedding` — embeddings + +Phase 3 will add provider selection; v0.11.0 is OpenRouter-only. + ## Git Workflow — PR-Gated, Soft-Enforced This repo is private on GitHub's free plan, which does not support branch protection or rulesets. The review gate is enforced at the agent layer, not server-side. Every agent and human contributor follows these rules; the `.github/workflows/main-push-audit.yml` tripwire opens an audit issue on any direct push to `main` that did not come from a merged PR. diff --git a/assets/admin/peptiderepo-model-picker.css b/assets/admin/peptiderepo-model-picker.css new file mode 100644 index 0000000..5a7584f --- /dev/null +++ b/assets/admin/peptiderepo-model-picker.css @@ -0,0 +1,6 @@ +/** + * Styling for model registry picker refresh functionality. + * + * Button animations and toast notifications are defined in admin.css. + * This file is reserved for model-picker-specific styles (future extensibility). + */ diff --git a/assets/admin/peptiderepo-model-picker.js b/assets/admin/peptiderepo-model-picker.js new file mode 100644 index 0000000..eb92ba8 --- /dev/null +++ b/assets/admin/peptiderepo-model-picker.js @@ -0,0 +1,58 @@ +/** + * Model registry refresh handler for PRAutoBlogger admin settings. + * + * Handles the "Refresh Model List" button click in the AI Models admin tab. + * Sends AJAX request to refresh the OpenRouter model registry cache and shows + * success/error toast notifications. + * + * @see admin/class-admin-page.php — Renders the refresh button + nonce. + * @see ajax/class-model-registry-refresh.php — Server-side handler. + */ +(function ($) { + 'use strict'; + + // Refresh model registry button. + $(document).on('click', '#prautoblogger-refresh-models', function (e) { + e.preventDefault(); + var $btn = $(this); + var nonce = $btn.data('nonce'); + var originalText = $btn.html(); + + $btn.prop('disabled', true).addClass('ab-btn-loading'); + + $.ajax({ + url: ajaxurl, + method: 'POST', + data: { + action: 'prautoblogger_refresh_models', + nonce: nonce + }, + timeout: 30000 + }) + .done(function (response) { + if (response.success && response.data && response.data.message) { + // Show success toast + var $notice = $('
') + .html(' ' + response.data.message) + .insertBefore('.ab-layout') + .delay(4000) + .fadeOut(function () { + $(this).remove(); + }); + } + }) + .fail(function () { + var $notice = $('') + .html(' ' + 'Failed to refresh model list. Please try again.') + .insertBefore('.ab-layout') + .delay(5000) + .fadeOut(function () { + $(this).remove(); + }); + }) + .always(function () { + $btn.prop('disabled', false).removeClass('ab-btn-loading').html(originalText); + }); + }); + +})(jQuery); diff --git a/assets/js/admin.js b/assets/js/admin.js old mode 100755 new mode 100644 diff --git a/includes/admin/class-admin-page.php b/includes/admin/class-admin-page.php index b29ed8e..59e90dc 100755 --- a/includes/admin/class-admin-page.php +++ b/includes/admin/class-admin-page.php @@ -75,6 +75,8 @@ class PRAutoBlogger_Admin_Page { wp_enqueue_style( 'prautoblogger-model-picker', PRAUTOBLOGGER_PLUGIN_URL . 'assets/css/model-picker.css', array( 'prautoblogger-admin' ), PRAUTOBLOGGER_VERSION ); wp_enqueue_script( 'prautoblogger-admin', PRAUTOBLOGGER_PLUGIN_URL . 'assets/js/admin.js', array( 'jquery' ), PRAUTOBLOGGER_VERSION, true ); wp_enqueue_script( 'prautoblogger-model-picker', PRAUTOBLOGGER_PLUGIN_URL . 'assets/js/model-picker.js', array( 'jquery', 'prautoblogger-admin' ), PRAUTOBLOGGER_VERSION, true ); + wp_enqueue_script( 'peptiderepo-model-picker', PRAUTOBLOGGER_PLUGIN_URL . 'assets/admin/peptiderepo-model-picker.js', array( 'jquery' ), PRAUTOBLOGGER_VERSION, true ); + wp_enqueue_style( 'peptiderepo-model-picker', PRAUTOBLOGGER_PLUGIN_URL . 'assets/admin/peptiderepo-model-picker.css', array(), PRAUTOBLOGGER_VERSION ); wp_localize_script( 'prautoblogger-admin', diff --git a/includes/admin/fields/class-open-router-model-field.php b/includes/admin/fields/class-open-router-model-field.php index e4750a8..7b71b76 100644 --- a/includes/admin/fields/class-open-router-model-field.php +++ b/includes/admin/fields/class-open-router-model-field.php @@ -8,13 +8,16 @@ declare(strict_types=1); * * What: a button showing the current model's name + pricing. Clicking it * opens a JS-powered popup (model-picker.js) with a searchable list. + * Includes an estimated cost preview below based on historical usage. * Who calls it: PRAutoBlogger_Admin_Page::render_field() delegates here * for fields with type 'model_select'. - * Dependencies: PRAutoBlogger_Model_Registry_Interface (cached registry lookup). + * Dependencies: PRAutoBlogger_Model_Registry_Interface (cached registry lookup), + * PRAutoBlogger_Cost_Tracker (historical token averages). * * @see admin/class-admin-page.php — Calls render() from the main field switch. * @see assets/js/model-picker.js — JS popup that fetches + displays models. * @see services/interface-model-registry.php — Registry interface for find_model(). + * @see core/class-cost-tracker.php — get_avg_tokens_for_stages() for cost preview. */ class PRAutoBlogger_OpenRouter_Model_Field { @@ -71,6 +74,75 @@ class PRAutoBlogger_OpenRouter_Model_Field { esc_html( $display_name ), esc_html( $display_price ) ); + + // Cost preview panel — only for text→text models (not image). + if ( 'image_generation' !== $capability && '' !== $value ) { + $cost_preview = self::get_cost_preview( $id, $value ); + if ( $cost_preview ) { + printf( + '