load(); } /** * Get default settings values. * * @return array */ public function get_defaults(): array { $default_provider = \AISupportAssistant\Providers\WordPressAIProvider::is_available() ? 'wp_native_ai' : 'gemini'; return array( 'ai_provider' => $default_provider, 'openai_api_key' => '', 'openai_chat_model' => '', 'openai_embedding_model' => '', 'gemini_api_key' => '', 'gemini_chat_model' => '', 'gemini_embedding_model' => '', 'vector_store' => 'mysql', 'temperature' => 0.2, 'max_tokens' => 1000, 'top_k' => 5, 'similarity_threshold' => 0.5, 'chunk_size' => 700, 'chunk_overlap' => 100, 'enable_streaming' => 1, 'enable_chat_history' => 1, 'enable_citations' => 1, 'chat_rate_limit' => 20, 'show_powered_by' => 0, 'chat_widget_title' => 'AI Support Assistant', 'chat_widget_greeting' => 'Hello! I am your documentation assistant. Ask me anything, and I will search our guide files to help you.', 'ai_system_instruction' => 'You are a customer support assistant. Answer ONLY using the supplied documentation. Do not invent information. Provide concise and accurate answers.', 'chat_fallback_message' => 'I couldn\'t find that information in the uploaded documentation.', ); } /** * Load settings from database. * * @return void */ private function load(): void { $db_options = get_option( self::OPTION_KEY, array() ); if ( ! is_array( $db_options ) ) { $db_options = array(); } $this->settings = wp_parse_args( $db_options, $this->get_defaults() ); } /** * Get all settings array. * * @return array */ public function get_all(): array { if ( null === $this->settings ) { $this->load(); } return $this->settings; } /** * Get a specific settings value by key. * * @param string $key Settings key. * @param mixed $default Optional default fallback. * @return mixed */ public function get( string $key, $default = null ) { $all = $this->get_all(); if ( array_key_exists( $key, $all ) ) { return $all[ $key ]; } return $default; } /** * Read a provider credential without requiring it to be stored in wp_options. * Hosts may define these constants in wp-config.php. */ public function get_secret( string $key ): string { $constants = array( 'openai_api_key' => 'AI_SUPPORT_ASSISTANT_OPENAI_API_KEY', 'gemini_api_key' => 'AI_SUPPORT_ASSISTANT_GEMINI_API_KEY', ); if ( isset( $constants[ $key ] ) && defined( $constants[ $key ] ) ) { return trim( (string) constant( $constants[ $key ] ) ); } return trim( (string) $this->get( $key, '' ) ); } /** * Update settings. * * @param array $new_settings Settings to update. * @return bool True if value changed and update succeeded. */ public function save( array $new_settings ): bool { $current = $this->get_all(); $sanitized = array(); foreach ( $this->get_defaults() as $key => $default_val ) { if ( isset( $new_settings[ $key ] ) ) { // Secret inputs are intentionally blank in the UI. Blank means preserve the existing value. if ( in_array( $key, array( 'openai_api_key', 'gemini_api_key' ), true ) && '' === trim( (string) $new_settings[ $key ] ) ) { $sanitized[ $key ] = $current[ $key ] ?? $default_val; continue; } if ( is_int( $default_val ) ) { $sanitized[ $key ] = (int) $new_settings[ $key ]; } elseif ( is_float( $default_val ) ) { $sanitized[ $key ] = (float) $new_settings[ $key ]; } else { $sanitized[ $key ] = sanitize_text_field( $new_settings[ $key ] ); } } else { $sanitized[ $key ] = $current[ $key ] ?? $default_val; } } $sanitized['chat_rate_limit'] = max( 1, min( 1000, (int) $sanitized['chat_rate_limit'] ) ); $sanitized['similarity_threshold'] = max( -1.0, min( 1.0, (float) $sanitized['similarity_threshold'] ) ); $sanitized['chunk_size'] = max( 2, min( 8000, (int) $sanitized['chunk_size'] ) ); $sanitized['chunk_overlap'] = max( 0, min( $sanitized['chunk_size'] - 1, (int) $sanitized['chunk_overlap'] ) ); $sanitized['top_k'] = max( 1, min( 50, (int) $sanitized['top_k'] ) ); if ( ! \AISupportAssistant\Providers\WordPressAIProvider::is_available() && 'wp_native_ai' === $sanitized['ai_provider'] ) { $sanitized['ai_provider'] = 'gemini'; } foreach ( array( 'openai_chat_model', 'openai_embedding_model' ) as $model_key ) { if ( '' !== $sanitized[ $model_key ] && ! preg_match( '/^[A-Za-z0-9._:-]+$/', (string) $sanitized[ $model_key ] ) ) { throw new \Exception( esc_html__( 'The configured OpenAI model ID is invalid.', 'ai-support-assistant' ) ); } } foreach ( array( 'gemini_chat_model', 'gemini_embedding_model' ) as $model_key ) { if ( '' !== $sanitized[ $model_key ] && ! preg_match( '/^gemini-[A-Za-z0-9._-]+$/', (string) $sanitized[ $model_key ] ) ) { throw new \Exception( esc_html__( 'The configured Gemini model ID is invalid.', 'ai-support-assistant' ) ); } } $this->validate_api_keys_and_models( $sanitized ); $this->settings = $sanitized; return update_option( self::OPTION_KEY, $sanitized ); } /** * Validate models and API key endpoints live before saving settings. * * @param array $sanitized Settings payload to validate. * @throws \Exception If API key validation or model test fails. */ private function validate_api_keys_and_models( array $sanitized ): void { $old_settings = $this->settings; $this->settings = $sanitized; $provider_name = $sanitized['ai_provider'] ?? 'wp_native_ai'; if ( 'wp_native_ai' === $provider_name ) { return; } if ( 'openai' === $provider_name ) { $direct_key = trim( (string) ( $sanitized['openai_api_key'] ?? '' ) ); if ( ! empty( $direct_key ) ) { $provider = new \AISupportAssistant\Providers\OpenAIProvider( $direct_key ); try { $provider->get_embedding( 'test' ); } catch ( \Exception $e ) { $this->settings = $old_settings; /* translators: %s: OpenAI embedding model validation error message. */ throw new \Exception( esc_html( sprintf( __( 'OpenAI Embedding Model validation failed: %s', 'ai-support-assistant' ), $e->getMessage() ) ) ); } } } elseif ( 'gemini' === $provider_name ) { $direct_key = trim( (string) ( $sanitized['gemini_api_key'] ?? '' ) ); if ( ! empty( $direct_key ) ) { $provider = new \AISupportAssistant\Providers\GeminiProvider( $direct_key ); try { $provider->get_embedding( 'test' ); } catch ( \Exception $e ) { $this->settings = $old_settings; /* translators: %s: Gemini embedding model validation error message. */ throw new \Exception( esc_html( sprintf( __( 'Gemini Embedding Model validation failed: %s', 'ai-support-assistant' ), $e->getMessage() ) ) ); } } } } }