feat: interactive model picker with searchable popup and pricing
Replace plain text inputs for analysis/writing/editor model fields with a rich model picker that fetches the OpenRouter model registry and displays a searchable popup with model name, provider, input/output pricing per 1M tokens, and context window. Also extracts source status and model field renderers into dedicated field classes to keep class-admin-page.php under the 300-line cap. New files: - assets/js/model-picker.js — popup UI with search, filtering, caching - assets/css/model-picker.css — popup styles matching admin design system - includes/admin/fields/class-open-router-model-field.php — server render - includes/admin/fields/class-source-status-field.php — extracted field Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
78
includes/admin/fields/class-open-router-model-field.php
Normal file
78
includes/admin/fields/class-open-router-model-field.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Renders the interactive model-picker field for OpenRouter models.
|
||||
*
|
||||
* What: a button showing the current model's name + pricing. Clicking it
|
||||
* opens a JS-powered popup (model-picker.js) with a searchable list.
|
||||
* Who calls it: PRAutoBlogger_Admin_Page::render_field() delegates here
|
||||
* for fields with type 'model_select'.
|
||||
* Dependencies: PRAutoBlogger_Model_Registry_Interface (cached registry lookup).
|
||||
*
|
||||
* @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().
|
||||
*/
|
||||
class PRAutoBlogger_OpenRouter_Model_Field {
|
||||
|
||||
/**
|
||||
* Render the model picker trigger button and hidden input.
|
||||
*
|
||||
* @param string $id Field HTML id/name attribute.
|
||||
* @param string $value Current model id (e.g. 'google/gemini-2.5-flash-lite').
|
||||
* @param array<string, mixed> $args Field definition from settings fields.
|
||||
*/
|
||||
public static function render( string $id, string $value, array $args ): void {
|
||||
$capability = $args['capability'] ?? 'text→text';
|
||||
$display_name = $value ?: __( '— Select a model —', 'prautoblogger' );
|
||||
$display_price = '';
|
||||
|
||||
// Try to resolve display name + price from the cached registry.
|
||||
$registry = prautoblogger()->get_executor()->get_model_registry();
|
||||
$model = $registry->find_model( $value );
|
||||
if ( null !== $model ) {
|
||||
$display_name = $model['name'] ?? $value;
|
||||
$in = (float) ( $model['input_price_per_m'] ?? 0 );
|
||||
$out = (float) ( $model['output_price_per_m'] ?? 0 );
|
||||
$display_price = self::format_price( $in ) . ' / ' . self::format_price( $out );
|
||||
}
|
||||
|
||||
// Hidden input carries the actual value for the form submission.
|
||||
printf(
|
||||
'<input type="hidden" id="%s" name="%s" value="%s" />',
|
||||
esc_attr( $id ),
|
||||
esc_attr( $id ),
|
||||
esc_attr( $value )
|
||||
);
|
||||
|
||||
// Trigger button — opens the model picker popup via JS.
|
||||
printf(
|
||||
'<button type="button" class="ab-mp-trigger" data-field-id="%s" data-capability="%s">'
|
||||
. '<span class="ab-mp-display-name">%s</span>'
|
||||
. '<span class="ab-mp-display-price">%s</span>'
|
||||
. '<span class="ab-mp-display-arrow">▾</span>'
|
||||
. '</button>',
|
||||
esc_attr( $id ),
|
||||
esc_attr( $capability ),
|
||||
esc_html( $display_name ),
|
||||
esc_html( $display_price )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a price-per-million-tokens value for display.
|
||||
*
|
||||
* @param float $price Price per 1M tokens.
|
||||
* @return string Formatted price string like "$0.25" or "Free".
|
||||
*/
|
||||
private static function format_price( float $price ): string {
|
||||
if ( $price <= 0 ) {
|
||||
return __( 'Free', 'prautoblogger' );
|
||||
}
|
||||
if ( $price < 0.01 ) {
|
||||
return '$' . number_format( $price, 4 );
|
||||
}
|
||||
return '$' . number_format( $price, 2 );
|
||||
}
|
||||
}
|
||||
66
includes/admin/fields/class-source-status-field.php
Normal file
66
includes/admin/fields/class-source-status-field.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Renders the Reddit source status indicator in the admin settings.
|
||||
*
|
||||
* What: shows RSS (primary) and .json (fallback) availability with
|
||||
* last-collection timestamp.
|
||||
* Who calls it: PRAutoBlogger_Admin_Page::render_field() delegates here
|
||||
* for fields with type 'source_status'.
|
||||
* Dependencies: None — reads wp_options only.
|
||||
*
|
||||
* @see admin/class-admin-page.php — Calls render() from the main field switch.
|
||||
*/
|
||||
class PRAutoBlogger_Source_Status_Field {
|
||||
|
||||
/**
|
||||
* Render the Reddit source status indicator.
|
||||
*
|
||||
* Side effects: reads wp_options for last collection time.
|
||||
*/
|
||||
public static function render(): void {
|
||||
$last_collection = get_option( 'prautoblogger_last_collection_time', '' );
|
||||
$last_source_used = get_option( 'prautoblogger_last_source_used', '' );
|
||||
|
||||
echo '<div class="ab-source-status">';
|
||||
|
||||
// Reddit RSS — primary source.
|
||||
printf(
|
||||
'<div class="ab-source-row">'
|
||||
. '<span class="ab-source-dot ab-status-ok"></span>'
|
||||
. '<strong>%s</strong> <span class="ab-source-badge">%s</span>'
|
||||
. '<span class="ab-source-label">%s</span>'
|
||||
. '</div>',
|
||||
esc_html__( 'Reddit RSS', 'prautoblogger' ),
|
||||
esc_html__( 'Primary', 'prautoblogger' ),
|
||||
esc_html__( 'Reliable from all IPs', 'prautoblogger' )
|
||||
);
|
||||
|
||||
// Reddit .json — fallback source.
|
||||
printf(
|
||||
'<div class="ab-source-row">'
|
||||
. '<span class="ab-source-dot ab-status-ok"></span>'
|
||||
. '<strong>%s</strong> <span class="ab-source-badge ab-badge-secondary">%s</span>'
|
||||
. '<span class="ab-source-label">%s</span>'
|
||||
. '</div>',
|
||||
esc_html__( 'Reddit .json', 'prautoblogger' ),
|
||||
esc_html__( 'Fallback + Comments', 'prautoblogger' ),
|
||||
esc_html__( 'May be IP-blocked on some hosts', 'prautoblogger' )
|
||||
);
|
||||
|
||||
// Last collection info.
|
||||
if ( '' !== $last_collection ) {
|
||||
$time_ago = human_time_diff( (int) $last_collection, time() );
|
||||
printf(
|
||||
'<div class="ab-source-meta">%s <strong>%s</strong> %s</div>',
|
||||
esc_html__( 'Last collection:', 'prautoblogger' ),
|
||||
/* translators: %s is a human-readable time difference like "2 hours" */
|
||||
esc_html( sprintf( __( '%s ago', 'prautoblogger' ), $time_ago ) ),
|
||||
'' !== $last_source_used ? esc_html( sprintf( __( 'via %s', 'prautoblogger' ), $last_source_used ) ) : ''
|
||||
);
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user