1, 'mode' => 'local', 'hosted_site_key' => '', 'bot_name' => '', 'greeting' => '', 'instructions' => '', 'anthropic_api_key' => '', 'model' => 'claude-sonnet-5', 'woo_orders' => 1, 'woo_products' => 1, 'store_conversations' => 1, 'use_learnings' => 1, 'aicb_base' => 'https://caimunicate.com', 'aicb_agent_key' => '', 'aicb_recipients' => '', 'forward_leads' => 1, 'email_leads' => 1, 'aicb_platform_key' => '', 'aicb_handoff' => '', 'human_wait_secs' => 30, 'handoff_email' => '', ); } /** * Get all options merged with defaults. * * @return array */ public static function all() { $saved = get_option( AICBC_OPTION, array() ); if ( ! is_array( $saved ) ) { $saved = array(); } return array_merge( self::defaults(), $saved ); } /** * Get a single option value. * * @param string $key Option key. * @param mixed $fallback Fallback value. * @return mixed */ public static function get( $key, $fallback = '' ) { $all = self::all(); return isset( $all[ $key ] ) ? $all[ $key ] : $fallback; } /** * The display name for the chat header. * * @return string */ public static function bot_name() { $name = trim( (string) self::get( 'bot_name' ) ); return '' !== $name ? $name : get_bloginfo( 'name' ); } /** * The greeting line shown when a chat opens. * * @return string */ public static function greeting() { $greet = trim( (string) self::get( 'greeting' ) ); if ( '' !== $greet ) { return $greet; } /* translators: %s: site name. */ return sprintf( __( 'Hi! Welcome to %s. How can I help?', 'aicb-chat-widget' ), get_bloginfo( 'name' ) ); } /** * Whether AiCB forwarding is configured. * * @return bool */ public static function can_forward() { $all = self::all(); return '' !== trim( (string) $all['aicb_agent_key'] ) && '' !== trim( (string) $all['aicb_recipients'] ) && '' !== trim( (string) $all['aicb_base'] ); } /** * Parsed list of AiCB recipient handles (without the leading @). * * @return array */ public static function recipients() { $raw = (string) self::get( 'aicb_recipients' ); $list = preg_split( '/[\s,;]+/', $raw, -1, PREG_SPLIT_NO_EMPTY ); $out = array(); foreach ( (array) $list as $h ) { $h = strtolower( ltrim( trim( $h ), '@' ) ); if ( preg_match( '/^[a-z0-9_\-]{2,30}$/', $h ) ) { $out[] = $h; } } return array_values( array_unique( $out ) ); } /** * The @username live human chat connects to (falls back to the first * forwarding recipient). * * @return string Handle without the leading @, '' when unset. */ public static function handoff_handle() { $h = strtolower( ltrim( trim( (string) self::get( 'aicb_handoff' ) ), '@' ) ); if ( preg_match( '/^[a-z0-9_\-]{2,30}$/', $h ) ) { return $h; } $recipients = self::recipients(); return $recipients ? $recipients[0] : ''; } /** * Where escalations and leads are emailed (falls back to the site admin). * * @return string */ public static function handoff_email() { $email = sanitize_email( (string) self::get( 'handoff_email' ) ); return ( '' !== $email && is_email( $email ) ) ? $email : get_option( 'admin_email' ); } /** * Whether live two-way human chat is configured (platform key + handoff). * * @return bool */ public static function human_available() { return '' !== trim( (string) self::get( 'aicb_platform_key' ) ) && '' !== self::handoff_handle() && '' !== trim( (string) self::get( 'aicb_base' ) ); } /** * Seconds to wait for a human reply before offering the email fallback. * * @return int */ public static function human_wait_secs() { return min( 300, max( 5, (int) self::get( 'human_wait_secs', 30 ) ) ); } /** * Register hooks for the settings UI. * * @return void */ public function init() { add_action( 'admin_init', array( $this, 'register_settings' ) ); } /** * Register the option and its sanitizer. * * @return void */ public function register_settings() { register_setting( 'aicbc_group', AICBC_OPTION, array( 'type' => 'array', 'sanitize_callback' => array( $this, 'sanitize' ), 'default' => self::defaults(), ) ); } /** * Sanitize submitted options. * * @param array $input Raw input. * @return array */ public function sanitize( $input ) { $input = is_array( $input ) ? $input : array(); $out = self::defaults(); $prev = self::all(); $out['enabled'] = empty( $input['enabled'] ) ? 0 : 1; $out['woo_orders'] = empty( $input['woo_orders'] ) ? 0 : 1; $out['woo_products'] = empty( $input['woo_products'] ) ? 0 : 1; $out['store_conversations'] = empty( $input['store_conversations'] ) ? 0 : 1; $out['use_learnings'] = empty( $input['use_learnings'] ) ? 0 : 1; $out['forward_leads'] = empty( $input['forward_leads'] ) ? 0 : 1; $out['email_leads'] = empty( $input['email_leads'] ) ? 0 : 1; $mode = isset( $input['mode'] ) ? sanitize_key( $input['mode'] ) : 'local'; $out['mode'] = in_array( $mode, array( 'local', 'hosted' ), true ) ? $mode : 'local'; $model = isset( $input['model'] ) ? sanitize_text_field( $input['model'] ) : ''; $models = array( 'claude-sonnet-5', 'claude-haiku-4-5', 'claude-opus-4-8' ); $out['model'] = in_array( $model, $models, true ) ? $model : 'claude-sonnet-5'; $out['hosted_site_key'] = isset( $input['hosted_site_key'] ) ? sanitize_text_field( $input['hosted_site_key'] ) : ''; $out['aicb_handoff'] = isset( $input['aicb_handoff'] ) ? sanitize_text_field( $input['aicb_handoff'] ) : ''; $out['handoff_email'] = isset( $input['handoff_email'] ) ? sanitize_email( $input['handoff_email'] ) : ''; $out['human_wait_secs'] = isset( $input['human_wait_secs'] ) ? min( 300, max( 5, absint( $input['human_wait_secs'] ) ) ) : 30; $out['bot_name'] = isset( $input['bot_name'] ) ? sanitize_text_field( $input['bot_name'] ) : ''; $out['greeting'] = isset( $input['greeting'] ) ? sanitize_text_field( $input['greeting'] ) : ''; $out['instructions'] = isset( $input['instructions'] ) ? sanitize_textarea_field( $input['instructions'] ) : ''; $out['aicb_recipients'] = isset( $input['aicb_recipients'] ) ? sanitize_text_field( $input['aicb_recipients'] ) : ''; $base = isset( $input['aicb_base'] ) ? esc_url_raw( trim( (string) $input['aicb_base'] ) ) : ''; $out['aicb_base'] = '' !== $base ? untrailingslashit( $base ) : 'https://caimunicate.com'; // Secrets: keep the stored value when the masked placeholder comes back unchanged. $out['anthropic_api_key'] = $this->keep_secret( $input, 'anthropic_api_key', $prev ); $out['aicb_agent_key'] = $this->keep_secret( $input, 'aicb_agent_key', $prev ); $out['aicb_platform_key'] = $this->keep_secret( $input, 'aicb_platform_key', $prev ); return $out; } /** * Resolve a secret field: blank keeps the previous value, "-" clears it. * * @param array $input Raw input. * @param string $key Field key. * @param array $prev Previously saved options. * @return string */ private function keep_secret( $input, $key, $prev ) { $val = isset( $input[ $key ] ) ? trim( (string) $input[ $key ] ) : ''; if ( '' === $val ) { return isset( $prev[ $key ] ) ? (string) $prev[ $key ] : ''; } if ( '-' === $val ) { return ''; } return sanitize_text_field( $val ); } /** * Render the settings page. * * @return void */ public function render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $o = self::all(); ?>