Files
wp-agentic-writer/includes/class-settings-v2.php
2026-01-28 00:26:00 +07:00

1155 lines
38 KiB
PHP

<?php
/**
* Settings Page V2
*
* Refactored settings page with Bootstrap design and separated view files.
*
* @package WP_Agentic_Writer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class WP_Agentic_Writer_Settings_V2
*
* @since 0.2.0
*/
class WP_Agentic_Writer_Settings_V2 {
/**
* Get singleton instance.
*
* @since 0.2.0
* @return WP_Agentic_Writer_Settings_V2
*/
public static function get_instance() {
static $instance = null;
if ( null === $instance ) {
$instance = new self();
}
return $instance;
}
/**
* Constructor.
*
* @since 0.2.0
*/
private function __construct() {
add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'wp_ajax_wpaw_refresh_models', array( $this, 'ajax_refresh_models' ) );
add_action( 'wp_ajax_wpaw_get_cost_log_data', array( $this, 'ajax_get_cost_log_data' ) );
add_action( 'wp_ajax_wpaw_get_header_stats', array( $this, 'ajax_get_header_stats' ) );
add_action( 'wp_ajax_wpaw_test_api_connection', array( $this, 'ajax_test_api_connection' ) );
add_action( 'wp_ajax_wpaw_debug_models', array( $this, 'ajax_debug_models' ) );
add_action( 'wp_ajax_wpaw_save_custom_model', array( $this, 'ajax_save_custom_model' ) );
add_action( 'wp_ajax_wpaw_delete_custom_model', array( $this, 'ajax_delete_custom_model' ) );
}
/**
* Enqueue scripts for settings page.
*
* @since 0.2.0
* @param string $hook Current admin page hook.
*/
public function enqueue_scripts( $hook ) {
if ( 'settings_page_wp-agentic-writer-settings' !== $hook ) {
return;
}
// Bootstrap 5.3
wp_enqueue_style( 'bootstrap', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css', array(), '5.3.3' );
wp_enqueue_style( 'bootstrap-icons', 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css', array(), '1.11.1' );
wp_enqueue_script( 'bootstrap', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js', array(), '5.3.3', true );
// Select2 for searchable dropdowns
wp_enqueue_style( 'select2', 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css', array(), '4.1.0' );
wp_enqueue_style( 'select2-bootstrap-5', 'https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css', array( 'select2', 'bootstrap' ), '1.3.0' );
wp_enqueue_script( 'select2', 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js', array( 'jquery' ), '4.1.0', true );
// Agentic Vibe CSS - Design System (in order)
wp_enqueue_style( 'wpaw-agentic-variables', WP_AGENTIC_WRITER_URL . 'assets/css/agentic-variables.css', array(), WP_AGENTIC_WRITER_VERSION );
wp_enqueue_style( 'wpaw-agentic-bootstrap-custom', WP_AGENTIC_WRITER_URL . 'assets/css/agentic-bootstrap-custom.css', array( 'bootstrap', 'wpaw-agentic-variables' ), WP_AGENTIC_WRITER_VERSION );
wp_enqueue_style( 'wpaw-agentic-components', WP_AGENTIC_WRITER_URL . 'assets/css/agentic-components.css', array( 'wpaw-agentic-variables' ), WP_AGENTIC_WRITER_VERSION );
// Legacy plugin styles
wp_enqueue_style( 'wp-agentic-writer-admin-v2', WP_AGENTIC_WRITER_URL . 'assets/css/admin-v2.css', array( 'bootstrap', 'select2-bootstrap-5' ), WP_AGENTIC_WRITER_VERSION );
wp_enqueue_style( 'wp-agentic-writer-settings-v2', WP_AGENTIC_WRITER_URL . 'assets/css/settings-v2.css', array( 'wpaw-agentic-components' ), WP_AGENTIC_WRITER_VERSION );
wp_enqueue_style( 'wp-agentic-writer-cost-log-grouped', WP_AGENTIC_WRITER_URL . 'assets/css/cost-log-grouped.css', array( 'wp-agentic-writer-settings-v2' ), WP_AGENTIC_WRITER_VERSION );
// Plugin scripts
wp_enqueue_script( 'wp-agentic-writer-settings-v2', WP_AGENTIC_WRITER_URL . 'assets/js/settings-v2.js', array( 'jquery', 'bootstrap', 'select2' ), WP_AGENTIC_WRITER_VERSION, true );
$settings = get_option( 'wp_agentic_writer_settings', array() );
wp_localize_script( 'wp-agentic-writer-settings-v2', 'wpawSettingsV2', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'wpaw_settings' ),
'models' => $this->get_models_for_select(),
'currentModels' => array(
'planning' => $settings['planning_model'] ?? 'google/gemini-2.0-flash-exp:free',
'writing' => $settings['writing_model'] ?? ( $settings['execution_model'] ?? 'anthropic/claude-3.5-sonnet' ),
'execution' => $settings['writing_model'] ?? ( $settings['execution_model'] ?? 'anthropic/claude-3.5-sonnet' ),
'clarity' => $settings['clarity_model'] ?? 'google/gemini-2.0-flash-exp:free',
'refinement' => $settings['refinement_model'] ?? 'anthropic/claude-3.5-sonnet',
'chat' => $settings['chat_model'] ?? 'google/gemini-2.0-flash-exp:free',
'image' => $settings['image_model'] ?? 'openai/gpt-4o',
),
'i18n' => array(
'refreshing' => __( 'Refreshing...', 'wp-agentic-writer' ),
'refreshModels' => __( 'Refresh Models', 'wp-agentic-writer' ),
'saveSuccess' => __( 'Settings saved successfully!', 'wp-agentic-writer' ),
'saveError' => __( 'Error saving settings.', 'wp-agentic-writer' ),
'confirmReset' => __( 'Are you sure you want to reset all settings to defaults?', 'wp-agentic-writer' ),
'loading' => __( 'Loading...', 'wp-agentic-writer' ),
'noResults' => __( 'No models found', 'wp-agentic-writer' ),
'searchPlaceholder' => __( 'Search models...', 'wp-agentic-writer' ),
),
) );
}
/**
* Get models for select dropdowns.
*
* @since 0.2.0
* @return array Models grouped by category.
*/
public function get_models_for_select() {
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$models = $provider->get_cached_models();
if ( is_wp_error( $models ) ) {
return $this->get_fallback_models();
}
$transformed = $this->transform_models_for_js( $models );
// Debug logging
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
$custom_models = get_option( 'wp_agentic_writer_custom_models', array() );
error_log( 'WPAW get_models_for_select: custom_models in DB = ' . wp_json_encode( $custom_models ) );
error_log( 'WPAW get_models_for_select: image models count = ' . count( $transformed['image']['all'] ?? array() ) );
}
return $transformed;
}
/**
* Format model name from ID.
*
* @since 0.2.0
* @param string $model_id Model ID.
* @return string Formatted model name.
*/
private function format_model_name( $model_id ) {
// Remove provider prefix
$parts = explode( '/', $model_id );
$name = end( $parts );
// Remove :free suffix
$name = preg_replace( '/:free$/i', '', $name );
// Convert hyphens and underscores to spaces
$name = str_replace( array( '-', '_' ), ' ', $name );
// Capitalize words
$name = ucwords( $name );
// Add provider prefix back
if ( count( $parts ) > 1 ) {
$provider = ucfirst( $parts[0] );
$name = $provider . ': ' . $name;
}
return $name;
}
/**
* Get fallback models when API fails.
*
* @since 0.2.0
* @return array Fallback model structure.
*/
private function get_fallback_models() {
return array(
'planning' => array(
'recommended' => array(
array( 'id' => 'google/gemini-2.0-flash-exp:free', 'name' => 'Google Gemini 2.0 Flash' ),
),
'all' => array(
array( 'id' => 'google/gemini-2.0-flash-exp:free', 'name' => 'Google Gemini 2.0 Flash' ),
),
),
'execution' => array(
'recommended' => array(
array( 'id' => 'anthropic/claude-3.5-sonnet', 'name' => 'Anthropic Claude 3.5 Sonnet' ),
),
'all' => array(
array( 'id' => 'anthropic/claude-3.5-sonnet', 'name' => 'Anthropic Claude 3.5 Sonnet' ),
),
),
'image' => array(
'recommended' => array(
array( 'id' => 'openai/gpt-4o', 'name' => 'OpenAI GPT-4o' ),
),
'all' => array(
array( 'id' => 'openai/gpt-4o', 'name' => 'OpenAI GPT-4o' ),
),
),
);
}
/**
* Transform models structure for JavaScript consumption.
*
* @since 0.2.0
* @param array $models Models from provider.
* @return array Transformed models.
*/
private function transform_models_for_js( $models ) {
// Handle flat model list from OpenRouter
if ( ! empty( $models ) && array_keys( $models ) === range( 0, count( $models ) - 1 ) ) {
$settings = get_option( 'wp_agentic_writer_settings', array() );
$planning_id = $settings['planning_model'] ?? 'google/gemini-2.0-flash-exp:free';
$execution_id = $settings['execution_model'] ?? 'anthropic/claude-3.5-sonnet';
$image_id = $settings['image_model'] ?? 'black-forest-labs/flux-schnell';
$text_models = array();
$image_models = array();
// Categorize models using OpenRouter's output_modalities field
foreach ( $models as $model ) {
if ( empty( $model['id'] ) ) {
continue;
}
$prompt_price = isset( $model['pricing']['prompt'] ) ? (float) $model['pricing']['prompt'] : 0;
$completion_price = isset( $model['pricing']['completion'] ) ? (float) $model['pricing']['completion'] : 0;
$image_price = isset( $model['pricing']['image'] ) ? (float) $model['pricing']['image'] : 0;
$model_data = array(
'id' => $model['id'],
'name' => $model['name'] ?? $model['id'],
'is_free' => $prompt_price <= 0.0 && $completion_price <= 0.0 && $image_price <= 0.0,
'pricing' => array(
'prompt' => $prompt_price,
'completion' => $completion_price,
'image' => $image_price,
),
);
// Use OpenRouter's output_modalities to categorize - trust OpenRouter's classification
$output_modalities = $model['architecture']['output_modalities'] ?? array();
// Image generation models have 'image' in output_modalities
if ( in_array( 'image', $output_modalities, true ) ) {
$image_models[] = $model_data;
}
// Text models have 'text' in output_modalities (most models)
if ( in_array( 'text', $output_modalities, true ) ) {
$text_models[] = $model_data;
}
}
$chat_id = $settings['chat_model'] ?? 'google/gemini-2.0-flash-exp:free';
$clarity_id = $settings['clarity_model'] ?? 'google/gemini-2.0-flash-exp:free';
$refinement_id = $settings['refinement_model'] ?? 'anthropic/claude-3.5-sonnet';
$writing_id = $settings['writing_model'] ?? ( $settings['execution_model'] ?? 'anthropic/claude-3.5-sonnet' );
// Add currently selected models to text_models if not already present
$current_model_ids = array( $planning_id, $execution_id, $chat_id, $clarity_id, $refinement_id, $writing_id );
foreach ( $current_model_ids as $model_id ) {
$found = false;
foreach ( $text_models as $tm ) {
if ( $tm['id'] === $model_id ) {
$found = true;
break;
}
}
if ( ! $found && ! empty( $model_id ) ) {
$text_models[] = array(
'id' => $model_id,
'name' => $this->format_model_name( $model_id ),
'is_free' => false,
'pricing' => array(
'prompt' => 0,
'completion' => 0,
'image' => 0,
),
);
}
}
// Add currently selected image model to image_models if not already present
if ( ! empty( $image_id ) ) {
$found = false;
foreach ( $image_models as $im ) {
if ( $im['id'] === $image_id ) {
$found = true;
break;
}
}
if ( ! $found ) {
$image_models[] = array(
'id' => $image_id,
'name' => $this->format_model_name( $image_id ),
'is_free' => false,
'pricing' => array(
'prompt' => 0,
'completion' => 0,
'image' => 0,
),
);
}
}
// Add user's custom models (not listed in API but callable by ID)
$custom_models = get_option( 'wp_agentic_writer_custom_models', array() );
foreach ( $custom_models as $custom ) {
if ( empty( $custom['id'] ) ) {
continue;
}
$custom_model_data = array(
'id' => $custom['id'],
'name' => ! empty( $custom['name'] ) ? $custom['name'] : $this->format_model_name( $custom['id'] ),
'is_free' => false,
'is_custom' => true,
'pricing' => array(
'prompt' => 0,
'completion' => 0,
'image' => 0,
),
);
$type = $custom['type'] ?? 'text';
if ( 'image' === $type ) {
$image_models[] = $custom_model_data;
} else {
$text_models[] = $custom_model_data;
}
}
// Now create find_model closure after all models are added
$find_model = function ( $model_id ) use ( $text_models, $image_models ) {
foreach ( array_merge( $text_models, $image_models ) as $model ) {
if ( $model['id'] === $model_id ) {
return $model;
}
}
// If model not found, create a fallback entry
if ( ! empty( $model_id ) ) {
return array(
'id' => $model_id,
'name' => $this->format_model_name( $model_id ),
'is_free' => false,
'pricing' => array(
'prompt' => 0,
'completion' => 0,
'image' => 0,
),
);
}
return null;
};
return array(
'planning' => array(
'recommended' => array_filter( array( $find_model( $planning_id ) ) ),
'all' => $text_models,
),
'execution' => array(
'recommended' => array_filter( array( $find_model( $execution_id ) ) ),
'all' => $text_models,
),
'chat' => array(
'recommended' => array_filter( array( $find_model( $chat_id ) ) ),
'all' => $text_models,
),
'image' => array(
'recommended' => array_filter( array( $find_model( $image_id ) ) ),
'all' => $image_models,
),
);
}
$transformed = array();
foreach ( $models as $type => $categories ) {
if ( ! isset( $transformed[ $type ] ) ) {
$transformed[ $type ] = array(
'recommended' => array(),
'all' => array(),
);
}
// Combine free and paid into 'all' array
$all_models = array_merge(
$categories['free'] ?? array(),
$categories['paid'] ?? array()
);
// Remove duplicates
$recommended_ids = array();
foreach ( $categories['recommended'] ?? array() as $model ) {
$transformed[ $type ]['recommended'][] = $model;
$recommended_ids[ $model['id'] ] = true;
}
// Add all models, avoiding duplicates with recommended
foreach ( $all_models as $model ) {
if ( ! isset( $recommended_ids[ $model['id'] ] ) ) {
$transformed[ $type ]['all'][] = $model;
}
}
}
return $transformed;
}
/**
* AJAX handler for refreshing models.
*
* @since 0.2.0
*/
public function ajax_refresh_models() {
if ( ! check_ajax_referer( 'wpaw_settings', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied' ) );
}
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$models = $provider->fetch_and_cache_models( true );
if ( is_wp_error( $models ) ) {
wp_send_json_error( array( 'message' => $models->get_error_message() ) );
}
$transformed = $this->transform_models_for_js( $models );
wp_send_json_success( array(
'models' => $transformed,
'message' => __( 'Models refreshed successfully!', 'wp-agentic-writer' ),
) );
}
/**
* AJAX handler for saving a custom model.
*
* @since 0.2.0
*/
public function ajax_save_custom_model() {
if ( ! check_ajax_referer( 'wpaw_settings', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied' ) );
return;
}
$model_id = isset( $_POST['model_id'] ) ? sanitize_text_field( $_POST['model_id'] ) : '';
$model_name = isset( $_POST['model_name'] ) ? sanitize_text_field( $_POST['model_name'] ) : '';
$model_type = isset( $_POST['model_type'] ) ? sanitize_text_field( $_POST['model_type'] ) : 'text';
if ( empty( $model_id ) ) {
wp_send_json_error( array( 'message' => 'Model ID is required' ) );
return;
}
// Use separate option for custom models
$custom_models = get_option( 'wp_agentic_writer_custom_models', array() );
// Check if model already exists, update it
$found = false;
foreach ( $custom_models as $index => $cm ) {
if ( $cm['id'] === $model_id ) {
$custom_models[ $index ] = array(
'id' => $model_id,
'name' => $model_name,
'type' => $model_type,
);
$found = true;
break;
}
}
// Add new model if not found
if ( ! $found ) {
$custom_models[] = array(
'id' => $model_id,
'name' => $model_name,
'type' => $model_type,
);
}
$saved = update_option( 'wp_agentic_writer_custom_models', array_values( $custom_models ) );
// Get fresh combined models for Select2
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$models = $provider->get_cached_models();
if ( is_wp_error( $models ) ) {
$models = array();
}
$transformed = $this->transform_models_for_js( $models );
wp_send_json_success( array(
'message' => __( 'Custom model saved!', 'wp-agentic-writer' ),
'models' => $transformed,
) );
}
/**
* AJAX handler for deleting a custom model.
*
* @since 0.2.0
*/
public function ajax_delete_custom_model() {
if ( ! check_ajax_referer( 'wpaw_settings', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied' ) );
return;
}
$model_id = isset( $_POST['model_id'] ) ? sanitize_text_field( $_POST['model_id'] ) : '';
if ( empty( $model_id ) ) {
wp_send_json_error( array( 'message' => 'Model ID is required' ) );
return;
}
// Use separate option for custom models
$custom_models = get_option( 'wp_agentic_writer_custom_models', array() );
// Remove the model
$custom_models = array_filter( $custom_models, function ( $cm ) use ( $model_id ) {
return $cm['id'] !== $model_id;
} );
update_option( 'wp_agentic_writer_custom_models', array_values( $custom_models ) );
// Get fresh combined models for Select2
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$models = $provider->get_cached_models();
if ( is_wp_error( $models ) ) {
$models = array();
}
$transformed = $this->transform_models_for_js( $models );
wp_send_json_success( array(
'message' => __( 'Custom model deleted!', 'wp-agentic-writer' ),
'models' => $transformed,
) );
}
/**
* AJAX handler for getting cost log data (server-side pagination).
*
* @since 0.2.0
*/
public function ajax_get_cost_log_data() {
if ( ! check_ajax_referer( 'wpaw_settings', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied' ) );
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'wpaw_cost_tracking';
// Check if table exists
$table_exists = $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) === $table_name;
if ( ! $table_exists ) {
wp_send_json_success(
array(
'records' => array(),
'total_items' => 0,
'total_pages' => 0,
'current_page' => 1,
'per_page' => 25,
'stats' => array(
'all_time' => '0.0000',
'monthly' => '0.0000',
'today' => '0.0000',
'avg_per_post' => '0.0000',
),
'filters' => array(
'models' => array(),
'types' => array(),
),
)
);
return;
}
// Get parameters
$page = isset( $_POST['page'] ) ? max( 1, intval( $_POST['page'] ) ) : 1;
$per_page = isset( $_POST['per_page'] ) ? min( 100, max( 10, intval( $_POST['per_page'] ) ) ) : 25;
$offset = ( $page - 1 ) * $per_page;
// Filters
$filter_post = isset( $_POST['filter_post'] ) ? intval( $_POST['filter_post'] ) : 0;
$filter_model = isset( $_POST['filter_model'] ) ? sanitize_text_field( $_POST['filter_model'] ) : '';
$filter_type = isset( $_POST['filter_type'] ) ? sanitize_text_field( $_POST['filter_type'] ) : '';
$filter_date_from = isset( $_POST['filter_date_from'] ) ? sanitize_text_field( $_POST['filter_date_from'] ) : '';
$filter_date_to = isset( $_POST['filter_date_to'] ) ? sanitize_text_field( $_POST['filter_date_to'] ) : '';
// Build WHERE clause
$where = array( '1=1' );
if ( $filter_post > 0 ) {
$where[] = $wpdb->prepare( 'post_id = %d', $filter_post );
}
if ( ! empty( $filter_model ) ) {
$where[] = $wpdb->prepare( 'model = %s', $filter_model );
}
if ( ! empty( $filter_type ) ) {
$where[] = $wpdb->prepare( 'action = %s', $filter_type );
}
if ( ! empty( $filter_date_from ) ) {
$where[] = $wpdb->prepare( 'DATE(created_at) >= %s', $filter_date_from );
}
if ( ! empty( $filter_date_to ) ) {
$where[] = $wpdb->prepare( 'DATE(created_at) <= %s', $filter_date_to );
}
$where_clause = implode( ' AND ', $where );
// Get total count
$total_items = $wpdb->get_var( "SELECT COUNT(*) FROM {$table_name} WHERE {$where_clause}" );
$total_pages = ceil( $total_items / $per_page );
// Get all records grouped by post
$all_records = $wpdb->get_results(
"SELECT * FROM {$table_name} WHERE {$where_clause} ORDER BY post_id DESC, created_at DESC"
);
// Group records by post_id
$grouped_records = array();
foreach ( $all_records as $record ) {
$post_id = $record->post_id;
if ( ! isset( $grouped_records[ $post_id ] ) ) {
$post_title = get_the_title( $post_id );
if ( ! $post_title && $post_id > 0 ) {
$post_title = sprintf( __( '[Removed Post #%d]', 'wp-agentic-writer' ), $post_id );
$post_link = '';
} elseif ( $post_id > 0 ) {
$post_link = get_edit_post_link( $post_id, 'raw' );
} else {
$post_title = __( 'System/Other', 'wp-agentic-writer' );
$post_link = '';
}
$grouped_records[ $post_id ] = array(
'post_id' => $post_id,
'post_title' => $post_title,
'post_link' => $post_link,
'total_cost' => 0,
'call_count' => 0,
'details' => array(),
);
}
$grouped_records[ $post_id ]['total_cost'] += (float) $record->cost;
$grouped_records[ $post_id ]['call_count']++;
$grouped_records[ $post_id ]['details'][] = array(
'id' => $record->id,
'created_at' => date_i18n( 'Y-m-d H:i:s', strtotime( $record->created_at ) ),
'model' => $record->model,
'action' => ucfirst( str_replace( '_', ' ', $record->action ) ),
'input_tokens' => number_format( $record->input_tokens ),
'output_tokens' => number_format( $record->output_tokens ),
'cost' => number_format( $record->cost, 4 ),
);
}
// Convert to indexed array and format
$formatted_records = array();
foreach ( $grouped_records as $group ) {
$group['total_cost'] = number_format( $group['total_cost'], 4 );
$formatted_records[] = $group;
}
// Paginate grouped records
$total_items = count( $formatted_records );
$total_pages = ceil( $total_items / $per_page );
$formatted_records = array_slice( $formatted_records, $offset, $per_page );
// Get summary stats
$cost_tracker = WP_Agentic_Writer_Cost_Tracker::get_instance();
$total_all_time = $wpdb->get_var( "SELECT SUM(cost) FROM {$table_name}" );
$monthly_total = $cost_tracker->get_monthly_total();
$today_total = $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(cost) FROM {$table_name} WHERE DATE(created_at) = %s",
current_time( 'Y-m-d' )
)
);
$total_posts = $wpdb->get_var( "SELECT COUNT(DISTINCT post_id) FROM {$table_name} WHERE post_id > 0" );
$avg_per_post = $total_posts > 0 ? $total_all_time / $total_posts : 0;
// Get filter options
$models = $wpdb->get_col( "SELECT DISTINCT model FROM {$table_name} ORDER BY model" );
$types = $wpdb->get_col( "SELECT DISTINCT action FROM {$table_name} ORDER BY action" );
wp_send_json_success( array(
'records' => $formatted_records,
'total_items' => intval( $total_items ),
'total_pages' => intval( $total_pages ),
'current_page' => $page,
'per_page' => $per_page,
'stats' => array(
'all_time' => number_format( (float) $total_all_time, 4 ),
'monthly' => number_format( (float) $monthly_total, 4 ),
'today' => number_format( (float) $today_total, 4 ),
'avg_per_post' => number_format( (float) $avg_per_post, 4 ),
),
'filters' => array(
'models' => $models,
'types' => $types,
),
) );
}
/**
* AJAX handler for header statistics.
*
* @since 0.2.0
*/
public function ajax_get_header_stats() {
if ( ! check_ajax_referer( 'wpaw_settings', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied' ) );
return;
}
global $wpdb;
$table_name = $wpdb->prefix . 'wpaw_cost_tracking';
// Check if table exists
$table_exists = $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) === $table_name;
if ( ! $table_exists ) {
wp_send_json_success(
array(
'articles' => 0,
'total_cost' => '0.00',
'api_status' => 'Not configured',
'api_online' => false,
'last_updated' => 'Never',
)
);
return;
}
// Get total articles
$total_articles = $wpdb->get_var(
"SELECT COUNT(DISTINCT post_id) FROM {$table_name} WHERE post_id > 0"
);
// Get total cost
$total_cost = $wpdb->get_var( "SELECT SUM(cost) FROM {$table_name}" );
// Check API status
$settings = get_option( 'wp_agentic_writer_settings', array() );
$api_key = $settings['openrouter_api_key'] ?? '';
$api_online = ! empty( $api_key );
// Get last activity
$last_activity = $wpdb->get_var(
"SELECT created_at FROM {$table_name} ORDER BY created_at DESC LIMIT 1"
);
$last_updated = $last_activity ? human_time_diff( strtotime( $last_activity ), current_time( 'timestamp' ) ) . ' ago' : 'Never';
wp_send_json_success(
array(
'articles' => intval( $total_articles ),
'total_cost' => number_format( (float) $total_cost, 2 ),
'api_status' => $api_online ? 'Online' : 'Not configured',
'api_online' => $api_online,
'last_updated' => $last_updated,
)
);
}
/**
* AJAX handler for debugging models.
*
* @since 0.2.0
*/
public function ajax_debug_models() {
if ( ! check_ajax_referer( 'wpaw_settings', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied' ) );
return;
}
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$models = $provider->get_cached_models();
if ( is_wp_error( $models ) ) {
wp_send_json_error( array( 'message' => $models->get_error_message() ) );
return;
}
// Check for specific models
$check_models = array( 'deepseek/deepseek-chat-v3-0324', 'anthropic/claude-3.5-sonnet' );
$found_models = array();
$missing_models = array();
foreach ( $check_models as $check_id ) {
$found = false;
foreach ( $models as $model ) {
if ( isset( $model['id'] ) && $model['id'] === $check_id ) {
$found = true;
$found_models[] = array(
'id' => $model['id'],
'name' => $model['name'] ?? 'N/A',
);
break;
}
}
if ( ! $found ) {
$missing_models[] = $check_id;
}
}
wp_send_json_success( array(
'total_models' => count( $models ),
'found_models' => $found_models,
'missing_models' => $missing_models,
'sample_models' => array_slice( array_map( function( $m ) {
return array( 'id' => $m['id'] ?? 'N/A', 'name' => $m['name'] ?? 'N/A' );
}, $models ), 0, 10 ),
) );
}
/**
* AJAX handler for testing API connection.
*
* @since 0.2.0
*/
public function ajax_test_api_connection() {
if ( ! check_ajax_referer( 'wpaw_settings', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied' ) );
return;
}
$settings = get_option( 'wp_agentic_writer_settings', array() );
$api_key = $settings['openrouter_api_key'] ?? '';
if ( empty( $api_key ) ) {
wp_send_json_error( array( 'message' => 'API key is not configured' ) );
return;
}
// Test API connection by making a simple request
$response = wp_remote_get(
'https://openrouter.ai/api/v1/models',
array(
'headers' => array(
'Authorization' => 'Bearer ' . $api_key,
'HTTP-Referer' => home_url(),
),
'timeout' => 10,
)
);
if ( is_wp_error( $response ) ) {
wp_send_json_error(
array(
'message' => 'Connection failed: ' . $response->get_error_message(),
)
);
return;
}
$status_code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
if ( 200 === $status_code ) {
$data = json_decode( $body, true );
if ( isset( $data['data'] ) && is_array( $data['data'] ) ) {
wp_send_json_success(
array(
'message' => 'API connection successful!',
'models_count' => count( $data['data'] ),
)
);
return;
}
}
// Handle error responses
if ( 401 === $status_code ) {
wp_send_json_error( array( 'message' => 'Invalid API key' ) );
return;
}
if ( 403 === $status_code ) {
wp_send_json_error( array( 'message' => 'Access forbidden - check your API key permissions' ) );
return;
}
wp_send_json_error(
array(
'message' => 'API connection failed with status code: ' . $status_code,
)
);
}
/**
* Add settings page to admin menu.
*
* @since 0.2.0
*/
public function add_settings_page() {
add_options_page(
__( 'WP Agentic Writer', 'wp-agentic-writer' ),
__( 'Agentic Writer', 'wp-agentic-writer' ),
'manage_options',
'wp-agentic-writer-settings',
array( $this, 'render_settings_page' )
);
}
/**
* Register settings.
*
* @since 0.2.0
*/
public function register_settings() {
register_setting(
'wp_agentic_writer_settings',
'wp_agentic_writer_settings',
array(
'sanitize_callback' => array( $this, 'sanitize_settings' ),
)
);
}
/**
* Sanitize settings.
*
* @since 0.2.0
* @param array $input Settings input.
* @return array Sanitized settings.
*/
public function sanitize_settings( $input ) {
$sanitized = array();
// Sanitize API key
$sanitized['openrouter_api_key'] = trim( $input['openrouter_api_key'] ?? '' );
// Sanitize model names (6 models)
$sanitized['chat_model'] = sanitize_text_field( $input['chat_model'] ?? 'google/gemini-2.5-flash' );
$sanitized['clarity_model'] = sanitize_text_field( $input['clarity_model'] ?? 'google/gemini-2.5-flash' );
$sanitized['planning_model'] = sanitize_text_field( $input['planning_model'] ?? 'google/gemini-2.5-flash' );
$sanitized['writing_model'] = sanitize_text_field( $input['writing_model'] ?? 'anthropic/claude-3.5-sonnet' );
$sanitized['refinement_model'] = sanitize_text_field( $input['refinement_model'] ?? 'anthropic/claude-3.5-sonnet' );
$sanitized['image_model'] = sanitize_text_field( $input['image_model'] ?? 'openai/gpt-4o' );
// Legacy support: map execution_model to writing_model
if ( isset( $input['execution_model'] ) && ! isset( $input['writing_model'] ) ) {
$sanitized['writing_model'] = sanitize_text_field( $input['execution_model'] );
}
// Sanitize boolean values
$sanitized['web_search_enabled'] = isset( $input['web_search_enabled'] ) && '1' === $input['web_search_enabled'];
$sanitized['cost_tracking_enabled'] = isset( $input['cost_tracking_enabled'] ) && '1' === $input['cost_tracking_enabled'];
$sanitized['enable_clarification_quiz'] = isset( $input['enable_clarification_quiz'] ) && '1' === $input['enable_clarification_quiz'];
// Sanitize search options
$sanitized['search_engine'] = in_array( $input['search_engine'] ?? '', array( 'auto', 'native', 'exa' ), true )
? $input['search_engine']
: 'auto';
$sanitized['search_depth'] = isset( $input['search_depth'] ) && in_array( $input['search_depth'], array( 'low', 'medium', 'high' ), true )
? $input['search_depth']
: 'medium';
// Sanitize budget
$sanitized['monthly_budget'] = floatval( $input['monthly_budget'] ?? 600 );
// Sanitize chat history limit
$chat_history_limit = isset( $input['chat_history_limit'] ) ? absint( $input['chat_history_limit'] ) : 20;
$sanitized['chat_history_limit'] = min( $chat_history_limit, 200 );
// Sanitize clarification quiz settings
$sanitized['clarity_confidence_threshold'] = in_array( $input['clarity_confidence_threshold'] ?? '', array( '0.5', '0.6', '0.7', '0.8', '0.9' ), true )
? $input['clarity_confidence_threshold']
: '0.6';
if ( isset( $input['required_context_categories'] ) && is_array( $input['required_context_categories'] ) ) {
$valid_categories = array( 'target_outcome', 'target_audience', 'tone', 'content_depth', 'expertise_level', 'content_type', 'pov' );
$sanitized['required_context_categories'] = array_intersect( $input['required_context_categories'], $valid_categories );
} else {
$sanitized['required_context_categories'] = array( 'target_outcome', 'target_audience', 'tone', 'content_depth', 'expertise_level', 'content_type', 'pov' );
}
// Sanitize preferred languages
if ( isset( $input['preferred_languages'] ) && is_array( $input['preferred_languages'] ) ) {
$sanitized['preferred_languages'] = array_map( 'sanitize_text_field', $input['preferred_languages'] );
} else {
$sanitized['preferred_languages'] = array( 'auto', 'English', 'Indonesian' );
}
// Sanitize custom languages
if ( isset( $input['custom_languages'] ) && is_array( $input['custom_languages'] ) ) {
$sanitized['custom_languages'] = array_filter( array_map( 'sanitize_text_field', $input['custom_languages'] ) );
} else {
$sanitized['custom_languages'] = array();
}
return $sanitized;
}
/**
* Render settings page - main entry point.
*
* @since 0.2.0
*/
public function render_settings_page() {
$settings = get_option( 'wp_agentic_writer_settings', array() );
// Extract settings for views
$view_data = $this->prepare_view_data( $settings );
// Include main layout
include WP_AGENTIC_WRITER_DIR . 'views/settings/layout.php';
}
/**
* Prepare data for view files.
*
* @since 0.2.0
* @param array $settings Plugin settings.
* @return array View data.
*/
private function prepare_view_data( $settings ) {
// Extract settings (6 models)
$api_key = $settings['openrouter_api_key'] ?? '';
$chat_model = $settings['chat_model'] ?? 'google/gemini-2.5-flash';
$clarity_model = $settings['clarity_model'] ?? 'google/gemini-2.5-flash';
$planning_model = $settings['planning_model'] ?? 'google/gemini-2.5-flash';
$writing_model = $settings['writing_model'] ?? ( $settings['execution_model'] ?? 'anthropic/claude-3.5-sonnet' );
$refinement_model = $settings['refinement_model'] ?? 'anthropic/claude-3.5-sonnet';
$image_model = $settings['image_model'] ?? 'openai/gpt-4o';
$web_search_enabled = $settings['web_search_enabled'] ?? false;
$search_engine = $settings['search_engine'] ?? 'auto';
$search_depth = $settings['search_depth'] ?? 'medium';
$cost_tracking_enabled = $settings['cost_tracking_enabled'] ?? true;
$monthly_budget = $settings['monthly_budget'] ?? 600;
$chat_history_limit = $settings['chat_history_limit'] ?? 20;
$enable_clarification_quiz = $settings['enable_clarification_quiz'] ?? true;
$clarity_confidence_threshold = $settings['clarity_confidence_threshold'] ?? '0.6';
$required_context_categories = $settings['required_context_categories'] ?? array(
'target_outcome',
'target_audience',
'tone',
'content_depth',
'expertise_level',
'content_type',
'pov',
);
$preferred_languages = $settings['preferred_languages'] ?? array( 'auto', 'English', 'Indonesian' );
$custom_languages = $settings['custom_languages'] ?? array();
$custom_models = get_option( 'wp_agentic_writer_custom_models', array() );
// Get cost tracking data
$cost_tracker = WP_Agentic_Writer_Cost_Tracker::get_instance();
$monthly_used = $cost_tracker->get_monthly_total();
$budget_percent = $monthly_budget > 0 ? ( $monthly_used / $monthly_budget ) * 100 : 0;
$budget_status = $budget_percent > 90 ? 'danger' : ( $budget_percent > 70 ? 'warning' : 'success' );
return compact(
'api_key',
'chat_model',
'clarity_model',
'planning_model',
'writing_model',
'refinement_model',
'image_model',
'web_search_enabled',
'search_engine',
'search_depth',
'cost_tracking_enabled',
'monthly_budget',
'chat_history_limit',
'enable_clarification_quiz',
'clarity_confidence_threshold',
'required_context_categories',
'preferred_languages',
'custom_languages',
'custom_models',
'monthly_used',
'budget_percent',
'budget_status'
);
}
/**
* Get available languages list.
*
* @since 0.2.0
* @return array Available languages.
*/
public function get_available_languages() {
return array(
'auto' => 'Auto-detect',
'English' => 'English',
'Indonesian' => 'Indonesian (Bahasa Indonesia)',
'Javanese' => 'Javanese (Basa Jawa)',
'Sundanese' => 'Sundanese (Basa Sunda)',
'Spanish' => 'Spanish (Español)',
'French' => 'French (Français)',
'Arabic' => 'Arabic (العربية)',
'Chinese' => 'Chinese (中文)',
'Japanese' => 'Japanese (日本語)',
'Portuguese' => 'Portuguese (Português)',
'German' => 'German (Deutsch)',
'Hindi' => 'Hindi (हिंदी)',
'Korean' => 'Korean (한국어)',
'Vietnamese' => 'Vietnamese (Tiếng Việt)',
'Thai' => 'Thai (ไทย)',
'Tagalog' => 'Tagalog',
'Malay' => 'Malay (Bahasa Melayu)',
'Russian' => 'Russian (Русский)',
'Italian' => 'Italian (Italiano)',
'Dutch' => 'Dutch (Nederlands)',
'Polish' => 'Polish (Polski)',
'Turkish' => 'Turkish (Türkçe)',
'Swedish' => 'Swedish (Svenska)',
);
}
}