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)', ); } }