is_activated; } /** * Capability that opens the Transcripts screen read-only (plan 93999a). * * Defaults to the dedicated mxchat_view_transcripts capability, which * activation grants to administrators. A site that would rather reuse a * capability its staff already have can return it from the * mxchat_transcripts_capability filter. * * @return string */ public static function mxchat_transcripts_capability() { $cap = apply_filters('mxchat_transcripts_capability', 'mxchat_view_transcripts'); return (is_string($cap) && $cap !== '') ? $cap : 'mxchat_view_transcripts'; } /** * Whether the current user may READ transcripts: list, open, search and * export conversations and leads (plan 93999a). Administrators always can, * whether or not the capability was granted to their role. Every write on * the Transcripts screen (delete, translate, notification and retention * settings) keeps requiring manage_options. * * @return bool */ public static function mxchat_can_view_transcripts() { return current_user_can('manage_options') || current_user_can(self::mxchat_transcripts_capability()); } /** * woocommerce_prevent_admin_access: a user who holds the transcripts * capability reaches wp-admin, as they would on a site without WooCommerce * (plan 93999a). Every other user keeps WooCommerce's decision. * * @param bool $prevent * @return bool */ public function mxchat_let_transcripts_readers_into_admin($prevent) { if ($prevent && current_user_can(self::mxchat_transcripts_capability())) { return false; } return $prevent; } /** * Get plugin options * @return array */ public function get_options() { return $this->options; } public function __construct($knowledge_manager = null) { $this->options = get_option('mxchat_options'); $this->chat_count = get_option('mxchat_chat_count', 0); $this->is_activated = $this->is_license_active(); $this->knowledge_manager = $knowledge_manager; // Seed defaults on init, not here — the constructor runs on // plugins_loaded, before the mxchat textdomain loads (init, priority 10), // so translating the default strings here both triggers WP 6.7's // _load_textdomain_just_in_time notice and stores the untranslated // English fallbacks. Priority 20 lands after mxchat_load_textdomain(). if (!$this->options) { add_action('init', array($this, 'initialize_default_options'), 20); } // Add admin menu and initialize settings add_action('admin_menu', array($this, 'mxchat_add_plugin_page')); // Pro & Extensions registers at priority 30 so it always lands last in // the sidebar — below API Access (priority 20) and below any other // submenu that hooks at default priority 10. add_action('admin_menu', array($this, 'mxchat_add_pro_extensions_page'), 30); // Onboarding visibility — runs LAST so it can remove the submenu after every // other add_submenu_page() call. The page stays reachable by direct URL. add_action('admin_menu', array($this, 'mxchat_apply_onboarding_visibility'), 999); add_action('admin_init', array($this, 'mxchat_page_init')); add_action('admin_init', array($this, 'mxchat_prompts_page_init')); add_action('admin_enqueue_scripts', array($this, 'mxchat_enqueue_admin_assets')); // Add body class for the Onboarding wizard (plan-905439) so the // chrome-surgery CSS in admin-onboarding-wizard.css can scope its // WP-sidebar collapse to this page only. add_filter('admin_body_class', array($this, 'mxchat_add_onboarding_body_class')); add_action('wp_ajax_mxchat_delete_chat_history', array($this, 'mxchat_delete_chat_history')); add_action('admin_post_mxchat_delete_prompt', array($this, 'mxchat_handle_delete_prompt')); add_action('wp_ajax_mxchat_fetch_chat_history', array($this, 'mxchat_fetch_chat_history')); add_action('wp_ajax_nopriv_mxchat_fetch_chat_history', array($this, 'mxchat_fetch_chat_history')); add_action('wp_ajax_mxchat_fetch_conversation', array($this, 'mxchat_fetch_conversation')); add_action('wp_footer', array($this, 'mxchat_append_chatbot_to_body')); add_action('admin_head-mxchat-prompts', array($this, 'mxchat_enqueue_admin_assets')); add_action('admin_head-toplevel_page_mxchat-max', array($this, 'mxchat_enqueue_admin_assets')); add_action('admin_notices', array($this, 'mxchat_display_admin_notice')); add_action('admin_post_mxchat_delete_all_prompts', array($this, 'mxchat_handle_delete_all_prompts')); add_action('admin_post_mxchat_add_intent', array($this, 'mxchat_handle_add_intent')); add_action('admin_post_mxchat_delete_intent', array($this, 'mxchat_handle_delete_intent')); add_action('admin_post_mxchat_edit_intent', array($this, 'mxchat_handle_edit_intent')); add_action('wp_ajax_mxchat_export_transcripts', array($this, 'export_chat_transcripts')); // Leads tab (inside Transcripts) add_action('wp_ajax_mxchat_fetch_leads', array($this, 'mxchat_fetch_leads')); add_action('wp_ajax_mxchat_delete_leads', array($this, 'mxchat_delete_leads')); add_action('wp_ajax_mxchat_export_leads', array($this, 'mxchat_export_leads')); add_action('admin_init', array($this, 'mxchat_transcripts_page_init')); // WooCommerce sends anyone without edit_posts from wp-admin to My // Account, which would lock the read-only transcripts role out of the // only screen it is for (plan 93999a). add_filter('woocommerce_prevent_admin_access', array($this, 'mxchat_let_transcripts_readers_into_admin')); add_action('wp_ajax_dismiss_live_agent_notice', array($this, 'dismiss_live_agent_notice')); add_action('wp_ajax_dismiss_theme_migration_notice', array($this, 'dismiss_theme_migration_notice')); add_action('mxchat_cleanup_old_transcripts', array($this, 'cleanup_old_transcripts')); // Self-heal: the cleanup event is otherwise only scheduled at activation or // when the retention setting CHANGES — if it is ever lost (deactivate cycle, // cron-option wipe, DB restore) nothing re-registers it and retention goes // silently inert while the UI still says it is on (plan-bc08a6). add_action('admin_init', array($this, 'ensure_transcript_cleanup_scheduled')); add_action('admin_init', array($this, 'register_pinecone_settings')); add_action('admin_init', array($this, 'register_openai_vectorstore_settings')); add_action('admin_notices', array($this, 'display_admin_notices')); add_action('wp_ajax_mxchat_test_vectorstore_connection', array($this, 'mxchat_test_vectorstore_connection')); // wp_ajax_mxchat_save_selected_bot is registered (and handled) in // admin/class-ajax-handler.php — a duplicate registration here pointed // at a method this class never defined, a load-order-dependent fatal // waiting to fire (plan 52bcad). add_action('wp_ajax_mxchat_fetch_openrouter_models', array($this, 'fetch_openrouter_models')); add_action('wp_ajax_mxchat_get_rag_context', array($this, 'mxchat_get_rag_context')); // Actions page AJAX handlers add_action('wp_ajax_mxchat_fetch_actions_list', array($this, 'mxchat_fetch_actions_list')); add_action('wp_ajax_mxchat_toggle_action_status', array($this, 'mxchat_toggle_action_status')); add_action('wp_ajax_mxchat_bulk_delete_actions', array($this, 'mxchat_bulk_delete_actions')); add_action('wp_ajax_mxchat_add_intent_ajax', array($this, 'mxchat_add_intent_ajax')); add_action('wp_ajax_mxchat_edit_intent_ajax', array($this, 'mxchat_edit_intent_ajax')); add_action('wp_ajax_mxchat_delete_intent_ajax', array($this, 'mxchat_delete_intent_ajax')); add_action('wp_ajax_mxchat_add_phrase', array($this, 'mxchat_add_phrase_ajax')); add_action('wp_ajax_mxchat_delete_phrase', array($this, 'mxchat_delete_phrase_ajax')); add_action('wp_ajax_mxchat_get_phrases', array($this, 'mxchat_get_phrases_ajax')); add_action('wp_ajax_mxchat_delete_legacy_phrases', array($this, 'mxchat_delete_legacy_phrases_ajax')); // Slack test connection add_action('wp_ajax_mxchat_test_slack_connection', array($this, 'mxchat_test_slack_connection')); // Translation handlers add_action('wp_ajax_mxchat_translate_messages', array($this, 'mxchat_translate_messages')); add_action('wp_ajax_mxchat_get_transcript_translation', array($this, 'mxchat_get_transcript_translation')); // 3.2.3: Embedding model switch protection add_action('wp_ajax_mxchat_check_embedding_switch', array($this, 'mxchat_check_embedding_switch_ajax')); add_action('wp_ajax_mxchat_dismiss_embedding_mismatch', array($this, 'mxchat_dismiss_embedding_mismatch_ajax')); add_action('wp_ajax_mxchat_dismiss_telegram_secret_notice', array($this, 'mxchat_dismiss_telegram_secret_notice_ajax')); add_action('admin_notices', array($this, 'mxchat_embedding_mismatch_notice')); add_action('admin_notices', array($this, 'mxchat_telegram_secret_notice')); } /** * Nudge admins to configure a Telegram webhook secret (plan-0c17b5). * * Shown only when a Telegram bot token is configured (integration active) * AND no webhook secret is set. Without a secret the webhook falls back to a * Telegram source-IP check — safer than the old fail-open, but a real secret * is the recommended protection. Dismissible; does not block anything. */ public function mxchat_telegram_secret_notice() { if (!current_user_can('manage_options')) { return; } $options = get_option('mxchat_options', array()); $has_token = !empty($options['telegram_bot_token']); $has_secret = !empty($options['telegram_webhook_secret']); if (!$has_token) { return; } // Inbound rejections recorded by verify_telegram_request() (plan 30398e). // When the webhook is actually being rejected this stops being a hardening // suggestion and becomes an outage report: it shows even when a secret IS // set (a wrong or unregistered secret rejects just as hard as none), and it // ignores the dismissal flag, because a nudge someone dismissed in July // must not silence live breakage. It self-clears on the first accepted // request, so a fixed install stops nagging without anyone touching it. $rejects = get_option('mxchat_telegram_webhook_rejects', array()); $reject_count = (is_array($rejects) && !empty($rejects['count'])) ? (int) $rejects['count'] : 0; if ($reject_count < 1) { // Nothing is failing — the original advisory, unchanged. if ($has_secret) { return; } if (get_option('mxchat_dismissed_telegram_secret_notice', '') === '1') { return; } } $settings_url = admin_url('admin.php?page=mxchat-settings#integrations-telegram'); ?> 0) : ?>
$is_mismatch, 'active_model' => $active_model, 'active_label' => MxChat_Utils::embedding_model_label($active_model), 'new_model' => $new_model, 'new_label' => MxChat_Utils::embedding_model_label($new_model), 'dims_differ' => ($active_dims > 0 && $new_dims > 0 && $active_dims !== $new_dims), 'active_dims' => $active_dims, 'new_dims' => $new_dims, )); } /** * 3.2.3: Dismiss the persistent mismatch banner. Tied to the active+selected * pair so the banner reappears on the next switch event. */ public function mxchat_dismiss_embedding_mismatch_ajax() { check_ajax_referer('mxchat_admin_nonce', 'security'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Unauthorized', 'mxchat')); } $options = get_option('mxchat_options', array()); $selected = MxChat_Utils::get_selected_embedding_model($options); $active = MxChat_Utils::get_active_embedding_model(); update_option('mxchat_dismissed_embedding_mismatch', $active . '|' . $selected, false); wp_send_json_success(); } /** * 3.2.3: Persistent admin banner shown whenever the active embedding model * (last used to actually embed something) differs from the currently * selected model. Pure option comparison — no DB queries on every page * load. The banner auto-clears once both match again, i.e. after a delete * + re-embed cycle. */ public function mxchat_embedding_mismatch_notice() { if (!current_user_can('manage_options')) { return; } $options = get_option('mxchat_options', array()); $selected = MxChat_Utils::get_selected_embedding_model($options); $active = MxChat_Utils::get_active_embedding_model(); if (empty($active) || empty($selected) || $active === $selected) { return; } $dismissed = get_option('mxchat_dismissed_embedding_mismatch', ''); if ($dismissed === $active . '|' . $selected) { return; } $active_label = MxChat_Utils::embedding_model_label($active); $selected_label = MxChat_Utils::embedding_model_label($selected); $active_dims = MxChat_Utils::embedding_model_dimensions($active); $selected_dims = MxChat_Utils::embedding_model_dimensions($selected); $dims_differ = ($active_dims > 0 && $selected_dims > 0 && $active_dims !== $selected_dims); $kb_url = admin_url('admin.php?page=mxchat-prompts'); $actions_url = admin_url('admin.php?page=mxchat-actions'); ?> '', 'xai_api_key' => '', 'claude_api_key' => '', 'deepseek_api_key' => '', 'voyage_api_key' => '', 'gemini_api_key' => '', 'enable_streaming_toggle' => 'off', 'enable_web_search' => 'off', 'embedding_model' => 'text-embedding-ada-002', 'system_prompt_instructions' => 'You are an AI Chatbot assistant for this website. Your main goal is to assist visitors with questions and provide helpful information. Here are your key guidelines: # Response Style - CRITICALLY IMPORTANT - MAXIMUM LENGTH: 1-3 short sentences per response - Ultra-concise: Get straight to the answer with no filler - No introductions like "Sure!" or "I\'d be happy to help" - No phrases like "based on my knowledge" or "according to information" - No explanatory text before giving the answer - No summaries or repetition - Hyperlink all URLs - Respond in user\'s language - Minor chit chat or conversation is okay, but try to keep it focused on [insert topic] # Knowledge Base Requirements - PREVENT HALLUCINATIONS - ONLY answer questions using information explicitly provided in OFFICIAL KNOWLEDGE DATABASE CONTENT sections marked with ===== delimiters - If required information is NOT in the knowledge database: "I don\'t have enough information in my knowledge base to answer that question accurately." - NEVER invent or hallucinate URLs, links, product specs, procedures, dates, statistics, names, contacts, or company information - When knowledge base information is unclear or contradictory, acknowledge the limitation rather than guessing - Better to admit insufficient information than provide inaccurate answers', 'model' => 'gpt-5.6-sol', 'rate_limit_logged_out' => '100', 'role_rate_limits' => array(), 'rate_limit_message' => esc_html__('Rate limit exceeded. Please try again later.', 'mxchat'), 'enable_email_block' => '', 'email_blocker_header_content' => __("Let's get started. Enter your email to begin chatting with us.
", 'mxchat'), 'email_blocker_button_text' => esc_html__('Start Chat', 'mxchat'), 'enable_name_field' => 'off', // NEW 'name_field_placeholder' => esc_html__('Enter your name', 'mxchat'), // NEW 'enable_consent_checkbox' => 'off', // b062c4 — default OFF: changes an existing form on every install 'consent_checkbox_label' => __('I agree to the Privacy Policy.', 'mxchat'), 'consent_checkbox_required' => 'off', // Lead capture options (c0cfaf). Both default to today's behaviour: // the form is hidden from logged-in visitors, and email is required. 'lead_capture_logged_in_toggle' => 'off', 'lead_capture_require_email_toggle' => 'on', 'top_bar_title' => esc_html__('MxChat', 'mxchat'), 'intro_message' => __('Hello! How can I assist you today?', 'mxchat'), 'ai_agent_text' => esc_html__('AI Agent', 'mxchat'), 'input_copy' => esc_html__('How can I assist?', 'mxchat'), 'append_to_body' => 'off', 'post_type_visibility_mode' => 'all', // 'all', 'include', 'exclude' 'post_type_visibility_list' => array(), // Array of post type slugs 'contextual_awareness_toggle' => 'off', 'citation_links_toggle' => 'on', 'satisfaction_rating_enabled' => 'off', 'satisfaction_rating_idle_seconds' => 60, 'satisfaction_rating_question' => '', 'satisfaction_rating_thanks' => '', 'satisfaction_rating_placeholder' => '', 'satisfaction_rating_saved' => '', 'close_button_color' => '#fff', 'chatbot_bg_color' => '#fff', 'user_message_bg_color' => '#fff', 'user_message_font_color' => '#212121', 'bot_message_bg_color' => '#212121', 'bot_message_font_color' => '#fff', 'top_bar_bg_color' => '#212121', 'send_button_font_color' => '#212121', 'chat_input_font_color' => '#212121', 'chatbot_background_color' => '#212121', 'icon_color' => '#fff', 'enable_woocommerce_integration' => '0', 'link_target_toggle' => 'off', 'pre_chat_message' => esc_html__('Hey there! Ask me anything!', 'mxchat'), // New fields for Loops Integration 'loops_api_key' => '', 'loops_mailing_list' => '', 'triggered_phrase_response' => __('Would you like to join our mailing list? Please provide your email below.', 'mxchat'), 'email_capture_response' => __('Thank you for providing your email! You\'ve been added to our list.', 'mxchat'), 'popular_question_1' => '', 'popular_question_2' => '', 'popular_question_3' => '', 'pdf_intent_trigger_text' => __("Please provide the URL to the PDF you'd like to discuss.", 'mxchat'), 'pdf_intent_success_text' => __("I've processed the PDF. What questions do you have about it?", 'mxchat'), 'pdf_intent_error_text' => __("Sorry, I couldn't process the PDF. Please ensure it's a valid file.", 'mxchat'), 'pdf_max_pages' => 69, 'show_pdf_upload_button' => 'on', 'show_word_upload_button' => 'on', // Live Agent Integration (Slack) 'live_agent_webhook_url' => '', 'live_agent_secret_key' => '', 'live_agent_bot_token' => '', 'live_agent_message_bg_color' => '#ffffff', 'live_agent_message_font_color' => '#333333', // Telegram Integration 'telegram_status' => 'off', 'telegram_bot_token' => '', 'telegram_group_id' => '', 'telegram_webhook_secret' => '', 'telegram_notification_message' => __("I've notified a support agent. Please allow a moment for them to respond. If you'd like to continue with AI, just type \"Switch to AI\" at any time.", 'mxchat'), 'telegram_away_message' => __("I just checked, and it looks like our support team isn't personally available at the moment. If you'd like, you can leave your email address, and they'll get back to you as soon as possible. In the meantime, you can keep chatting with me — just let me know how I can help!", 'mxchat'), // Webhook handoff destination (plan d88e22, outbound-only) 'webhook_handoff_status' => 'off', 'webhook_handoff_url' => '', 'webhook_handoff_secret' => '', 'webhook_handoff_notification_message' => __("I've notified our support team — they'll follow up with you soon. Meanwhile, I'm happy to keep helping.", 'mxchat'), 'webhook_handoff_away_message' => __('Sorry, live agents are currently unavailable. I can continue helping you as an AI assistant.', 'mxchat'), 'chat_toolbar_toggle' => 'off', 'mode_indicator_bg_color' => '#767676', 'mode_indicator_font_color' => '#ffffff', 'toolbar_icon_color' => '#212121', // Optimization settings 'script_loading_strategy' => 'default', // Debug settings 'debug_mode' => 'off', ); // Merge existing options with defaults $existing_options = get_option('mxchat_options', array()); $merged_options = wp_parse_args($existing_options, $default_options); // Update the options if they have changed if ($existing_options !== $merged_options) { update_option('mxchat_options', $merged_options); } // Add default limits for each role $roles = wp_roles()->get_names(); foreach ($roles as $role_id => $role_name) { $default_options['role_rate_limits'][$role_id] = '100'; } return $default_options; // Update the $this->options property $this->options = $merged_options; } public function mxchat_add_plugin_page() { // Onboarding lifecycle helpers (admin_init redirect + ajax handlers live in the file). require_once plugin_dir_path(__FILE__) . 'admin-onboarding-page.php'; // Main menu page — `mxchat-max` remains the parent slug for every MxChat submenu // (Settings, Knowledge, Transcripts, …). Hitting `?page=mxchat-max` directly now // dispatches to the Onboarding page (or Settings if the user has dismissed onboarding). add_menu_page( esc_html__('MxChat', 'mxchat'), esc_html__('MxChat', 'mxchat'), 'manage_options', 'mxchat-max', array($this, 'mxchat_create_dashboard_page'), 'dashicons-testimonial', 6 ); // Onboarding submenu — first child under MxChat (plan-d14e89). // First registration uses menu_slug === parent slug 'mxchat-max' → // WP-canonical override of the auto-duplicate "MxChat" entry. Result: the // first child shows as "Onboarding" instead of a redundant pair. add_submenu_page( 'mxchat-max', esc_html__('MxChat Onboarding', 'mxchat'), esc_html__('Onboarding', 'mxchat'), 'manage_options', 'mxchat-max', array($this, 'mxchat_create_dashboard_page') ); // Hidden route for `?page=mxchat-onboarding` (Settings "Show again" link // + legacy redirects still target this slug). Parent === null keeps it // out of the menu while remaining accessible by URL. add_submenu_page( null, esc_html__('MxChat Onboarding', 'mxchat'), esc_html__('Onboarding', 'mxchat'), 'manage_options', 'mxchat-onboarding', array($this, 'mxchat_create_dashboard_page') ); // Settings submenu — same callback as before, just at a new slug. add_submenu_page( 'mxchat-max', esc_html__('MxChat Settings', 'mxchat'), esc_html__('Settings', 'mxchat'), 'manage_options', 'mxchat-settings', array($this, 'mxchat_create_admin_page') ); // Submenu page for Knowledge add_submenu_page( 'mxchat-max', esc_html__('Prompts', 'mxchat'), esc_html__('Knowledge', 'mxchat'), 'manage_options', 'mxchat-prompts', array($this, 'mxchat_create_prompts_page') ); // Transcripts opens for administrators exactly as before, and read-only for // a role holding the transcripts capability (plan 93999a). Every other // MxChat submenu keeps manage_options, so that role sees only this item. $this->transcripts_page_hook = (string) add_submenu_page( 'mxchat-max', esc_html__('Chat Transcripts', 'mxchat'), esc_html__('Transcripts', 'mxchat'), current_user_can('manage_options') ? 'manage_options' : self::mxchat_transcripts_capability(), 'mxchat-transcripts', array($this, 'mxchat_create_transcripts_page') ); add_submenu_page( 'mxchat-max', esc_html__('MxChat Actions', 'mxchat'), esc_html__('Actions', 'mxchat'), 'manage_options', 'mxchat-actions', array($this, 'mxchat_actions_page_html') ); // Content Generator page add_submenu_page( 'mxchat-max', esc_html__('Content', 'mxchat'), esc_html__('Content', 'mxchat'), 'manage_options', 'mxchat-content', array($this, 'mxchat_create_content_page') ); } /** * Register the Pro & Extensions submenu on a later admin_menu priority so it * always renders as the bottom-most item in the MxChat sidebar — below * configuration pages like API Access (priority 20). */ public function mxchat_add_pro_extensions_page() { add_submenu_page( 'mxchat-max', esc_html__('Pro & Extensions', 'mxchat'), esc_html__('Pro & Extensions', 'mxchat'), 'manage_options', 'mxchat-activation', array($this, 'mxchat_create_activation_page') ); } public function mxchat_create_addons_page() { require_once plugin_dir_path(__FILE__) . 'class-mxchat-addons.php'; $addons_page = new MxChat_Addons(); $addons_page->render_page(); } /** * Render the Content Generator admin page */ public function mxchat_create_content_page() { require_once plugin_dir_path(__FILE__) . 'admin-content-page.php'; // The Editor Assistant toggle on the Settings tab renders through // mxchat_render_field_wrapper(), which lives in admin-settings-page.php // (plan-f7df40 relocation) — load it the same way the settings page does. require_once plugin_dir_path(__FILE__) . 'admin-settings-page.php'; mxchat_render_content_page($this); } public function register_pinecone_settings() { register_setting( 'mxchat_pinecone_addon_options', 'mxchat_pinecone_addon_options', array( 'type' => 'array', 'sanitize_callback' => array($this, 'sanitize_pinecone_settings'), 'default' => array( 'mxchat_use_pinecone' => '0', 'mxchat_pinecone_api_key' => '', 'mxchat_pinecone_host' => '', 'mxchat_pinecone_index' => '', 'mxchat_pinecone_environment' => '', 'mxchat_pinecone_top_k' => '50' ) ) ); } public function sanitize_pinecone_settings($input) { $sanitized = array(); // The form posts value="1" for a ticked box and no key for an unticked one; a caller // that passes '0' explicitly must not switch Pinecone ON (plan 95b2da). $sanitized['mxchat_use_pinecone'] = (isset($input['mxchat_use_pinecone']) && (string) $input['mxchat_use_pinecone'] === '1') ? '1' : '0'; $sanitized['mxchat_pinecone_api_key'] = sanitize_text_field($input['mxchat_pinecone_api_key'] ?? ''); $sanitized['mxchat_pinecone_host'] = sanitize_text_field($input['mxchat_pinecone_host'] ?? ''); $sanitized['mxchat_pinecone_index'] = sanitize_text_field($input['mxchat_pinecone_index'] ?? ''); $sanitized['mxchat_pinecone_environment'] = sanitize_text_field($input['mxchat_pinecone_environment'] ?? ''); // d0cae1: this function REBUILDS the array, so the topK key must be // carried here or any form save strips it. Clamp mirrors the read site. $top_k = absint($input['mxchat_pinecone_top_k'] ?? 50); $sanitized['mxchat_pinecone_top_k'] = (string) (($top_k >= 1 && $top_k <= 1000) ? $top_k : 50); // Remove https:// from host if present $sanitized['mxchat_pinecone_host'] = str_replace(['https://', 'http://'], '', $sanitized['mxchat_pinecone_host']); // Index type (plan 362c31). The document index is opt-in, and its host // must have passed Check index (or come from Create index for me) before // the choice sticks — a classic host routed through the documents API // answers 400 on every query. The verified host is carried from the // stored option; only the AJAX handlers set it, never the form. $existing = get_option('mxchat_pinecone_addon_options', array()); $existing = is_array($existing) ? $existing : array(); $verified_host = (string) ($existing['mxchat_pinecone_docs_verified_host'] ?? ''); $index_type = (($input['mxchat_pinecone_index_type'] ?? 'vector') === 'document') ? 'document' : 'vector'; $host_for_check = strtolower(trim($sanitized['mxchat_pinecone_host'], '/')); if ($index_type === 'document' && ($host_for_check === '' || $host_for_check !== strtolower($verified_host))) { $index_type = 'vector'; if (function_exists('add_settings_error')) { add_settings_error('mxchat_pinecone_addon_options', 'mxchat_pinecone_docs_unverified', __('The document index host has not been checked, so the index type stayed on Vector index. Click Check index (or Create index for me) and save again.', 'mxchat')); } } // 3e83e4: never point the live chatbot at a document index whose copy is // still running — the save keeps the previous type unless the owner // ticked Switch anyway. Only a vector → document switch is gated. $existing_type = (($existing['mxchat_pinecone_index_type'] ?? 'vector') === 'document') ? 'document' : 'vector'; if ($index_type === 'document' && $existing_type !== 'document' && class_exists('MxChat_Pinecone_Documents') && empty($input['mxchat_pinecone_docs_switch_anyway'])) { $running = MxChat_Pinecone_Documents::migration_blocks_switch($host_for_check); if ($running !== null) { $index_type = 'vector'; if (function_exists('add_settings_error')) { add_settings_error('mxchat_pinecone_addon_options', 'mxchat_pinecone_docs_copy_running', MxChat_Pinecone_Documents::switch_blocked_message($running)); } } } $sanitized['mxchat_pinecone_index_type'] = $index_type; $sanitized['mxchat_pinecone_docs_verified_host'] = $verified_host; $vector_host = str_replace(['https://', 'http://'], '', sanitize_text_field($input['mxchat_pinecone_vector_host'] ?? ($existing['mxchat_pinecone_vector_host'] ?? ''))); $sanitized['mxchat_pinecone_vector_host'] = trim($vector_host, '/'); $sanitized['mxchat_pinecone_vector_index'] = sanitize_text_field($input['mxchat_pinecone_vector_index'] ?? ($existing['mxchat_pinecone_vector_index'] ?? '')); $docs_cloud = sanitize_key($input['mxchat_pinecone_docs_cloud'] ?? ($existing['mxchat_pinecone_docs_cloud'] ?? 'aws')); $sanitized['mxchat_pinecone_docs_cloud'] = in_array($docs_cloud, array('aws', 'gcp', 'azure'), true) ? $docs_cloud : 'aws'; $docs_region = sanitize_text_field($input['mxchat_pinecone_docs_region'] ?? ($existing['mxchat_pinecone_docs_region'] ?? 'us-east-1')); $sanitized['mxchat_pinecone_docs_region'] = $docs_region !== '' ? $docs_region : 'us-east-1'; $docs_language = sanitize_key($input['mxchat_pinecone_docs_language'] ?? ($existing['mxchat_pinecone_docs_language'] ?? 'en')); $sanitized['mxchat_pinecone_docs_language'] = (class_exists('MxChat_Pinecone_Documents') && array_key_exists($docs_language, MxChat_Pinecone_Documents::languages())) ? $docs_language : 'en'; // The namespace has no form field; carry the stored value so a form save cannot drop it. if (isset($existing['mxchat_pinecone_namespace'])) { $sanitized['mxchat_pinecone_namespace'] = sanitize_text_field($existing['mxchat_pinecone_namespace']); } return $sanitized; } public function register_openai_vectorstore_settings() { register_setting( 'mxchat_openai_vectorstore_options', 'mxchat_openai_vectorstore_options', array( 'type' => 'array', 'sanitize_callback' => array($this, 'sanitize_openai_vectorstore_settings'), 'default' => array( 'mxchat_use_openai_vectorstore' => '0', 'mxchat_vectorstore_ids' => '', 'mxchat_vectorstore_max_results' => 5, 'mxchat_vectorstore_sync_enabled' => '0', 'mxchat_vectorstore_sync_store_id' => '' ) ) ); } public function sanitize_openai_vectorstore_settings($input) { $sanitized = array(); $sanitized['mxchat_use_openai_vectorstore'] = isset($input['mxchat_use_openai_vectorstore']) ? '1' : '0'; $sanitized['mxchat_vectorstore_ids'] = sanitize_text_field($input['mxchat_vectorstore_ids'] ?? ''); $sanitized['mxchat_vectorstore_max_results'] = absint($input['mxchat_vectorstore_max_results'] ?? 5); // Knowledge-base sync (write path, plan 15b5c6). Store ID may be blank — // the sync falls back to the first retrieval ID above. $sanitized['mxchat_vectorstore_sync_enabled'] = isset($input['mxchat_vectorstore_sync_enabled']) ? '1' : '0'; $sanitized['mxchat_vectorstore_sync_store_id'] = sanitize_text_field($input['mxchat_vectorstore_sync_store_id'] ?? ''); // Ensure max results is within reasonable range if ($sanitized['mxchat_vectorstore_max_results'] < 1) { $sanitized['mxchat_vectorstore_max_results'] = 1; } if ($sanitized['mxchat_vectorstore_max_results'] > 20) { $sanitized['mxchat_vectorstore_max_results'] = 20; } return $sanitized; } /** * AJAX handler to test OpenAI Vector Store connection */ public function mxchat_test_vectorstore_connection() { check_ajax_referer('mxchat_admin_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied.', 'mxchat'))); return; } $vectorstore_options = get_option('mxchat_openai_vectorstore_options', array()); $vectorstore_ids = $vectorstore_options['mxchat_vectorstore_ids'] ?? ''; $mxchat_options = get_option('mxchat_options', array()); $api_key = $mxchat_options['api_key'] ?? ''; if (empty($api_key)) { wp_send_json_error(array('message' => __('OpenAI API key is not configured.', 'mxchat'))); return; } if (empty($vectorstore_ids)) { wp_send_json_error(array('message' => __('No Vector Store ID configured.', 'mxchat'))); return; } // Get the first Vector Store ID for testing $ids_array = array_map('trim', explode(',', $vectorstore_ids)); $test_id = $ids_array[0]; // Test by retrieving the Vector Store info $response = wp_remote_get( 'https://api.openai.com/v1/vector_stores/' . $test_id, array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', 'OpenAI-Beta' => 'assistants=v2' ), 'timeout' => 30 ) ); if (is_wp_error($response)) { wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $response->get_error_message())); return; } $status_code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($status_code === 200 && isset($body['id'])) { $file_count = $body['file_counts']['completed'] ?? 0; $name = $body['name'] ?? $test_id; wp_send_json_success(array( 'message' => sprintf( __('Connected successfully! Vector Store: %s (%d files)', 'mxchat'), esc_html($name), $file_count ) )); } elseif ($status_code === 404) { wp_send_json_error(array('message' => __('Vector Store not found. Please check the ID.', 'mxchat'))); } elseif ($status_code === 401) { wp_send_json_error(array('message' => __('Invalid API key.', 'mxchat'))); } else { $error_message = $body['error']['message'] ?? __('Unknown error occurred.', 'mxchat'); wp_send_json_error(array('message' => $error_message)); } } /** * AJAX handler to test Slack connection and validate scopes */ public function mxchat_test_slack_connection() { check_ajax_referer('mxchat_admin_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied.', 'mxchat'))); return; } $bot_token = $this->options['live_agent_bot_token'] ?? ''; if (empty($bot_token)) { wp_send_json_error(array('message' => __('Slack Bot Token is not configured. Please enter your token and save settings first.', 'mxchat'))); return; } // Test 1: Validate bot token with auth.test $auth_response = wp_remote_post('https://slack.com/api/auth.test', array( 'headers' => array( 'Authorization' => 'Bearer ' . $bot_token, 'Content-Type' => 'application/json' ), 'timeout' => 15 )); if (is_wp_error($auth_response)) { wp_send_json_error(array('message' => __('Connection failed: ', 'mxchat') . $auth_response->get_error_message())); return; } $auth_body = json_decode(wp_remote_retrieve_body($auth_response), true); if (!isset($auth_body['ok']) || !$auth_body['ok']) { $error = $auth_body['error'] ?? 'unknown_error'; $error_messages = array( 'invalid_auth' => __('Invalid bot token. Please check your token starts with xoxb-', 'mxchat'), 'not_authed' => __('No authentication token provided.', 'mxchat'), 'account_inactive' => __('The Slack workspace has been deactivated.', 'mxchat'), 'token_revoked' => __('The bot token has been revoked. Please generate a new one.', 'mxchat'), ); $message = $error_messages[$error] ?? sprintf(__('Authentication failed: %s', 'mxchat'), $error); wp_send_json_error(array('message' => $message)); return; } $team_name = $auth_body['team'] ?? 'Unknown Workspace'; $bot_name = $auth_body['user'] ?? 'Unknown Bot'; // Test 2: Check if we can list channels (tests channels:read scope) $channels_response = wp_remote_post('https://slack.com/api/conversations.list', array( 'headers' => array( 'Authorization' => 'Bearer ' . $bot_token, 'Content-Type' => 'application/json' ), 'body' => json_encode(array('limit' => 1)), 'timeout' => 15 )); $channels_body = json_decode(wp_remote_retrieve_body($channels_response), true); $can_read_channels = isset($channels_body['ok']) && $channels_body['ok']; // Test 3: Check if we can create channels (tests channels:manage scope) // We'll just check the error message without actually creating $create_response = wp_remote_post('https://slack.com/api/conversations.create', array( 'headers' => array( 'Authorization' => 'Bearer ' . $bot_token, 'Content-Type' => 'application/json' ), 'body' => json_encode(array('name' => 'mxchat-test-' . time(), 'is_private' => false)), 'timeout' => 15 )); $create_body = json_decode(wp_remote_retrieve_body($create_response), true); // If channel was created, delete it immediately if (isset($create_body['ok']) && $create_body['ok'] && isset($create_body['channel']['id'])) { wp_remote_post('https://slack.com/api/conversations.archive', array( 'headers' => array( 'Authorization' => 'Bearer ' . $bot_token, 'Content-Type' => 'application/json' ), 'body' => json_encode(array('channel' => $create_body['channel']['id'])), 'timeout' => 15 )); $can_create_channels = true; } else { $create_error = $create_body['error'] ?? ''; // name_taken means we have permission but channel exists $can_create_channels = ($create_error === 'name_taken' || (isset($create_body['ok']) && $create_body['ok'])); // Check for missing scope errors if ($create_error === 'missing_scope') { $can_create_channels = false; } } // Build result message $results = array(); $results[] = sprintf(__('Workspace: %s', 'mxchat'), esc_html($team_name)); $results[] = sprintf(__('Bot: %s', 'mxchat'), esc_html($bot_name)); $results[] = ''; $results[] = ($can_read_channels ? '✓' : '✗') . ' ' . __('channels:read - List channels', 'mxchat'); $results[] = ($can_create_channels ? '✓' : '✗') . ' ' . __('channels:manage - Create channels', 'mxchat'); $missing_scopes = array(); if (!$can_read_channels) $missing_scopes[] = 'channels:read'; if (!$can_create_channels) $missing_scopes[] = 'channels:manage'; if (!empty($missing_scopes)) { wp_send_json_error(array( 'message' => implode("\n", $results), 'missing_scopes' => $missing_scopes, 'partial' => true )); } else { wp_send_json_success(array( 'message' => implode("\n", $results) )); } } public function mxchat_display_admin_notice() { // Success notice if ($message = get_transient('mxchat_admin_notice_success')) { ?>We've temporarily disabled your Live Agent integration due to recent enhancements that have made it much better! You can easily turn it back on by going to Settings → Integrations → Toolbar and reviewing the new configuration options.
If you're using an AI-generated chatbot theme, you'll need to migrate it to match the new CSS structure. Go to Theme Settings, select your theme from the sidebar, and click the Migrate button.
' . esc_html__('Configure email notifications for new chat transcripts. You will receive an email notification when a new chat session begins.', 'mxchat') . '
'; } public function mxchat_enable_notifications_callback() { $options = get_option('mxchat_transcripts_options', array()); $enabled = isset($options['mxchat_enable_notifications']) ? $options['mxchat_enable_notifications'] : 0; ?>
0 it overrides the bucket dropdown above * and deletes transcripts older than the given number of days. Set to 0 to fall * back to the dropdown (or "Never" if the dropdown is also Never). * * Devs can override the final day count via the `mxchat_transcript_retention_days` * filter — runs in `cleanup_old_transcripts()` after this option is read. * * (plan-mxchat-20260509-9b80b1) */ public function mxchat_retention_days_callback() { $options = get_option('mxchat_transcripts_options', array()); $days = isset($options['mxchat_retention_days']) ? (int) $options['mxchat_retention_days'] : 0; ?>
apply_filters( 'mxchat_transcript_retention_days', $days )