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', ), ) ); } /** * Get models for select dropdowns. * * @since 0.1.0 * @return array Models grouped by category. */ private function get_models_for_select() { $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $models = $provider->get_cached_models(); if ( is_wp_error( $models ) ) { // Return fallback defaults if API fails. 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 model structure to match what JS expects. return $this->transform_models_for_js( $models ); } /** * Transform models structure for JavaScript consumption. * * @since 0.1.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'] ?? 'openai/gpt-4o'; $text_models = array(); $image_models = array(); // Known image model prefixes $image_model_prefixes = array( 'black-forest-labs/', 'stability-ai/', 'dall-e', 'midjourney' ); 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, 'pricing' => array( 'prompt' => $prompt_price, 'completion' => $completion_price, 'image' => $image_price, ), ); // Check if this is an image model $is_image_model = false; foreach ( $image_model_prefixes as $prefix ) { if ( stripos( $model['id'], $prefix ) === 0 ) { $is_image_model = true; break; } } // Also check if it has image pricing but no text pricing if ( $image_price > 0 && $prompt_price <= 0 && $completion_price <= 0 ) { $is_image_model = true; } if ( $is_image_model ) { $image_models[] = $model_data; } else { $text_models[] = $model_data; } } $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; } } return null; }; $chat_id = $settings['chat_model'] ?? 'google/gemini-2.0-flash-exp:free'; 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 (in case a model is in both recommended and all). $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.1.0 */ public function ajax_refresh_models() { check_ajax_referer( 'wpaw_settings', 'nonce' ); 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() ) ); } // Transform models for JS consumption. $transformed = $this->transform_models_for_js( $models ); wp_send_json_success( array( 'models' => $transformed, 'message' => __( 'Models refreshed successfully!', 'wp-agentic-writer' ), ) ); } /** * Add settings page to admin menu. * * @since 0.1.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', array( $this, 'render_settings_page' ) ); } /** * Register settings. * * @since 0.1.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.1.0 * @param array $input Settings input. * @return array Sanitized settings. */ public function sanitize_settings( $input ) { $sanitized = array(); // Sanitize API key (don't strip tags, but trim). $sanitized['openrouter_api_key'] = trim( $input['openrouter_api_key'] ?? '' ); // Sanitize model names (6 models as per model-preset-brief.md). $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. * * @since 0.1.0 */ public function render_settings_page() { $settings = get_option( 'wp_agentic_writer_settings', array() ); // Extract settings (6 models as per model-preset-brief.md). $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; // Clarification quiz settings. $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', ); // 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' : '' ); ?>