options = get_option('mxchat_options', array());
add_shortcode('mxchat_chatbot', array($this, 'render_chatbot_shortcode'));
add_action('wp_footer', array($this, 'append_chatbot_to_body'));
}
/**
* UPDATED: Enhanced should_hide_chatbot method - only blocks auto-append, not shortcodes
*/
private function should_hide_chatbot($context = 'auto') {
global $post;
if (!$post) {
return false;
}
// Only hide if it's the auto-append context (floating="yes" from global setting)
// Allow shortcodes to work regardless of this setting
if ($context === 'auto') {
// Check new visibility field first
$visibility = get_post_meta($post->ID, '_mxchat_page_visibility', true);
if ($visibility === 'hide') {
return true;
}
// Backward compat: check legacy field if new field not set
if (empty($visibility)) {
$hide_chatbot = get_post_meta($post->ID, '_mxchat_hide_chatbot', true);
if ($hide_chatbot === '1') {
return true;
}
}
}
return false;
}
/**
* UPDATED: Enhanced append_chatbot_to_body method with context
*/
public function append_chatbot_to_body() {
// Only run on public pages
if (is_admin() || wp_doing_ajax()) {
return;
}
// Check if auto-append chatbot should be hidden on this page
if ($this->should_hide_chatbot('auto')) {
return; // Don't show auto-appended chatbot
}
// Get the bot that should be displayed using new logic
$bot_to_show = $this->get_display_bot();
// Don't show chatbot if determination is false
if ($bot_to_show === false) {
return;
}
// Handle consent for display
$consent_category = 'marketing';
$has_consent = true;
if (
isset($this->options['complianz_toggle']) &&
$this->options['complianz_toggle'] === 'on' &&
function_exists('cmplz_has_consent')
) {
$has_consent = cmplz_has_consent($consent_category);
}
// Display the appropriate chatbot (always floating for auto-append)
if ($bot_to_show && $bot_to_show !== 'default') {
// Show specific bot
echo do_shortcode('[mxchat_chatbot floating="yes" bot_id="' . esc_attr($bot_to_show) . '" has_consent="' . ($has_consent ? 'yes' : 'no') . '"]');
} else {
// Show default bot
echo do_shortcode('[mxchat_chatbot floating="yes" has_consent="' . ($has_consent ? 'yes' : 'no') . '"]');
}
}
private function mxchat_get_user_identifier() {
return sanitize_text_field($_SERVER['REMOTE_ADDR']);
}
/**
* UPDATED: Enhanced shortcode with context-aware hiding
*/
public function render_chatbot_shortcode($atts) {
// Smart asset loading safety net (plan-915355): if the opt-in enqueue gate
// skipped assets on this request (shortcode invisible to has_shortcode —
// builder-stored content, template files, widget areas), force the FULL
// enqueue now, including the mxchatChat settings payload and the delayed
// loader wiring. The integrator method is idempotent, so this is a no-op
// when assets already went out at wp_enqueue_scripts time.
if (self::is_smart_asset_loading_enabled() && !wp_style_is('mxchat-chat-css', 'enqueued')) {
global $mxchat_integrator;
if (isset($mxchat_integrator) && is_object($mxchat_integrator)
&& method_exists($mxchat_integrator, 'mxchat_enqueue_scripts_styles')) {
$mxchat_integrator->mxchat_enqueue_scripts_styles(true);
}
}
// UPDATED: Add bot_id parameter support and improve logic
$attributes = shortcode_atts(array(
'floating' => 'yes',
'has_consent' => 'yes',
'bot_id' => '' // Support for multi-bot functionality - empty means auto-detect
), $atts);
// Determine which bot to use
$bot_id = $this->determine_bot_for_shortcode($attributes['bot_id']);
// UPDATED: Only check hiding for floating shortcodes that could conflict with auto-append
// Non-floating shortcodes should always work
if ($attributes['floating'] === 'yes') {
// For floating shortcodes, check if auto-append is hidden
// This prevents duplicate floating chatbots
if ($this->should_hide_chatbot('auto') && $this->is_auto_append_enabled()) {
// If auto-append is enabled but hidden on this page,
// allow the floating shortcode to work (user is overriding)
// But if auto-append is disabled globally, also allow shortcode
}
}
// Non-floating shortcodes (floating="no") should NEVER be blocked by the hide setting
// This allows embedded chatbots even when floating is hidden
$is_floating = $attributes['floating'] === 'yes';
$bot_id = sanitize_key($bot_id); // Sanitize bot ID
// Rest of your existing shortcode rendering logic continues unchanged...
// [All the existing HTML generation code remains the same]
// Get bot-specific options if multi-bot add-on is active
$bot_options = $this->get_bot_options($bot_id);
// Use bot-specific options or fall back to default options
$current_options = !empty($bot_options) ? $bot_options : $this->options;
// [Rest of your existing rendering code stays exactly the same]
// Just remove the old should_hide_chatbot() check from the beginning
// Check for Complianz consent if the toggle is enabled
$initial_visibility = 'hidden';
$additional_class = '';
if (isset($current_options['complianz_toggle']) && $current_options['complianz_toggle'] === 'on') {
$additional_class = ' no-consent';
}
$visibility_class = $initial_visibility . $additional_class;
$theme_options = get_option('mxchat_theme_options', array());
$custom_send_image = isset($theme_options['custom_send_button_image']) ? esc_url($theme_options['custom_send_button_image']) : '';
$send_width = isset($theme_options['send_button_width']) ? intval($theme_options['send_button_width']) : 24;
$send_height = isset($theme_options['send_button_height']) ? intval($theme_options['send_button_height']) : 24;
$send_rotation = isset($theme_options['send_button_rotation']) ? intval($theme_options['send_button_rotation']) : 0;
// Check if an AI theme is active (global or bot-specific) - if so, skip inline color styles
$ai_theme_active = !empty($theme_options['active_ai_theme_css']);
$bot_has_theme = isset($theme_options['bot_theme_assignments'][$bot_id]);
$skip_inline_colors = $ai_theme_active || $bot_has_theme;
// UPDATED: Use current_options instead of $this->options throughout
$bg_color = $current_options['chatbot_background_color'] ?? '#fff';
$user_message_bg_color = $current_options['user_message_bg_color'] ?? '#fff';
$user_message_font_color = $current_options['user_message_font_color'] ?? '#212121';
$bot_message_bg_color = $current_options['bot_message_bg_color'] ?? '#212121';
$bot_message_font_color = $current_options['bot_message_font_color'] ?? '#fff';
$top_bar_bg_color = $current_options['top_bar_bg_color'] ?? '#212121';
$send_button_font_color = $current_options['send_button_font_color'] ?? '#212121';
$intro_message = $current_options['intro_message'] ?? esc_html__('Hello! How can I assist you today?', 'mxchat');
$top_bar_title = $current_options['top_bar_title'] ?? esc_html__('MxChat: Basic', 'mxchat');
$chatbot_background_color = $current_options['chatbot_background_color'] ?? '#212121';
$icon_color = $current_options['icon_color'] ?? '#fff';
$chat_input_font_color = $current_options['chat_input_font_color'] ?? '#212121';
$close_button_color = $current_options['close_button_color'] ?? '#fff';
$chatbot_bg_color = $current_options['chatbot_bg_color'] ?? '#fff';
$pre_chat_message = isset($current_options['pre_chat_message']) ? sanitize_textarea_field(trim($current_options['pre_chat_message'])) : '';
$user_id = sanitize_key($this->mxchat_get_user_identifier());
$email_state = $this->determine_email_collection_state();
$show_email_form = $email_state['show_email_form'];
$user_email = $email_state['user_email'] ?? '';
$user_name = $email_state['user_name'] ?? '';
// Pre-chat dismissal now handled client-side via localStorage (zero server load)
$input_copy = isset($current_options['input_copy']) ? esc_attr($current_options['input_copy']) : esc_attr__('How can I assist?', 'mxchat');
$rate_limit_message = isset($current_options['rate_limit_message']) ? esc_attr($current_options['rate_limit_message']) : esc_attr__('Rate limit exceeded. Please try again later.', 'mxchat');
$mode_indicator_bg_color = $current_options['mode_indicator_bg_color'] ?? '#212121';
$mode_indicator_font_color = $current_options['mode_indicator_font_color'] ?? '#fff';
$quick_questions_toggle_color = $current_options['quick_questions_toggle_color'] ?? '#212121';
$privacy_toggle = isset($current_options['privacy_toggle']) && $current_options['privacy_toggle'] === 'on';
$privacy_text = isset($current_options['privacy_text']) ? wp_kses_post($current_options['privacy_text']) : wp_kses_post(__('By chatting, you agree to our privacy policy.', 'mxchat'));
$popular_question_1 = isset($current_options['popular_question_1']) ? esc_html($current_options['popular_question_1']) : '';
$popular_question_2 = isset($current_options['popular_question_2']) ? esc_html($current_options['popular_question_2']) : '';
$popular_question_3 = isset($current_options['popular_question_3']) ? esc_html($current_options['popular_question_3']) : '';
$additional_questions = isset($current_options['additional_popular_questions']) ? $current_options['additional_popular_questions'] : [];
$custom_icon = isset($current_options['custom_icon']) ? esc_url($current_options['custom_icon']) : '';
$title_icon = isset($current_options['title_icon']) ? esc_url($current_options['title_icon']) : '';
// AI agent text - if explicitly set to empty string, hide the indicator entirely
$ai_agent_text = isset($current_options['ai_agent_text']) ? $current_options['ai_agent_text'] : __('AI Agent', 'mxchat');
$live_agent_message_bg_color = $current_options['live_agent_message_bg_color'] ?? '#212121';
$live_agent_message_font_color = $current_options['live_agent_message_font_color'] ?? '#fff';
$enable_email_block = isset($current_options['enable_email_block']) &&
($current_options['enable_email_block'] === '1' || $current_options['enable_email_block'] === 'on');
// Add name field variables
$enable_name_field = isset($current_options['enable_name_field']) &&
($current_options['enable_name_field'] === '1' || $current_options['enable_name_field'] === 'on');
$name_field_placeholder = isset($current_options['name_field_placeholder']) ?
esc_attr($current_options['name_field_placeholder']) :
esc_attr__('Enter your name', 'mxchat');
ob_start();
// Check if floating attribute is set to 'yes' and wrap accordingly
if ($is_floating) {
echo '
';
}
// Add bot_id to the chatbot wrapper as a data attribute
// data-nosnippet: keep the chat widget's UI copy (greeting, title, quick-question
// prompts, privacy notice) out of Google search snippets. This wrapper renders in both
// floating and inline/shortcode modes, so it covers all in-panel copy in one place.
echo '
';
echo '
';
echo '
';
echo '
';
if (!empty($title_icon)) {
echo ' ';
}
echo '
' . esc_html($top_bar_title) . '
';
echo '
';
// Only show mode indicator if ai_agent_text is not empty
if (!empty(trim($ai_agent_text))) {
echo '' . esc_html($ai_agent_text) . '';
}
echo '
';
// Overflow menu (3-dot) trigger + dropdown container.
// Sibling of .exit-chat, rendered to its left. JS hides the trigger
// if no menu items are enabled; outer click + Escape close the menu.
echo '
'; // end #chat-box
// Replace the existing popular questions section with this:
echo '
';
echo '
';
// Collapse button (down arrow) - shows when open, centered at top
echo ' ';
// Expand button (up arrow) - shows when collapsed
echo ' ';
if (!empty($popular_question_1)) {
echo '';
}
if (!empty($popular_question_2)) {
echo '';
}
if (!empty($popular_question_3)) {
echo '';
}
if (!empty($additional_questions) && is_array($additional_questions)) {
foreach ($additional_questions as $index => $question) {
if (!empty($question)) {
echo '';
}
}
}
echo '
';
echo '
';
echo '
';
// Max input length (plan a3fae2 part C) — global core setting, 0 = unlimited.
// Hard-caps typing/paste client-side; the chat handler enforces it server-side too.
$mxchat_max_input_length = isset($this->options['max_input_length']) ? intval($this->options['max_input_length']) : 0;
$mxchat_maxlength_attr = $mxchat_max_input_length > 0 ? ' maxlength="' . esc_attr($mxchat_max_input_length) . '"' : '';
echo ' ';
// Language-neutral character counter (plan 7091a2). Numbers only — no
// translatable strings — so it reads correctly on every-language install.
// Only rendered when a cap is set; hidden until ~80% of the cap, then
// ramps neutral -> amber -> red. Decorative (aria-hidden); the textarea's
// maxlength carries the real semantics for assistive tech.
if ($mxchat_max_input_length > 0) {
echo '
0/' . esc_html($mxchat_max_input_length) . '
';
}
echo ' ';
echo '
';
echo '
';
// PDF Upload Button - wrapped in conditional using current_options
$show_pdf_button = isset($current_options['show_pdf_upload_button']) ? $current_options['show_pdf_upload_button'] : 'on';
if ($show_pdf_button === 'on') {
echo ' ';
echo ' ';
}
// Word Upload Button - wrapped in conditional using current_options
$show_word_button = isset($current_options['show_word_upload_button']) ? $current_options['show_word_upload_button'] : 'on';
if ($show_word_button === 'on') {
echo ' ';
echo ' ';
}
// File containers
echo '
';
if (!empty($pre_chat_message)) {
// Rendered hidden by default — JS checkPreChatDismissal() handles show/hide via localStorage
// data-nosnippet: the pre-chat teaser bubble is a sibling outside
// .mxchat-chatbot-wrapper, so it needs its own marker to stay out of snippets.
echo '
';
}
return ob_get_clean();
}
/**
* Get bot-specific options for multi-bot functionality
* Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
*/
private function get_bot_options($bot_id = 'default') {
// If default bot or multi-bot add-on not active, return empty (use default options)
if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
return array();
}
//Hook for multi-bot add-on to provide bot-specific options
$bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
return is_array($bot_options) ? $bot_options : array();
}
/**
* Get bot-specific Pinecone configuration
* Used in the knowledge retrieval functions
*/
private function get_bot_pinecone_config($bot_id = 'default') {
// If default bot or multi-bot add-on not active, use default Pinecone config
if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
$addon_options = get_option('mxchat_pinecone_addon_options', array());
return array(
'use_pinecone' => (isset($addon_options['mxchat_use_pinecone']) && $addon_options['mxchat_use_pinecone'] === '1'),
'api_key' => $addon_options['mxchat_pinecone_api_key'] ?? '',
'host' => $addon_options['mxchat_pinecone_host'] ?? '',
'namespace' => $addon_options['mxchat_pinecone_namespace'] ?? ''
);
}
// Hook for multi-bot add-on to provide bot-specific Pinecone config
$bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
return is_array($bot_pinecone_config) ? $bot_pinecone_config : array();
}
private function determine_email_collection_state() {
// Logged-in users skip the email form
if (is_user_logged_in()) {
$current_user = wp_get_current_user();
return [
'show_email_form' => false,
'user_email' => $current_user->user_email,
'user_name' => $current_user->display_name ?: $current_user->first_name ?: ''
];
}
// For guests, default to showing the email form.
// The session-based check happens client-side via AJAX since the
// session ID lives in the browser cookie/localStorage.
return [
'show_email_form' => true,
'user_email' => '',
'user_name' => ''
];
}
/**
* NEW: Determine if and which chatbot should be displayed
*/
private function get_display_bot() {
// Delegates to the static so the enqueue-time gate (smart asset loading,
// plan-915355) and this render-time decision share ONE code path and can
// never disagree. The static reads mxchat_options fresh — same row this
// instance loaded at construct.
return self::compute_display_bot();
}
/**
* Static single source of truth for the display decision (plan-915355).
* Combines the per-page meta box, the append_to_body global toggle, and the
* post-type include/exclude mode. Returns a bot id ('default' or specific)
* when the auto-append widget will render on the current request, false when
* it won't. Static (not a second instance) deliberately: MxChat_Public's
* constructor registers a wp_footer action, so constructing a throwaway
* instance would double-append the widget.
*/
public static function compute_display_bot() {
$options = get_option('mxchat_options', array());
if (!is_array($options)) {
$options = array();
}
// Get page-specific settings from meta box
$page_setting = self::get_page_bot_setting();
$global_autoshow = isset($options['append_to_body']) && $options['append_to_body'] === 'on';
$global_default_bot = isset($options['default_bot']) ? $options['default_bot'] : 'default';
// If page specifically hides chatbot, don't show anything
if ($page_setting && $page_setting['action'] === 'hide') {
return false;
}
// If page specifies a specific bot, use that
if ($page_setting && $page_setting['action'] === 'show') {
return $page_setting['bot_id'];
}
// Page setting is 'global' or no page setting exists
// Check global auto-show setting
if ($global_autoshow) {
// Check post type visibility settings
if (!self::should_show_on_current_post_type()) {
return false;
}
// Global auto-show is enabled, return the default bot
return $global_default_bot;
}
// Global auto-show is disabled and no page-specific bot selected
// Don't show chatbot (user should use shortcodes)
return false;
}
/**
* Smart asset loading opt-in (plan-915355). Standalone option — deliberately
* NOT a mxchat_options key, so it can never be stripped by mxchat_sanitize().
* Default off: enqueue behavior is byte-identical to before until an owner
* turns the toggle on.
*/
public static function is_smart_asset_loading_enabled() {
return get_option('mxchat_smart_asset_loading', 'off') === 'on';
}
/**
* Will the chat widget render on the current request? (plan-915355)
*
* True when the auto-append decision resolves to a bot, OR the singular
* post's content contains the [mxchat_chatbot] shortcode (first-chance
* detection so shortcode pages keep head-loaded CSS — no FOUC). Computed
* once per request and cached, so the wp_enqueue_scripts gate and any
* add-on consulting this later in the same request always get one answer.
*
* Filter `mxchat_should_load_assets` is the force-load escape hatch for
* headless/builder/custom-JS setups whose shortcode placement is invisible
* to has_shortcode (builder-stored content, template files, widget areas).
* Note the render-time safety net in render_chatbot_shortcode() still
* force-loads assets whenever the shortcode actually renders — the filter
* is only needed where even that net can't fire (e.g. markup assembled
* outside WP rendering).
*/
public static function should_load_assets() {
static $cached = null;
if ($cached !== null) {
return $cached;
}
// Never gate admin/ajax requests — this decision is for front-end enqueues only.
if (is_admin()) {
$cached = true;
return $cached;
}
$display_bot = self::compute_display_bot();
$has_shortcode = false;
if ($display_bot === false && is_singular()) {
$post = get_post();
if ($post && has_shortcode((string) $post->post_content, 'mxchat_chatbot')) {
$has_shortcode = true;
}
}
$should = ($display_bot !== false) || $has_shortcode;
$cached = (bool) apply_filters('mxchat_should_load_assets', $should, array(
'display_bot' => $display_bot,
'has_shortcode' => $has_shortcode,
'post_id' => get_the_ID(),
));
return $cached;
}
/**
* Check if chatbot should be shown on the current post type
*/
private static function should_show_on_current_post_type() {
$options = get_option('mxchat_options', array());
if (!is_array($options)) {
$options = array();
}
// Get visibility settings
$mode = isset($options['post_type_visibility_mode']) ? $options['post_type_visibility_mode'] : 'all';
$list = isset($options['post_type_visibility_list']) ? $options['post_type_visibility_list'] : array();
// Ensure list is an array
if (!is_array($list)) {
$list = array();
}
// If mode is 'all', show on all post types
if ($mode === 'all') {
return true;
}
// Get current post type
$current_post_type = self::get_current_post_type();
// If we can't determine post type, default to showing
if (empty($current_post_type)) {
return true;
}
// Check based on mode
if ($mode === 'include') {
// Only show on selected post types
return in_array($current_post_type, $list);
} elseif ($mode === 'exclude') {
// Hide on selected post types
return !in_array($current_post_type, $list);
}
// Default to showing
return true;
}
/**
* Get the current post type
*/
private static function get_current_post_type() {
// Try to get from queried object first
$queried_object = get_queried_object();
if ($queried_object instanceof WP_Post) {
return $queried_object->post_type;
}
// Try get_post_type()
$post_type = get_post_type();
if ($post_type) {
return $post_type;
}
// Check if we're on an archive
if (is_post_type_archive()) {
return get_query_var('post_type');
}
// Check common archive types
if (is_home() || is_single()) {
return 'post';
}
if (is_page()) {
return 'page';
}
return '';
}
/**
* Get page-specific bot setting using new visibility field with backward compat
*/
private static function get_page_bot_setting($post_id = null) {
if (!$post_id) {
$post_id = get_the_ID();
}
if (!$post_id) {
return null;
}
// Check new visibility field first
$visibility = get_post_meta($post_id, '_mxchat_page_visibility', true);
if ($visibility === 'hide') {
return array('action' => 'hide');
}
if ($visibility === 'show') {
$selected_bot = get_post_meta($post_id, '_mxchat_selected_bot', true);
$bot_id = !empty($selected_bot) ? $selected_bot : 'default';
return array('action' => 'show', 'bot_id' => $bot_id);
}
// Backward compat: check legacy hide checkbox if no new field set
if (empty($visibility)) {
$hide_chatbot = get_post_meta($post_id, '_mxchat_hide_chatbot', true);
if ($hide_chatbot === '1') {
return array('action' => 'hide');
}
}
// Check if specific bot is selected (legacy path)
$selected_bot = get_post_meta($post_id, '_mxchat_selected_bot', true);
if (!empty($selected_bot)) {
return array('action' => 'show', 'bot_id' => $selected_bot);
}
// Use global setting
return array('action' => 'global');
}
/**
* NEW: Determine which bot to use for shortcode
*/
private function determine_bot_for_shortcode($shortcode_bot_id) {
// If bot_id explicitly provided in shortcode, use that
if (!empty($shortcode_bot_id)) {
return $shortcode_bot_id;
}
// No bot_id in shortcode, check page setting
$page_setting = self::get_page_bot_setting();
if ($page_setting && $page_setting['action'] === 'show') {
return $page_setting['bot_id'];
}
// Fall back to default
return 'default';
}
/**
* NEW: Helper to check if auto-append is enabled globally
*/
private function is_auto_append_enabled() {
return isset($this->options['append_to_body']) && $this->options['append_to_body'] === 'on';
}
/**
* UPDATED: Debug function with new context info
*/
/**
* NEW: Helper method to get available bots (for admin notices, etc.)
*/
private function get_available_bots() {
$bots = array('default' => __('Default Bot', 'mxchat'));
// Check if multi-bot addon is active
if (class_exists('MxChat_Multi_Bot_Manager')) {
$multi_bot_manager = MxChat_Multi_Bot_Core_Manager::get_instance();
$available_bots = $multi_bot_manager->get_available_bots();
// Add the available bots
foreach ($available_bots as $bot_id => $bot_name) {
$bots[$bot_id] = $bot_name;
}
}
return $bots;
}
}
?>