'GPT-4o (Default)', 'gpt-4-turbo' => 'GPT-4 Turbo (Default)', ]; /** * Constructor. * * Hooks into WordPress to register the settings page. */ public function __construct() { add_action( 'admin_menu', array( $this, 'add_settings_page' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); // Clear models cache when API key changes. add_action( 'update_option_' . $this->option_name, array( $this, 'clear_models_cache' ), 10, 0 ); // AJAX handler for refreshing models. add_action( 'wp_ajax_aichatbotbuilder_refresh_models', array( $this, 'handle_ajax_refresh_models' ) ); // Enqueue scripts for settings page. add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_settings_scripts' ) ); } /** * Adds the settings page to the admin menu. * * @return void */ public function add_settings_page(): void { add_menu_page( esc_html__( 'AI Chatbot Builder Settings', 'ai-chatbot-builder' ), esc_html__( 'AI Chatbot', 'ai-chatbot-builder' ), 'manage_options', $this->settings_page_slug, array( $this, 'render_settings_page' ), 'dashicons-format-chat', 80 ); add_submenu_page( $this->settings_page_slug, esc_html__( 'Settings', 'ai-chatbot-builder' ), esc_html__( 'Settings', 'ai-chatbot-builder' ), 'manage_options', $this->settings_page_slug, array( $this, 'render_settings_page' ) ); } /** * Registers the plugin settings, sections, and fields. * * @return void */ public function register_settings(): void { register_setting( $this->option_group, $this->option_name, array( $this, 'sanitize_options' ) ); add_settings_section( 'aichatbotbuilder_openai_section', esc_html__( 'OpenAI API Settings', 'ai-chatbot-builder' ), array( $this, 'render_openai_section' ), $this->settings_page_slug ); add_settings_field( 'openai_api_key', esc_html__( 'OpenAI API Key', 'ai-chatbot-builder' ), array( $this, 'render_api_key_field' ), $this->settings_page_slug, 'aichatbotbuilder_openai_section' ); add_settings_field( 'openai_model', esc_html__( 'OpenAI Model', 'ai-chatbot-builder' ), array( $this, 'render_model_field' ), $this->settings_page_slug, 'aichatbotbuilder_openai_section' ); // Add Usage Quota Settings section and field add_settings_section( 'aichatbotbuilder_quota_section', esc_html__( 'Usage Quota Settings', 'ai-chatbot-builder' ), array( $this, 'render_quota_section' ), $this->settings_page_slug ); add_settings_field( 'daily_token_limit', esc_html__( 'Daily Token Limit', 'ai-chatbot-builder' ), array( $this, 'render_quota_field' ), $this->settings_page_slug, 'aichatbotbuilder_quota_section' ); // Add Usage Statistics section (read-only) add_settings_section( 'aichatbotbuilder_quota_stats_section', esc_html__( 'Usage Statistics', 'ai-chatbot-builder' ), array( $this, 'render_quota_stats_section' ), $this->settings_page_slug ); } /** * Sanitizes the plugin options before saving. * * @param array $input The input options. * @return array Sanitized options. */ public function sanitize_options( array $input ): array { // Nonce verification for settings form. $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : ''; if ( ! $nonce || ! wp_verify_nonce( $nonce, $this->option_group . '-options' ) ) { wp_die( esc_html__( 'Security check failed. Please try again.', 'ai-chatbot-builder' ) ); } $sanitized_input = array(); $options = get_option( $this->option_name ); // Sanitize API Key. $new_api_key = isset( $input['openai_api_key'] ) ? sanitize_text_field( $input['openai_api_key'] ) : null; $old_api_key = $options['openai_api_key'] ?? null; if ( $new_api_key !== null ) { $sanitized_input['openai_api_key'] = $new_api_key; if ( $new_api_key !== $old_api_key ) { $this->clear_models_cache(); } } elseif ( $old_api_key !== null ) { $sanitized_input['openai_api_key'] = $old_api_key; } // Fetch available models for validation. $current_key = $sanitized_input['openai_api_key'] ?? null; $available_models = $this->get_available_models( $current_key ); // Sanitize OpenAI Model. if ( isset( $input['openai_model'] ) && array_key_exists( $input['openai_model'], $available_models ) ) { $sanitized_input['openai_model'] = $input['openai_model']; } else { $old_model = $options['openai_model'] ?? self::DEFAULT_MODEL; if ( array_key_exists( $old_model, $available_models ) ) { $sanitized_input['openai_model'] = $old_model; } else { $sanitized_input['openai_model'] = self::DEFAULT_MODEL; if ( ! empty( $available_models ) && ! array_key_exists( self::DEFAULT_MODEL, $available_models ) ) { if ( function_exists( 'array_key_first' ) ) { $sanitized_input['openai_model'] = array_key_first( $available_models ); } else { reset( $available_models ); $sanitized_input['openai_model'] = key( $available_models ); } } } } // Sanitize Daily Token Limit. if ( isset( $input['daily_token_limit'] ) ) { $daily_limit = intval( $input['daily_token_limit'] ); } else { $daily_limit = self::DEFAULT_DAILY_TOKEN_LIMIT; } $sanitized_input['daily_token_limit'] = max( 0, $daily_limit ); return $sanitized_input; } /** * Renders the description for the OpenAI API Settings section. * * @return void */ public function render_openai_section(): void { echo '

' . esc_html__( 'Enter your OpenAI API key and select the model for AI features.', 'ai-chatbot-builder' ) . '

'; } /** * Renders the OpenAI API Key input field. * * @return void */ public function render_api_key_field(): void { $options = get_option( $this->option_name ); $api_key = $options['openai_api_key'] ?? ''; ?>

', '' ); esc_html_e( ' Saving the key will attempt to fetch the available models.', 'ai-chatbot-builder' ); ?>

option_name ); $selected_model = $opts['openai_model'] ?? self::DEFAULT_MODEL; $models = $this->get_available_models(); // Fallback selection if needed. if ( ! empty( $models ) && ! array_key_exists( $selected_model, $models ) ) { if ( array_key_exists( self::DEFAULT_MODEL, $models ) ) { $selected_model = self::DEFAULT_MODEL; } else { if ( function_exists( 'array_key_first' ) ) { $selected_model = array_key_first( $models ); } else { reset( $models ); $selected_model = key( $models ); } } } ?>

default_models ) : ?>

' . esc_html__( 'Set the maximum number of tokens that can be used per day across all chat instances. Set to 0 for unlimited.', 'ai-chatbot-builder' ) . '

'; } /** * Renders the Daily Token Limit input field. * * @return void */ public function render_quota_field(): void { $opts = get_option( $this->option_name ); $limit = $opts['daily_token_limit'] ?? self::DEFAULT_DAILY_TOKEN_LIMIT; ?>

'', 'count' => 0 ) ); $today = current_time( 'Y-m-d' ); $used = ( isset( $usage_data['date'] ) && $usage_data['date'] === $today ) ? intval( $usage_data['count'] ) : 0; $opts = get_option( $this->option_name ); $limit = isset( $opts['daily_token_limit'] ) ? intval( $opts['daily_token_limit'] ) : self::DEFAULT_DAILY_TOKEN_LIMIT; if ( $limit > 0 ) { $limit_display = number_format_i18n( $limit ); $remaining = max( 0, $limit - $used ); $remaining_display = number_format_i18n( $remaining ); } else { $limit_display = esc_html__( 'Unlimited', 'ai-chatbot-builder' ); $remaining_display = esc_html__( 'Unlimited', 'ai-chatbot-builder' ); } echo '

'; printf( /* translators: %1$s is tokens used today, %2$s is daily token limit, %3$s is tokens remaining. */ esc_html__( 'Tokens used today: %1$s / %2$s (Remaining: %3$s)', 'ai-chatbot-builder' ), esc_html( number_format_i18n( $used ) ), esc_html( $limit_display ), esc_html( $remaining_display ) ); echo '

'; } /** * Renders the settings page HTML. * * @return void */ public function render_settings_page(): void { ?>

option_group ); do_settings_sections( $this->settings_page_slug ); submit_button(); ?>
label on success, WP_Error on failure. */ private function fetch_openai_models( ?string $api_key ) { if ( empty( $api_key ) ) { return new \WP_Error( 'missing_key', esc_html__( 'API key is required to fetch models.', 'ai-chatbot-builder' ) ); } $response = wp_remote_get( 'https://api.openai.com/v1/models', array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, ), 'timeout' => 20, ) ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( $code !== 200 || ! isset( $data['data'] ) || ! is_array( $data['data'] ) ) { $message = isset( $data['error']['message'] ) ? $data['error']['message'] : esc_html__( 'Invalid response from API.', 'ai-chatbot-builder' ); return new \WP_Error( 'api_error', $message, array( 'status' => $code ) ); } $models = array(); foreach ( $data['data'] as $model ) { if ( isset( $model['id'] ) ) { $models[ $model['id'] ] = $model['id']; } } ksort( $models ); if ( empty( $models ) ) { return new \WP_Error( 'no_models', esc_html__( 'No models found via API.', 'ai-chatbot-builder' ) ); } return $models; } /** * Gets available models, using cache if possible. * * @param string|null $api_key Optional API key. * @return array Available models [id => label]. */ private function get_available_models( ?string $api_key = null ): array { $cached = get_transient( self::MODELS_TRANSIENT_KEY ); if ( false !== $cached && is_array( $cached ) ) { return $cached; } if ( null === $api_key ) { $opts = get_option( $this->option_name ); $api_key = $opts['openai_api_key'] ?? null; } $fetched = $this->fetch_openai_models( $api_key ); if ( ! is_wp_error( $fetched ) && ! empty( $fetched ) ) { set_transient( self::MODELS_TRANSIENT_KEY, $fetched, 0 ); return $fetched; } return $this->default_models; } /** * Enqueues scripts for settings page. * * @param string $hook Current admin page hook. */ public function enqueue_settings_scripts( string $hook ): void { if ( 'toplevel_page_' . $this->settings_page_slug !== $hook ) { return; } wp_enqueue_script( 'aichatbotbuilder-settings', AICHATBOTBUILDER_URL . 'assets/js/aichatbotbuilder-settings.js', array( 'jquery' ), AICHATBOTBUILDER_VERSION, true ); wp_localize_script( 'aichatbotbuilder-settings', 'aichatbotbuilderSettings', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'aichatbotbuilder_refresh_models_nonce' ), 'refreshing_text'=> esc_html__( 'Refreshing...', 'ai-chatbot-builder' ), 'refreshed_text' => esc_html__( 'List updated.', 'ai-chatbot-builder' ), 'error_text' => esc_html__( 'Error updating list.', 'ai-chatbot-builder' ), ) ); } /** * Handles the AJAX request to refresh the OpenAI models list. */ public function handle_ajax_refresh_models(): void { check_ajax_referer( 'aichatbotbuilder_refresh_models_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Permission denied.', 'ai-chatbot-builder' ) ), 403 ); } $opts = get_option( $this->option_name ); $apiKey = $opts['openai_api_key'] ?? null; if ( empty( $apiKey ) ) { wp_send_json_error( array( 'message' => esc_html__( 'API Key is not set in the settings page.', 'ai-chatbot-builder' ) ), 400 ); } $this->clear_models_cache(); $fetched = $this->fetch_openai_models( $apiKey ); if ( is_wp_error( $fetched ) ) { wp_send_json_error( array( 'message' => esc_html( $fetched->get_error_message() ) ), 500 ); } if ( empty( $fetched ) ) { wp_send_json_error( array( 'message' => esc_html__( 'No models returned by API.', 'ai-chatbot-builder' ) ), 500 ); } set_transient( self::MODELS_TRANSIENT_KEY, $fetched, 0 ); wp_send_json_success( array( 'models' => $fetched ) ); } }