'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_delete_data_on_uninstall', [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_prompt_response_style', [ 'sanitize_callback' => 'salyut_agents_sanitize_text_field_b64', 'default' => 'You are having a conversation, not giving a presentation. Respond to what the person just said in one sentence. Then either ask one question if it genuinely moves the conversation forward, or stop — do not default to a question if the visitor has expressed a clear need, pain point, or readiness to move forward; address it directly instead. Never volunteer multiple points unprompted. Never invent or assume features, facts, or capabilities that are not explicitly stated in your instructions. If you do not know the answer to a question, say so honestly and direct the visitor to the appropriate contact or resource.', ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_default_model_resolved', [ 'type' => 'string', 'sanitize_callback' => 'salyut_agents_sanitize_default_model_override', 'default' => '', ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_chat_disclaimer', [ 'type' => 'string', 'sanitize_callback' => 'salyut_agents_sanitize_html_field', 'default' => salyut_agents_get_default_disclaimer(), ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_field_name', [ 'sanitize_callback' => 'salyut_agents_sanitize_field_state', 'default' => 'optional', ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_field_email', [ 'sanitize_callback' => 'salyut_agents_sanitize_field_state', 'default' => 'optional', ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_field_telephone', [ 'sanitize_callback' => 'salyut_agents_sanitize_field_state', 'default' => 'off', ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_newsletter_optin_enabled', [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_newsletter_optin_label', [ 'type' => 'string', 'sanitize_callback' => 'salyut_agents_sanitize_html_field', 'default' => __( 'Opt in to our newsletter', 'salyut-agents' ), ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_support_email', [ 'sanitize_callback' => 'sanitize_email', 'default' => '', ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_email_notifications', [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_notification_email', [ 'sanitize_callback' => 'sanitize_email', 'default' => '', ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_contact_url', [ 'sanitize_callback' => 'esc_url_raw', 'default' => '', ] ); // --------------------------------------------------------------------------- // WIDGET BEHAVIOUR SETTINGS // --------------------------------------------------------------------------- register_setting( 'salyut_agents_general_group', 'salyut_agents_start_minimized', [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_open_delay', [ 'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 0, ] ); register_setting( 'salyut_agents_general_group', 'salyut_agents_scroll_threshold', [ 'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 0, ] ); // --------------------------------------------------------------------------- // VISITOR IP DETECTION SETTINGS // --------------------------------------------------------------------------- // NOTE: these live in salyut_agents_security_group (not salyut_agents_general_group) so // they render + save under the Security tab. Keeping them in the general group // while rendering them on a separate tab would let each tab's save null out the // other tab's options (options.php processes every option in the submitted group). register_setting( 'salyut_agents_security_group', 'salyut_agents_ip_source', [ 'type' => 'string', 'sanitize_callback' => 'salyut_agents_sanitize_ip_source', 'default' => 'direct', ] ); register_setting( 'salyut_agents_security_group', 'salyut_agents_ip_custom_header', [ 'type' => 'string', 'sanitize_callback' => 'salyut_agents_sanitize_ip_custom_header', 'default' => '', ] ); // --------------------------------------------------------------------------- // BOT PROTECTION (TURNSTILE) SETTINGS // --------------------------------------------------------------------------- register_setting( 'salyut_agents_security_group', 'salyut_agents_turnstile_enabled', [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, ] ); register_setting( 'salyut_agents_security_group', 'salyut_agents_turnstile_site_key', [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '', ] ); // Secret key: server-side only. Never localized to JS or returned in any REST // response — read exclusively during siteverify in salyut_agents_session(). register_setting( 'salyut_agents_security_group', 'salyut_agents_turnstile_secret_key', [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '', ] ); // --------------------------------------------------------------------------- // DESIGN TAB SETTINGS // --------------------------------------------------------------------------- register_setting( 'salyut_agents_design_group', 'salyut_agents_primary_color', [ 'type' => 'string', 'sanitize_callback' => 'sanitize_hex_color', 'default' => '', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_accent_color', [ 'type' => 'string', 'sanitize_callback' => 'sanitize_hex_color', 'default' => '', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_header_text_color', [ 'sanitize_callback' => 'sanitize_hex_color', 'default' => '', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_dept_color', [ 'sanitize_callback' => 'sanitize_hex_color', 'default' => '#888888', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_bot_bubble_bg', [ 'sanitize_callback' => 'sanitize_hex_color', 'default' => '', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_bot_bubble_text', [ 'sanitize_callback' => 'sanitize_hex_color', 'default' => '', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_user_bubble_bg', [ 'sanitize_callback' => 'sanitize_hex_color', 'default' => '', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_user_bubble_text', [ 'sanitize_callback' => 'sanitize_hex_color', 'default' => '', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_minimize_btn_color', [ 'sanitize_callback' => 'sanitize_hex_color', 'default' => '', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_switch_btn_color', [ 'sanitize_callback' => 'sanitize_hex_color', 'default' => '', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_switch_agent_btn_bg', [ 'sanitize_callback' => 'sanitize_hex_color', 'default' => '', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_switch_agent_btn_text', [ 'sanitize_callback' => 'sanitize_hex_color', 'default' => '', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_border_radius', [ 'sanitize_callback' => 'absint', 'default' => 16, ] ); // Widget height — same two-option "mode + companion value" shape as // salyut_agents_ip_source/salyut_agents_ip_custom_header above: a mode // selector (default/custom) plus the pixel value that only applies // when custom is chosen. The pixel value is independently re-clamped // on RENDER too (includes/widget.php), not just here on save — a row // edited directly in the database (migration, direct SQL, a future // import tool) must never be able to push the rendered widget outside // [400, 900]px either. register_setting( 'salyut_agents_design_group', 'salyut_agents_widget_height_mode', [ 'type' => 'string', 'sanitize_callback' => 'salyut_agents_sanitize_widget_height_mode', 'default' => 'default', ] ); register_setting( 'salyut_agents_design_group', 'salyut_agents_widget_height_px', [ 'type' => 'integer', 'sanitize_callback' => 'salyut_agents_sanitize_widget_height_px', 'default' => SALYUT_AGENTS_WIDGET_HEIGHT_DEFAULT_PX, ] ); add_settings_section( 'salyut_agents_general_section', '', 'salyut_agents_general_section_notice_render', 'salyut-agents-chatbot-general' ); add_settings_section( 'salyut_agents_leadcap_section', __( 'Lead Capture', 'salyut-agents' ), 'salyut_agents_section_head_render', 'salyut-agents-chatbot-general' ); add_settings_section( 'salyut_agents_notify_section', __( 'Notifications', 'salyut-agents' ), 'salyut_agents_section_head_render', 'salyut-agents-chatbot-general' ); add_settings_section( 'salyut_agents_behaviour_section',__( 'Widget Behaviour', 'salyut-agents' ), 'salyut_agents_section_head_render', 'salyut-agents-chatbot-general' ); add_settings_section( 'salyut_agents_data_section', __( 'Data', 'salyut-agents' ), 'salyut_agents_section_head_render', 'salyut-agents-chatbot-general' ); add_settings_section( 'salyut_agents_design_section', '', '__return_false', 'salyut-agents-chatbot-design' ); // Security tab. The overview section is read-only (no fields) and registered first // so it renders above the configurable IP-detection and Turnstile sections. add_settings_section( 'salyut_agents_security_overview_section', __( 'Security Overview', 'salyut-agents' ), 'salyut_agents_security_overview_render', 'salyut-agents-chatbot-security' ); add_settings_section( 'salyut_agents_security_section', __( 'Visitor IP Detection', 'salyut-agents' ), 'salyut_agents_section_head_render', 'salyut-agents-chatbot-security' ); add_settings_section( 'salyut_agents_turnstile_section', __( 'Bot Protection (Turnstile)', 'salyut-agents' ), 'salyut_agents_section_head_render', 'salyut-agents-chatbot-security' ); function salyut_agents_section_head_render( $args ) { $blurbs = [ 'salyut_agents_leadcap_section' => __( 'What visitors are asked before a chat begins.', 'salyut-agents' ), 'salyut_agents_notify_section' => __( 'Where new-lead alerts are sent.', 'salyut-agents' ), 'salyut_agents_behaviour_section' => __( 'How and when the widget appears on the page.', 'salyut-agents' ), 'salyut_agents_data_section' => __( 'What happens to your stored data if the plugin is deleted.', 'salyut-agents' ), 'salyut_agents_security_section' => __( 'How the plugin determines each visitor\'s real IP address for rate limiting.', 'salyut-agents' ), 'salyut_agents_turnstile_section' => __( 'Optional Cloudflare Turnstile challenge on new chat sessions — the strongest bot gate. Off by default.', 'salyut-agents' ), ]; $id = $args['id'] ?? ''; if ( ! empty( $blurbs[ $id ] ) ) { echo '

' . esc_html( $blurbs[ $id ] ) . '

'; } } // General /* translators: %s: product display name */ add_settings_field( 'salyut_agents_master_enabled', sprintf( __( '%s Status', 'salyut-agents' ), salyut_agents_display_name() ), 'salyut_agents_master_enabled_render', 'salyut-agents-chatbot-general', 'salyut_agents_general_section' ); add_settings_field( 'salyut_agents_prompt_response_style', __( 'Response Style', 'salyut-agents' ), 'salyut_agents_prompt_response_style_render', 'salyut-agents-chatbot-general', 'salyut_agents_general_section' ); add_settings_field( 'salyut_agents_default_model_resolved', __( 'Default Model', 'salyut-agents' ), 'salyut_agents_default_model_resolved_render', 'salyut-agents-chatbot-general', 'salyut_agents_general_section' ); add_settings_field( 'salyut_agents_chat_disclaimer', __( 'Chatbot Disclaimer', 'salyut-agents' ), 'salyut_agents_chat_disclaimer_render', 'salyut-agents-chatbot-general', 'salyut_agents_general_section' ); add_settings_field( 'salyut_agents_field_name', __( 'Name Field', 'salyut-agents' ), 'salyut_agents_field_name_render', 'salyut-agents-chatbot-general', 'salyut_agents_leadcap_section' ); add_settings_field( 'salyut_agents_field_email', __( 'Email Field', 'salyut-agents' ), 'salyut_agents_field_email_render', 'salyut-agents-chatbot-general', 'salyut_agents_leadcap_section' ); add_settings_field( 'salyut_agents_field_telephone', __( 'Telephone Field', 'salyut-agents' ), 'salyut_agents_field_telephone_render', 'salyut-agents-chatbot-general', 'salyut_agents_leadcap_section' ); add_settings_field( 'salyut_agents_newsletter_optin_enabled', __( 'Newsletter Opt-in', 'salyut-agents' ), 'salyut_agents_newsletter_optin_enabled_render', 'salyut-agents-chatbot-general', 'salyut_agents_leadcap_section' ); add_settings_field( 'salyut_agents_newsletter_optin_label', __( 'Newsletter Opt-in Label', 'salyut-agents' ), 'salyut_agents_newsletter_optin_label_render', 'salyut-agents-chatbot-general', 'salyut_agents_leadcap_section' ); add_settings_field( 'salyut_agents_support_email', __( 'Support Email', 'salyut-agents' ), 'salyut_agents_support_email_render', 'salyut-agents-chatbot-general', 'salyut_agents_notify_section' ); add_settings_field( 'salyut_agents_email_notifications', __( 'Lead Email Notifications', 'salyut-agents' ), 'salyut_agents_email_notifications_render', 'salyut-agents-chatbot-general', 'salyut_agents_notify_section' ); add_settings_field( 'salyut_agents_notification_email', __( 'Notification Email', 'salyut-agents' ), 'salyut_agents_notification_email_render', 'salyut-agents-chatbot-general', 'salyut_agents_notify_section' ); add_settings_field( 'salyut_agents_contact_url', __( 'Contact Page URL', 'salyut-agents' ), 'salyut_agents_contact_url_render', 'salyut-agents-chatbot-general', 'salyut_agents_notify_section' ); // Widget behaviour add_settings_field( 'salyut_agents_start_minimized', __( 'Start Minimised', 'salyut-agents' ), 'salyut_agents_start_minimized_render', 'salyut-agents-chatbot-general', 'salyut_agents_behaviour_section' ); add_settings_field( 'salyut_agents_open_delay', __( 'Auto-open Delay', 'salyut-agents' ), 'salyut_agents_open_delay_render', 'salyut-agents-chatbot-general', 'salyut_agents_behaviour_section' ); add_settings_field( 'salyut_agents_scroll_threshold', __( 'Auto-open on Scroll', 'salyut-agents' ), 'salyut_agents_scroll_threshold_render', 'salyut-agents-chatbot-general', 'salyut_agents_behaviour_section' ); // Data add_settings_field( 'salyut_agents_delete_data_on_uninstall', __( 'On Deletion', 'salyut-agents' ), 'salyut_agents_delete_data_on_uninstall_render', 'salyut-agents-chatbot-general', 'salyut_agents_data_section' ); // Visitor IP detection (Security tab) add_settings_field( 'salyut_agents_ip_source', __( 'CDN / Proxy', 'salyut-agents' ), 'salyut_agents_ip_source_render', 'salyut-agents-chatbot-security', 'salyut_agents_security_section' ); // Bot protection (Turnstile) (Security tab) add_settings_field( 'salyut_agents_turnstile_enabled', __( 'Enable Turnstile', 'salyut-agents' ), 'salyut_agents_turnstile_enabled_render', 'salyut-agents-chatbot-security', 'salyut_agents_turnstile_section' ); add_settings_field( 'salyut_agents_turnstile_site_key', __( 'Site Key', 'salyut-agents' ), 'salyut_agents_turnstile_site_key_render', 'salyut-agents-chatbot-security', 'salyut_agents_turnstile_section' ); add_settings_field( 'salyut_agents_turnstile_secret_key', __( 'Secret Key', 'salyut-agents' ), 'salyut_agents_turnstile_secret_key_render', 'salyut-agents-chatbot-security', 'salyut_agents_turnstile_section' ); // Design Tab add_settings_field( 'salyut_agents_primary_color', __( 'Primary Colour', 'salyut-agents' ), 'salyut_agents_primary_color_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_accent_color', __( 'Accent Colour', 'salyut-agents' ), 'salyut_agents_accent_color_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_header_text_color', __( 'Header Text Colour', 'salyut-agents' ), 'salyut_agents_header_text_color_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_dept_color', __( 'Department Text Colour', 'salyut-agents' ), 'salyut_agents_dept_color_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_minimize_btn_color', __( 'Minimize Icon', 'salyut-agents' ), 'salyut_agents_minimize_btn_color_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_switch_btn_color', __( 'Minimize Button Color', 'salyut-agents' ), 'salyut_agents_switch_btn_color_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_switch_agent_btn_bg', __( 'Switch Agent Button BG', 'salyut-agents' ), 'salyut_agents_switch_agent_btn_bg_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_switch_agent_btn_text', __( 'Switch Agent Button Text', 'salyut-agents' ), 'salyut_agents_switch_agent_btn_text_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_bot_bubble_bg', __( 'Bot Bubble Background', 'salyut-agents' ), 'salyut_agents_bot_bubble_bg_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_bot_bubble_text', __( 'Bot Bubble Text', 'salyut-agents' ), 'salyut_agents_bot_bubble_text_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_user_bubble_bg', __( 'User Bubble Background', 'salyut-agents' ), 'salyut_agents_user_bubble_bg_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_user_bubble_text', __( 'User Bubble Text', 'salyut-agents' ), 'salyut_agents_user_bubble_text_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_border_radius', __( 'Widget Corner Radius', 'salyut-agents' ), 'salyut_agents_border_radius_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); add_settings_field( 'salyut_agents_widget_height', __( 'Chat Widget Height', 'salyut-agents' ), 'salyut_agents_widget_height_render', 'salyut-agents-chatbot-design', 'salyut_agents_design_section' ); } add_action( 'admin_init', 'salyut_agents_register_settings' ); // --------------------------------------------------------------------------- // Admin notices // --------------------------------------------------------------------------- /** * Whether an AI provider is connected and usable for text generation. * * Replaces the old "is the OpenAI API key set" check. Credentials now live in * Settings > Connectors (WP core), so we ask the AI Client directly rather than * reading a stored key. * * @return bool */ function salyut_agents_ai_is_available(): bool { if ( ! function_exists( 'wp_ai_client_prompt' ) ) { return false; } return (bool) wp_ai_client_prompt( 'test' )->is_supported_for_text_generation(); } add_action( 'admin_notices', 'salyut_agents_configuration_notices', 25 ); function salyut_agents_configuration_notices(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $screen = get_current_screen(); if ( ! $screen ) { return; } $master_enabled = (bool) get_option( 'salyut_agents_master_enabled', false ); $ai_available = salyut_agents_ai_is_available(); $settings_url = admin_url( 'edit.php?post_type=salyut_lead&page=salyut-agents-settings' ); $connectors_url = admin_url( 'options-connectors.php' ); $display_name = salyut_agents_display_name(); $is_salyut_agents_page = ( $screen->post_type === 'salyut_lead' || $screen->post_type === 'salyut_agent' || strpos( $screen->id, 'salyut' ) !== false ); // Show globally when the chatbot is on but something is broken $is_broken = $master_enabled && ! $ai_available; if ( ! $is_salyut_agents_page && ! $is_broken ) { return; } $notices = []; if ( ! $ai_available ) { $notices[] = [ 'type' => 'error', 'message' => sprintf( /* translators: 1: product display name, 2: Connectors settings URL */ __( '%1$s: No AI provider connected. The chatbot will not work until you connect a provider in Settings › Connectors.', 'salyut-agents' ), $display_name, esc_url( $connectors_url ) ), ]; } if ( $is_salyut_agents_page && ! $master_enabled ) { $notices[] = [ 'type' => 'warning', 'message' => sprintf( /* translators: 1: product display name, 2: settings page URL, 3: product display name */ __( '%1$s is currently disabled and will not appear anywhere on your website. Enable %3$s', 'salyut-agents' ), $display_name, esc_url( $settings_url ), $display_name ), ]; } if ( $is_salyut_agents_page && $master_enabled && ! salyut_agents_has_active_prompt() ) { $agents_url = admin_url( 'edit.php?post_type=salyut_agent' ); $notices[] = [ 'type' => 'warning', 'message' => sprintf( /* translators: 1: product display name, 2: agents page URL */ __( '%1$s: The chatbot is enabled but has no agents configured. Add an agent.', 'salyut-agents' ), $display_name, esc_url( $agents_url ) ), ]; } if ( $is_salyut_agents_page && get_option( 'salyut_agents_field_name', 'optional' ) === 'off' && get_option( 'salyut_agents_field_email', 'optional' ) === 'off' && get_option( 'salyut_agents_field_telephone', 'off' ) === 'off' ) { $notices[] = [ 'type' => 'warning', 'message' => __( 'No lead capture fields selected. The chatbot will start immediately without collecting information upfront.', 'salyut-agents' ), ]; } if ( $is_salyut_agents_page && ! get_option( 'salyut_agents_default_agent_id', 0 ) ) { $agents_url = admin_url( 'edit.php?post_type=salyut_agent' ); $notices[] = [ 'type' => 'warning', 'message' => sprintf( /* translators: 1: product display name, 2: agents page URL */ __( '%1$s: No default agent assigned. The chatbot will not appear on any page that does not have a specific agent assigned. Assign a default agent.', 'salyut-agents' ), $display_name, esc_url( $agents_url ) ), ]; } foreach ( $notices as $notice ) { printf( '

%s

', esc_attr( $notice['type'] ), wp_kses( $notice['message'], [ 'strong' => [], 'a' => [ 'href' => [] ] ] ) ); } if ( $is_salyut_agents_page ) { salyut_agents_render_provider_connection_notice( $display_name ); } } /** * A short, human reason for one agent's recorded issue, for the provider * connection notice. Never guesses provider-specific detail beyond the AI * Client's own error code and HTTP status. * * @param array $issue One entry from salyut_agents_get_agent_model_issues(), * or [] if the agent only appears in the cached * disconnected-models list (no issue recorded yet * from an actual attempt). * @param array $models salyut_agents_get_available_models(). * @return string */ function salyut_agents_describe_model_issue( array $issue, array $models ): string { if ( empty( $issue ) ) { return __( 'provider not connected', 'salyut-agents' ); } if ( 'mismatch' === ( $issue['type'] ?? '' ) ) { $actual_label = $models[ $issue['actual_model'] ?? '' ]['label'] ?? ( $issue['actual_model'] ?? '' ); return sprintf( /* translators: %s: the model label that actually answered */ __( 'replied from %s instead of the selected model — unexpected, please check', 'salyut-agents' ), $actual_label ); } $error_class = (string) ( $issue['error_class'] ?? '' ); if ( 'provider_not_connected' === $error_class ) { return __( 'provider not connected', 'salyut-agents' ); } if ( 'model_resolution_error' === $error_class ) { return __( 'the model could not be resolved', 'salyut-agents' ); } if ( false !== strpos( $error_class, '(401)' ) || false !== strpos( $error_class, '(403)' ) ) { return __( 'the provider rejected the credentials', 'salyut-agents' ); } if ( false !== strpos( $error_class, '(429)' ) ) { return __( 'the provider’s quota or rate limit was reached', 'salyut-agents' ); } if ( false !== strpos( $error_class, '(402)' ) ) { return __( 'a payment issue with the provider', 'salyut-agents' ); } // Point 4c: a transient class means the provider itself never actually // answered with an error — it simply didn't answer in time (a timeout, // a network error, an "overloaded"/5xx status, or a response that ran // out of budget before producing content) — distinct wording from a // hard failure the provider genuinely rejected. if ( salyut_agents_is_transient_issue_class( $error_class ) ) { return __( 'did not answer in time — may already be working again', 'salyut-agents' ); } return __( 'the provider returned an error', 'salyut-agents' ); } /** * Dismissible warning, shown only on this plugin's own admin screens, listing * every agent (and the default agent) that is not answering because its * selected model's provider is not connected, or whose last attempt failed * for some other reason (an invalid key, an exhausted quota, a payment * failure, etc.) — one line per agent, with its last recorded reason. * * Dismissal is remembered per user against the current set of problems (see * salyut_agents_provider_notice_signature()) — fixing or changing what is * wrong brings the notice back even if a user already dismissed an older * version of it. * * @param string $display_name */ function salyut_agents_render_provider_connection_notice( string $display_name ): void { $disconnected = salyut_agents_get_disconnected_agent_models(); // Point 4b: already drops any transient issue past its TTL — the // provider may simply be working again, and this notice should not // keep claiming otherwise forever. $issues = salyut_agents_get_live_model_issues(); if ( empty( $disconnected ) && empty( $issues ) ) { return; } $signature = salyut_agents_provider_notice_signature( $disconnected, $issues ); $dismissed = get_user_meta( get_current_user_id(), 'salyut_agents_provider_notice_dismissed', true ); if ( $dismissed === $signature ) { return; } $models = salyut_agents_get_available_models(); $connectors_url = admin_url( 'options-connectors.php' ); // Merge the cached (dropdown-marking) list with the per-agent issues // record into one row per affected agent, keyed by agent ID. An agent // with a real recorded issue takes that over the cached list's generic // "provider not connected" guess, since it reflects an actual attempt. $rows = []; foreach ( $disconnected as $item ) { $rows[ $item['agent_id'] ] = [ 'agent_title' => $item['agent_title'], 'model_id' => $item['model_id'], 'model_label' => $models[ $item['model_id'] ]['label'] ?? $item['model_id'], 'is_default_agent' => $item['is_default_agent'], 'issue' => [], ]; } foreach ( $issues as $agent_id => $issue ) { $agent_id = (int) $agent_id; $agent_title = get_the_title( $agent_id ); $model_id = $issue['model'] ?? $issue['requested_model'] ?? ''; if ( '' === $agent_title ) { $agent_title = __( '(unknown agent)', 'salyut-agents' ); } $rows[ $agent_id ] = [ 'agent_title' => $agent_title, 'model_id' => $model_id, 'model_label' => $models[ $model_id ]['label'] ?? $model_id, 'is_default_agent' => $agent_id === (int) get_option( 'salyut_agents_default_agent_id', 0 ), 'issue' => $issue, ]; } // Point 4d: one last live try per agent, before ever showing the // banner — a provider that has simply come back clears itself here // instead of waiting for the next real visitor message. foreach ( $rows as $agent_id => $row ) { if ( '' === $row['model_id'] ) { continue; } $model_config = salyut_agents_get_model_config( $row['model_id'] ); if ( ! $model_config ) { continue; } if ( salyut_agents_maybe_quick_recheck_agent( $agent_id, $row['model_id'], $model_config ) ) { unset( $rows[ $agent_id ] ); } } if ( empty( $rows ) ) { return; } echo '
'; printf( '

%s

', esc_html( sprintf( /* translators: %s: product display name */ __( '%s: the agents below are not answering until this is fixed.', 'salyut-agents' ), $display_name ) ) ); echo ''; printf( '

%2$s · %4$s

', esc_url( $connectors_url ), esc_html__( 'Connect a provider', 'salyut-agents' ), esc_url( wp_nonce_url( add_query_arg( 'salyut_agents_dismiss_provider_notice', '1' ), 'salyut_agents_dismiss_provider_notice' ) ), esc_html__( 'Dismiss', 'salyut-agents' ) ); echo '
'; } function salyut_agents_has_active_prompt(): bool { // Configured == at least one published agent exists. $agents = get_posts( [ 'post_type' => 'salyut_agent', 'post_status' => 'publish', 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => true, ] ); return ! empty( $agents ); } // --------------------------------------------------------------------------- // Field render callbacks // --------------------------------------------------------------------------- function salyut_agents_master_enabled_render() { $enabled = get_option( 'salyut_agents_master_enabled', false ); ?>

'OpenAI', 'anthropic' => 'Anthropic', 'google' => 'Google', ]; $provider_label = fn( string $p ): string => $provider_labels[ $p ] ?? ucfirst( $p ); $models_by_provider = []; foreach ( $selectable_models as $model_id => $model ) { $provider = $model['provider'] ?? 'other'; $models_by_provider[ $provider ][ $model_id ] = $model; } ?>

%2$s Terms of Service.', 'salyut-agents' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [] ] ] ), 'https://salyutplugins.com/terms-of-service', esc_html( $name ) ); ?>

Terms and Conditions.', 'salyut-agents' ); } function salyut_agents_chat_disclaimer_render() { $raw = get_option( 'salyut_agents_chat_disclaimer', salyut_agents_get_default_disclaimer() ); $decoded = base64_decode( $raw, true ); $content = ( $decoded !== false && base64_encode( $decoded ) === $raw ) ? $decoded : $raw; ?>

'salyut_agents_chat_disclaimer', 'textarea_rows' => 5, 'media_buttons' => false, 'teeny' => true, 'quicktags' => true, ] ); } function salyut_agents_field_name_render() { $value = get_option( 'salyut_agents_field_name', 'optional' ); ?>

'salyut_agents_newsletter_optin_label', 'textarea_rows' => 3, 'media_buttons' => false, 'teeny' => true, 'quicktags' => true, ] ); } function salyut_agents_support_email_render() { $email = get_option( 'salyut_agents_support_email', '' ); ?>

Start Minimised is on. Set to 0 to disable.', 'salyut-agents' ), [ 'strong' => [] ] ); ?>

seconds 0 ) : ?>

Widget will expand second after page load.

Start Minimised is on. Set to 0 to disable.', 'salyut-agents' ), [ 'strong' => [] ] ); ?>

px 0 ) : ?>

Widget will expand after px of scroll.

↗

__( 'Direct connection (no CDN/proxy)', 'salyut-agents' ), 'cloudflare' => __( 'Cloudflare', 'salyut-agents' ), 'akamai' => __( 'Akamai', 'salyut-agents' ), 'sucuri' => __( 'Sucuri', 'salyut-agents' ), 'custom' => __( 'Custom header', 'salyut-agents' ), ]; ?>



Security warning: a chosen header is only trustworthy if your origin server is locked down to accept traffic only from your CDN. Otherwise an attacker can bypass the CDN, connect to your origin directly, and forge that header to spoof any IP they like.', 'salyut-agents' ), [ 'strong' => [], 'em' => [] ] ); ?>

Cloudflare Turnstile dashboard.', 'salyut-agents' ), [ 'strong' => [] ] ); ?>

Managed (recommended) or Invisible — not Non-Interactive, which clutters the chat. Add your site\'s domain to the widget\'s allowed hostnames, then copy the Site Key and Secret Key below. See the security documentation for full setup steps.', 'salyut-agents' ), esc_url( $doc_url ) ), [ 'strong' => [], 'a' => [ 'href' => [], 'target' => [], 'rel' => [] ], ] ); ?>

' . esc_html__( 'Text colour in the chat header.', 'salyut-agents' ) . '

'; echo ''; } function salyut_agents_dept_color_render() { $color = get_option( 'salyut_agents_dept_color', '#888888' ); echo '

' . esc_html__( 'Colour of the agent department label shown in the chat header.', 'salyut-agents' ) . '

'; echo ''; } function salyut_agents_bot_bubble_bg_render() { $color = get_option( 'salyut_agents_bot_bubble_bg', '' ); echo '

' . esc_html__( 'Background colour of bot chat bubbles.', 'salyut-agents' ) . '

'; echo ''; } function salyut_agents_bot_bubble_text_render() { $color = get_option( 'salyut_agents_bot_bubble_text', '' ); echo '

' . esc_html__( 'Text colour of bot chat bubbles.', 'salyut-agents' ) . '

'; echo ''; } function salyut_agents_user_bubble_bg_render() { $color = get_option( 'salyut_agents_user_bubble_bg', '' ); echo '

' . esc_html__( 'Background colour of user chat bubbles.', 'salyut-agents' ) . '

'; echo ''; } function salyut_agents_user_bubble_text_render() { $color = get_option( 'salyut_agents_user_bubble_text', '' ); echo '

' . esc_html__( 'Text colour of user chat bubbles.', 'salyut-agents' ) . '

'; echo ''; } function salyut_agents_minimize_btn_color_render() { $color = get_option( 'salyut_agents_minimize_btn_color', '' ); echo '

' . esc_html__( 'Colour of the − (minus) icon in the chat header that minimises the widget.', 'salyut-agents' ) . '

'; echo ''; } function salyut_agents_switch_btn_color_render() { $color = get_option( 'salyut_agents_switch_btn_color', '' ); echo '

' . esc_html__( 'Background colour of the floating button that reopens the chat widget when it is minimised.', 'salyut-agents' ) . '

'; echo ''; } function salyut_agents_switch_agent_btn_bg_render() { $color = get_option( 'salyut_agents_switch_agent_btn_bg', '' ); echo '

' . esc_html__( 'Background colour of the "Yes, switch agents" button in the agent handoff prompt.', 'salyut-agents' ) . '

'; echo ''; } function salyut_agents_switch_agent_btn_text_render() { $color = get_option( 'salyut_agents_switch_agent_btn_text', '' ); echo '

' . esc_html__( 'Text colour of the "Yes, switch agents" button in the agent handoff prompt.', 'salyut-agents' ) . '

'; echo ''; } function salyut_agents_border_radius_render() { $value = get_option( 'salyut_agents_border_radius', 16 ); ?> px


px

post_type === 'salyut_lead' && $screen->base === 'post' ) { return 'edit.php?post_type=salyut_lead'; } return $parent_file; } function salyut_agents_fix_submenu_highlight( $submenu_file ) { $screen = get_current_screen(); if ( ! $screen ) return $submenu_file; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- reading the request only to detect the current admin screen for menu highlighting; nothing is processed. $is_leads_list = $screen->base === 'edit' && $screen->post_type === 'salyut_lead' && empty( $_GET['page'] ); $is_leads_post = $screen->base === 'post' && $screen->post_type === 'salyut_lead'; if ( $is_leads_list || $is_leads_post ) { return 'edit.php?post_type=salyut_lead&view=list'; } return $submenu_file; } // --------------------------------------------------------------------------- // Settings page renderer // --------------------------------------------------------------------------- function salyut_agents_render_settings_page() { if ( ! current_user_can( 'manage_options' ) ) return; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- reading the request only to pick which settings tab to render; the form itself is protected by the Settings API nonce on save. $current_tab = sanitize_text_field( wp_unslash( $_GET['tab'] ?? 'general' ) ); $base_url = admin_url( 'edit.php?post_type=salyut_lead&page=salyut-agents-settings' ); ?>