label, default model, key setting, key help URL. */ function avcap_ai_providers() { return array( 'openai' => array( 'label' => 'OpenAI (ChatGPT)', 'model' => 'gpt-6-luna', 'key_url' => 'https://platform.openai.com/api-keys', ), 'gemini' => array( 'label' => 'Google Gemini', 'model' => 'gemini-3.1-flash-lite', 'key_url' => 'https://aistudio.google.com/app/apikey', ), 'anthropic' => array( 'label' => 'Anthropic Claude', 'model' => 'claude-haiku-5-5', 'key_url' => 'https://console.anthropic.com/settings/keys', ), 'openrouter' => array( 'label' => 'OpenRouter (many models)', 'model' => 'openrouter/auto', 'key_url' => 'https://openrouter.ai/keys', ), 'deepseek' => array( 'label' => 'DeepSeek', 'model' => 'deepseek-chat', 'key_url' => 'https://platform.deepseek.com/api_keys', ), ); } /** * Zoho data centres. * * @return array Code => label. */ function avcap_zoho_data_centres() { return array( 'com' => __( 'United States (zoho.com)', 'avi-support-assistant-pro' ), 'in' => __( 'India (zoho.in)', 'avi-support-assistant-pro' ), 'eu' => __( 'Europe (zoho.eu)', 'avi-support-assistant-pro' ), 'com.au' => __( 'Australia (zoho.com.au)', 'avi-support-assistant-pro' ), 'jp' => __( 'Japan (zoho.jp)', 'avi-support-assistant-pro' ), 'ca' => __( 'Canada (zohocloud.ca)', 'avi-support-assistant-pro' ), 'sa' => __( 'Saudi Arabia (zoho.sa)', 'avi-support-assistant-pro' ), ); } /** * Default Pro settings. * * @return array */ function avcap_get_defaults() { $defaults = array( // AI. 'ai_enabled' => 0, 'ai_provider' => 'openai', 'ai_instructions' => __( 'You are a friendly, helpful support assistant for this website. Be concise and polite. Only answer questions about this business, its products and services. If you are not sure, say so and suggest the visitor leaves their details so the team can help.', 'avi-support-assistant-pro' ), 'ai_use_faqs' => 1, 'ai_use_content' => 1, 'ai_match_lang' => 1, 'ai_badge' => 1, 'ai_temperature' => '0.3', 'ai_max_tokens' => 400, 'ai_monthly_cap' => 2000, 'ai_visitor_cap' => 20, // Voice. 'voice_input' => 1, 'voice_output' => 'after_voice', // CRM. 'zoho_enabled' => 0, 'zoho_dc' => 'in', 'zoho_client_id' => '', 'zoho_secret' => '', 'zoho_refresh' => '', 'zoho_source' => __( 'Website Chat', 'avi-support-assistant-pro' ), 'webhook_url' => '', 'webhook_secret' => '', // WooCommerce. 'woo_products' => 1, 'woo_orders' => 1, 'woo_auto' => 0, // Office hours. 'hours_enabled' => 0, 'hours' => array(), 'closed_message' => __( "We're closed right now, but you can still ask me questions or leave your details and we'll get back to you when we open.", 'avi-support-assistant-pro' ), 'closed_hide' => 1, // Languages. 'translate' => 1, // History. 'history_enabled' => 1, 'retention_days' => 90, ); foreach ( avcap_ai_providers() as $provider => $info ) { $defaults[ 'key_' . $provider ] = ''; $defaults[ 'model_' . $provider ] = $info['model']; } // Monday–Saturday 09:30–18:30 by default. for ( $day = 0; $day <= 6; $day++ ) { $defaults['hours'][ $day ] = array( 'open' => 0 === $day ? 0 : 1, 'from' => '09:30', 'to' => '18:30', ); } return $defaults; } /** * Get Pro settings merged with defaults. * * @return array */ function avcap_get_settings() { $saved = get_option( AVCAP_OPTION, array() ); return array_intersect_key( wp_parse_args( is_array( $saved ) ? $saved : array(), avcap_get_defaults() ), avcap_get_defaults() ); } /** * Get one Pro setting. * * @param string $key Key. * @return mixed */ function avcap_get( $key ) { $settings = avcap_get_settings(); return isset( $settings[ $key ] ) ? $settings[ $key ] : null; } /** * Sanitize a HH:MM time. * * @param string $value Time. * @param string $default Fallback. * @return string */ function avcap_sanitize_time( $value, $default ) { return preg_match( '/^([01]\d|2[0-3]):[0-5]\d$/', (string) $value ) ? (string) $value : $default; } /** * Sanitize Pro settings. * * Secret fields (API keys, Zoho secret and refresh token) keep their saved value when * the field is submitted empty, so they never have to be printed back into the page. * * @param array $input Raw input. * @return array */ function avcap_sanitize_settings( $input ) { $input = is_array( $input ) ? $input : array(); $defaults = avcap_get_defaults(); $current = avcap_get_settings(); $out = array(); $checkboxes = array( 'ai_enabled', 'ai_use_faqs', 'ai_use_content', 'ai_match_lang', 'ai_badge', 'voice_input', 'zoho_enabled', 'woo_products', 'woo_orders', 'woo_auto', 'hours_enabled', 'closed_hide', 'translate', 'history_enabled' ); foreach ( $checkboxes as $key ) { $out[ $key ] = empty( $input[ $key ] ) ? 0 : 1; } $provider = isset( $input['ai_provider'] ) ? sanitize_key( $input['ai_provider'] ) : ''; $out['ai_provider'] = array_key_exists( $provider, avcap_ai_providers() ) ? $provider : $defaults['ai_provider']; $voice = isset( $input['voice_output'] ) ? sanitize_key( $input['voice_output'] ) : ''; $out['voice_output'] = in_array( $voice, array( 'off', 'after_voice', 'always' ), true ) ? $voice : $defaults['voice_output']; $dc = isset( $input['zoho_dc'] ) ? sanitize_text_field( $input['zoho_dc'] ) : ''; $out['zoho_dc'] = array_key_exists( $dc, avcap_zoho_data_centres() ) ? $dc : $defaults['zoho_dc']; foreach ( array( 'ai_instructions', 'closed_message' ) as $key ) { $out[ $key ] = isset( $input[ $key ] ) ? sanitize_textarea_field( $input[ $key ] ) : ''; } foreach ( array( 'zoho_client_id', 'zoho_source' ) as $key ) { $out[ $key ] = isset( $input[ $key ] ) ? sanitize_text_field( $input[ $key ] ) : ''; } // Secrets: empty submission keeps the saved value; "-" clears it. $secrets = array( 'zoho_secret', 'zoho_refresh', 'webhook_secret' ); foreach ( array_keys( avcap_ai_providers() ) as $p ) { $secrets[] = 'key_' . $p; } foreach ( $secrets as $key ) { $value = isset( $input[ $key ] ) ? trim( sanitize_text_field( $input[ $key ] ) ) : ''; if ( '-' === $value ) { $out[ $key ] = ''; } elseif ( '' === $value ) { $out[ $key ] = $current[ $key ]; } else { $out[ $key ] = $value; } } foreach ( avcap_ai_providers() as $p => $info ) { $model = isset( $input[ 'model_' . $p ] ) ? sanitize_text_field( $input[ 'model_' . $p ] ) : ''; $model = preg_replace( '/[^A-Za-z0-9._:\/\-]/', '', $model ); $out[ 'model_' . $p ] = '' !== $model ? $model : $info['model']; } $temperature = isset( $input['ai_temperature'] ) ? (float) $input['ai_temperature'] : 0.3; $out['ai_temperature'] = (string) max( 0, min( 1, round( $temperature, 2 ) ) ); $out['ai_max_tokens'] = isset( $input['ai_max_tokens'] ) ? max( 50, min( 2000, absint( $input['ai_max_tokens'] ) ) ) : 400; $out['ai_monthly_cap'] = isset( $input['ai_monthly_cap'] ) ? min( 1000000, absint( $input['ai_monthly_cap'] ) ) : 0; $out['ai_visitor_cap'] = isset( $input['ai_visitor_cap'] ) ? max( 1, min( 500, absint( $input['ai_visitor_cap'] ) ) ) : 20; $out['retention_days'] = isset( $input['retention_days'] ) ? max( 1, min( 3650, absint( $input['retention_days'] ) ) ) : 90; $url = isset( $input['webhook_url'] ) ? esc_url_raw( trim( (string) $input['webhook_url'] ), array( 'https', 'http' ) ) : ''; $out['webhook_url'] = $url; $out['hours'] = array(); for ( $day = 0; $day <= 6; $day++ ) { $row = isset( $input['hours'][ $day ] ) && is_array( $input['hours'][ $day ] ) ? $input['hours'][ $day ] : array(); $out['hours'][ $day ] = array( 'open' => empty( $row['open'] ) ? 0 : 1, 'from' => avcap_sanitize_time( isset( $row['from'] ) ? $row['from'] : '', '09:30' ), 'to' => avcap_sanitize_time( isset( $row['to'] ) ? $row['to'] : '', '18:30' ), ); } if ( $out['ai_enabled'] && '' === $out[ 'key_' . $out['ai_provider'] ] ) { add_settings_error( AVCAP_OPTION, 'avcap_no_key', __( 'AI answers are on, but there is no API key for the selected provider. Add a key or AI answers will be skipped.', 'avi-support-assistant-pro' ), 'warning' ); } if ( $out['zoho_enabled'] && ( '' === $out['zoho_client_id'] || '' === $out['zoho_secret'] || '' === $out['zoho_refresh'] ) ) { add_settings_error( AVCAP_OPTION, 'avcap_zoho_incomplete', __( 'Zoho CRM sync is on, but the Client ID, Client Secret or Refresh Token is missing.', 'avi-support-assistant-pro' ), 'warning' ); } // New credentials: forget the cached Zoho access token. if ( $out['zoho_refresh'] !== $current['zoho_refresh'] || $out['zoho_dc'] !== $current['zoho_dc'] || $out['zoho_client_id'] !== $current['zoho_client_id'] ) { delete_transient( 'avcap_zoho_token' ); } return $out; } /** * Shared per-visitor rate limit (same approach as the free plugin). * * @param string $action Action. * @param int $limit Max requests. * @param int $window Seconds. * @return bool True when over the limit. */ function avcap_rate_limited( $action, $limit, $window ) { return avca_rate_limited( 'pro_' . $action, $limit, $window ); }