PageSourceSearch

https://loveandscience.com/wp-content/plugins/mxchat-basic/js/chat-script.js?ver=3.0.5

js loveandscience.com collected 2026-10-01 23:48:35 UTC 125,890 bytes, 3,215 lines download raw bytes

1jQuery(document).ready(function($) {
2
3    // ====================================
4    // MULTI-INSTANCE MANAGEMENT SYSTEM
5    // ====================================
6
7    // Instance registry - tracks all chatbot instances on the page
8    const MxChatInstances = {
9        instances: {},
10
11        // Initialize an instance for a bot
12        init: function(botId) {
13            if (!this.instances[botId]) {
14                this.instances[botId] = {
15                    botId: botId,
16                    sessionId: this.getChatSession(botId),
17                    lastSeenMessageId: '',
18                    notificationCheckInterval: null,
19                    pollingInterval: null,
20                    processedMessageIds: new Set(),
21                    activePdfFile: null,
22                    activeWordFile: null,
23                    chatHistoryLoaded: false,
24                    isStreaming: false
25                };
26            }
27            return this.instances[botId];
28        },
29
30        // Get instance by botId
31        get: function(botId) {
32            return this.instances[botId] || this.init(botId);
33        },
34
35        // Get all active bot IDs
36        getAllBotIds: function() {
37            return Object.keys(this.instances);
38        },
39
40        // Session management per bot
41        getChatSession: function(botId) {
42            var cookieName = 'mxchat_session_id_' + botId;
43            var sessionId = getCookie(cookieName);
44
45            if (!sessionId) {
46                sessionId = generateSessionId();
47                this.setChatSession(botId, sessionId);
48            }
49
50            return sessionId;
51        },
52
53        setChatSession: function(botId, sessionId) {
54            var cookieName = 'mxchat_session_id_' + botId;
55            document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
56            if (this.instances[botId]) {
57                this.instances[botId].sessionId = sessionId;
58            }
59        },
60
61        resetChatSession: function(botId) {
62            var newSessionId = generateSessionId();
63            this.setChatSession(botId, newSessionId);
64            var $chatBox = getElement(botId, 'chat-box');
65            if ($chatBox.length) {
66                $chatBox.find('.user-message, .bot-message:not(:first), .agent-message').remove();
67            }
68            if (this.instances[botId]) {
69                this.instances[botId].chatHistoryLoaded = false;
70                this.instances[botId].processedMessageIds = new Set();
71            }
72        }
73    };
74
75    // ====================================
76    // ELEMENT SELECTOR HELPERS
77    // ====================================
78
79    // Check if a specific bot has an AI theme assigned (skip inline colors)
80    function shouldSkipInlineColors(botId) {
81        // If global AI theme is active, skip inline colors for all bots
82        if (mxchatChat.skip_inline_colors) {
83            return true;
84        }
85        // Check if this specific bot has a theme assignment
86        var botAssignments = mxchatChat.bot_theme_assignments || {};
87        return botAssignments.hasOwnProperty(botId);
88    }
89
90    // Get element by ID with bot suffix - returns jQuery object
91    function getElement(botId, elementName) {
92        return $('#' + elementName + '-' + botId);
93    }
94
95    // Get element by ID with bot suffix - returns DOM element
96    function getElementDOM(botId, elementName) {
97        return document.getElementById(elementName + '-' + botId);
98    }
99
100    // Get bot ID from any element within a chatbot instance
101    function getBotIdFromElement(element) {
102        var $wrapper = $(element).closest('.mxchat-chatbot-wrapper');
103        if ($wrapper.length) {
104            return $wrapper.data('bot-id') || 'default';
105        }
106        // Fallback: try to find from floating container
107        var $floating = $(element).closest('.floating-chatbot');
108        if ($floating.length) {
109            var id = $floating.attr('id') || '';
110            var match = id.match(/floating-chatbot-(.+)/);
111            if (match) return match[1];
112        }
113        // Fallback: check if element itself has an ID with bot suffix (e.g., floating-chatbot-button-{bot_id})
114        var elementId = $(element).attr('id') || '';
115        if (elementId) {
116            // Match patterns like: floating-chatbot-button-{bot_id}, pre-chat-message-{bot_id}
117            var idMatch = elementId.match(/^(?:floating-chatbot-button|pre-chat-message|chat-notification-badge)-(.+
117)$/);
118            if (idMatch) return idMatch[1];
119        }
120        return 'default';
121    }
122
123    // Get wrapper element for a bot
124    function getWrapper(botId) {
125        return getElement(botId, 'mxchat-chatbot-wrapper');
126    }
127
128    // ====================================
129    // GLOBAL VARIABLES & CONFIGURATION
130    // ====================================
131    const toolbarIconColor = mxchatChat.toolbar_icon_color || '#212121';
132
133    // Initialize color settings (these are global as they come from PHP)
134    var userMessageBgColor = mxchatChat.user_message_bg_color;
135    var userMessageFontColor = mxchatChat.user_message_font_color;
136    var botMessageBgColor = mxchatChat.bot_message_bg_color;
137    var botMessageFontColor = mxchatChat.bot_message_font_color;
138    var liveAgentMessageBgColor = mxchatChat.live_agent_message_bg_color;
139    var liveAgentMessageFontColor = mxchatChat.live_agent_message_font_color;
140
141    var linkTarget = mxchatChat.link_target_toggle === 'on' ? '_blank' : '_self';
142
143    // ====================================
144    // SESSION MANAGEMENT (Legacy compatibility)
145    // ====================================
146
147    function getCookie(name) {
148        let value = "; " + document.cookie;
149        let parts = value.split("; " + name + "=");
150        if (parts.length == 2) return parts.pop().split(";").shift();
151    }
152
153    function generateSessionId() {
154        return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
155    }
156
157    // Legacy function - now delegates to instance manager
158    function getChatSession(botId) {
159        botId = botId || 'default';
160        return MxChatInstances.getChatSession(botId);
161    }
162
163    function setChatSession(sessionId, botId) {
164        botId = botId || 'default';
165        MxChatInstances.setChatSession(botId, sessionId);
166    }
167
168    function resetChatSession(botId) {
169        botId = botId || 'default';
170        MxChatInstances.resetChatSession(botId);
171    }
172
173    // ====================================
174    // INITIALIZE ALL CHATBOT INSTANCES
175    // ====================================
176
177    function initializeAllInstances() {
178        // Find all chatbot wrappers on the page
179        $('.mxchat-chatbot-wrapper').each(function() {
180            var botId = $(this).data('bot-id') || 'default';
181            MxChatInstances.init(botId);
182            initializeBotInstance(botId);
183        });
184    }
185
186    function initializeBotInstance(botId) {
187        var instance = MxChatInstances.get(botId);
188
189        // Initialize quick questions state for this bot
190        checkQuickQuestionsState(botId);
191
192        // Note: Event handlers use event delegation with class selectors,
193        // so they work automatically for all instances without per-bot setup
194    }
195
196// ====================================
197// CONTEXTUAL AWARENESS FUNCTIONALITY
198// ====================================
199
200function getPageContext() {
201    // Check if contextual awareness is enabled
202    if (mxchatChat.contextual_awareness_toggle !== 'on') {
203        return null;
204    }
205    
206    // Get page URL
207    const pageUrl = window.location.href;
208    
209    // Get page title
210    const pageTitle = document.title || '';
211    
212    // Get main content from the page
213    let pageContent = '';
214    
215    // Try to get content from common content areas
216    const contentSelectors = [
217        'main',
218        '[role="main"]',
219        '.content',
220        '.main-content',
221        '.post-content',
222        '.entry-content',
223        '.page-content',
224        'article',
225        '#content',
226        '#main'
227    ];
228    
229    let contentElement = null;
230    for (const selector of contentSelectors) {
231        contentElement = document.querySelector(selector);
232        if (contentElement) {
233            break;
234        }
235    }
236    
237    // If no specific content area found, use body but exclude header, footer, nav, sidebar
238    if (!contentElement) {
239        contentElement = document.body;
240    }
241    
242    if (contentElement) {
243        // Clone the element to avoid modifying the original
244        const clone = contentElement.cloneNode(true);
245        
246        // Remove unwanted elements
247        const unwantedSelectors = [
248            'header',
249            'footer',
250            'nav',
251            '.navigation',
252            '.sidebar',
253            '.widget',
254            '.menu',
255            'script',
256            'style',
257            '.comments',
258            '#comments',
259            '.breadcrumb',
260            '.breadcrumbs',
261            '#floating-chatbot',
262            '#floating-chatbot-button',
263            '.mxchat',
264            '[class*="chat"]',
265            '[id*="chat"]'
266        ];
267        
268        unwantedSelectors.forEach(selector => {
269            const elements = clone.querySelectorAll(selector);
270            elements.forEach(el => el.remove());
271        });
272        
273        //   Extract MxChat context data attributes before getting text content
274        const contextData = [];
275        clone.querySelectorAll('[data-mxchat-context]').forEach(el => {
276            const contextValue = el.dataset.mxchatContext;
277            if (contextValue && contextValue.trim()) {
278                contextData.push(contextValue);
279            }
280        });
281        
282        // Get text content and clean it up
283        pageContent = clone.textContent || clone.innerText || '';
284        
285        // Add context data to page content if any were found
286        if (contextData.length > 0) {
287            pageContent += '\n\nAdditional Context:\n' + contextData.join('\n');
288        }
289        
290        // Clean up whitespace and limit length
291        pageContent = pageContent
292            .replace(/\s+/g, ' ')
293            .trim()
294            .substring(0, 3000); // Limit to 3000 characters to avoid token limits
295    }
296    
297    // Only return context if we have meaningful content
298    if (!pageContent || pageContent.length < 50) {
299        return null;
300    }
301    
302    return {
303        url: pageUrl,
304        title: pageTitle,
305        content: pageContent
306    };
307}
308
309//   Track originating page when chat starts
310function trackOriginatingPage() {
311    const sessionId = getChatSession();
312    const pageUrl = window.location.href;
313    const pageTitle = document.title || 'Untitled Page';
314    
315    // Only track once per session
316    const trackingKey = 'mxchat_originating_tracked_' + sessionId;
317    if (sessionStorage.getItem(trackingKey)) {
318        return;
319    }
320    
321    $.ajax({
322        url: mxchatChat.ajax_url,
323        type: 'POST',
324        data: {
325            action: 'mxchat_track_originating_page',
326            session_id: sessionId,
327            page_url: pageUrl,
328            page_title: pageTitle,
329            nonce: mxchatChat.nonce
330        },
331        success: function(response) {
332            if (response.success) {
333                sessionStorage.setItem(trackingKey, 'true');
334            }
335        }
336    });
337}
338
339// ====================================
340// CORE CHAT FUNCTIONALITY
341// ====================================
342
343// Helper functions to disable/enable chat input while waiting for response
344function disableChatInput(botId) {
345    botId = botId || 'default';
346    var chatInput = getElementDOM(botId, 'chat-input');
347    var sendButton = getElementDOM(botId, 'send-button');
348    if (chatInput) {
349        chatInput.disabled = true;
350        chatInput.style.opacity = '0.6';
351    }
352    if (sendButton) {
353        sendButton.disabled = true;
354        sendButton.style.opacity = '0.5';
355        sendButton.style.pointerEvents = 'none';
356    }
357}
358
359function enableChatInput(botId) {
360    botId = botId || 'default';
361    var chatInput = getElementDOM(botId, 'chat-input');
362    var sendButton = getElementDOM(botId, 'send-button');
363    if (chatInput) {
364        chatInput.disabled = false;
365        chatInput.style.opacity = '1';
366        chatInput.focus();
367    }
368    if (sendButton) {
369        sendButton.disabled = false;
370        sendButton.style.opacity = '1';
371        sendButton.style.pointerEvents = 'auto';
372    }
373}
374
375// Update your existing sendMessage function
376function sendMessage(botId) {
377    botId = botId || 'default';
378    var $chatInput = getElement(botId, 'chat-input');
379    var message = $chatInput.val();
380
381    // ADD PROMPT HOOK HERE
382    if (typeof customMxChatFilter === 'function') {
383        message = customMxChatFilter(message, "prompt");
384    }
385
386    if (message) {
387        // Disable input while waiting for response
388        disableChatInput(botId);
389
390        appendMessage("user", message, '', [], false, botId);
391        $chatInput.val('');
392        $chatInput.css('height', 'auto');
393
394        if (hasQuickQuestions(botId)) {
395            collapseQuickQuestions(botId);
396        }
397        appendThinkingMessage(botId);
398        scrollToBottom(botId);
399
400        const currentModel = mxchatChat.model || 'gpt-4o';
401
402        // Check if streaming is enabled AND supported for this model
403        if (shouldUseStreaming(currentModel)) {
404            callMxChatStream(message, function(response) {
405                getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
406            }, botId);
407        } else {
408            callMxChat(message, function(response) {
409                replaceLastMessage("bot", response, '', [], botId);
410            }, botId);
411        }
412    }
413}
414
415// Update your existing sendMessageToChatbot function
416function sendMessageToChatbot(message, botId) {
417    botId = botId || 'default';
418
419    // ADD PROMPT HOOK HERE
420    if (typeof customMxChatFilter === 'function') {
421        message = customMxChatFilter(message, "prompt");
422    }
423
424    // Disable input while waiting for response
425    disableChatInput(botId);
426
427    var sessionId = getChatSession(botId);
428
429    if (hasQuickQuestions(botId)) {
430        collapseQuickQuestions(botId);
431    }
432    appendThinkingMessage(botId);
433    scrollToBottom(botId);
434
435    const currentModel = mxchatChat.model || 'gpt-4o';
436
437    // Check if streaming is enabled AND supported for this model
438    if (shouldUseStreaming(currentModel)) {
439        callMxChatStream(message, function(response) {
440            getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
441        }, botId);
442    } else {
443        callMxChat(message, function(response) {
444            getElement(botId, 'chat-box').find('.temporary-message').remove();
445            replaceLastMessage("bot", response, '', [], botId);
446        }, botId);
447    }
448}
449
450// Updated shouldUseStreaming function with debugging
451function shouldUseStreaming(model) {
452    // Check if streaming is enabled in settings (using your toggle naming pattern)
453    const streamingEnabled = mxchatChat.enable_streaming_toggle === 'on';
454    
455    // Check if model supports streaming
456    const streamingSupported = isStreamingSupported(model);
457    
458    
459    // Only use streaming if both enabled and supported
460    return streamingEnabled && streamingSupported;
461}
462
463// Helper function to handle chat mode updates
464function handleChatModeUpdates(response, responseText) {
465    // Check for explicit chat mode in response (THIS IS THE KEY FIX)
466    if (response.chat_mode) {
467        updateChatModeIndicator(response.chat_mode);
468        return; // Return early since we found explicit mode
469    }
470    // Check for fallback response chat mode
471    else if (response.fallbackResponse && response.fallbackResponse.chat_mode) {
472        updateChatModeIndicator(response.fallbackResponse.chat_mode);
473        return; // Return early since we found explicit mode
474    }
475    
476    // Only do text-based detection if no explicit mode was provided
477    // Check for specific AI chatbot response text
478    if (responseText === 'You are now chatting with the AI chatbot.' || 
479        responseText.includes('now chatting with the AI') ||
480        responseText.includes('switched to AI mode') ||
481        responseText.includes('AI chatbot is now')) {
482        updateChatModeIndicator('ai');
483    }
484    // Check for agent transfer messages
485    else if (responseText.includes('agent') && 
486             (responseText.includes('transfer') || responseText.includes('connected'))) {
487        updateChatModeIndicator('agent');
488    }
489}
490
491// Function to get bot ID from any element or wrapper
492// If element is provided, finds the bot ID from its wrapper
493// If no element, returns 'default' (for backward compatibility)
494function getMxChatBotId(element) {
495    if (element) {
496        return getBotIdFromElement(element);
497    }
498    // Fallback: find first chatbot wrapper on page
499    const chatbotWrapper = document.querySelector('.mxchat-chatbot-wrapper');
500    return chatbotWrapper ? chatbotWrapper.getAttribute('data-bot-id') || 'default' : 'default';
501}
502
503function callMxChat(message, callback, botId) {
504    botId = botId || getMxChatBotId();
505
506    // Store the message in case we need to retry after session reset
507    getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
508
509    // Get page context if contextual awareness is enabled
510    const pageContext = getPageContext();
511
512    // Prepare AJAX data
513    const ajaxData = {
514        action: 'mxchat_handle_chat_request',
515        message: message,
516        session_id: getChatSession(botId),
517        nonce: mxchatChat.nonce,
518        current_page_url: window.location.href,
519        current_page_title: document.title,
520        bot_id: botId
521    };
522    
523    // Add page context if available
524    if (pageContext) {
525        ajaxData.page_context = JSON.stringify(pageContext);
526    }
527    
528    // CHECK FOR VISION FLAGS AND ADD THEM
529    if (window.mxchatVisionProcessed) {
530        ajaxData.vision_processed = true;
531        ajaxData.original_user_message = window.mxchatOriginalMessage || message;
532        ajaxData.vision_images_count = window.mxchatVisionImagesCount || 0;
533        // Clear the flags after use
534        window.mxchatVisionProcessed = false;
535        window.mxchatOriginalMessage = null;
536        window.mxchatVisionImagesCount = 0;
537    }
538    
539    $.ajax({
540        url: mxchatChat.ajax_url,
541        type: 'POST',
542        dataType: 'json',
543        data: ajaxData,
544        success: function(response) {
545            // IMMEDIATE CHAT MODE UPDATE - This should be FIRST
546            if (response.chat_mode) {
547                updateChatModeIndicator(response.chat_mode, botId);
548            }
549
550            // Also check in data property if response is wrapped
551            if (response.data && response.data.chat_mode) {
552                updateChatModeIndicator(response.data.chat_mode, botId);
553            }
554
555            // SECURITY FIX: Check for errors FIRST before checking for success
556            // This ensures API errors (quota exceeded, invalid key, rate limit) are properly displayed
557            if (response.success === false || (response.data && response.data.error_message)) {
558                let errorMessage = "";
559                let errorCode = "";
560
561                // Check various possible error locations in the response
562                if (response.data && response.data.error_message) {
563                    errorMessage = response.data.error_message;
564                    errorCode = response.data.error_code || "";
565                } else if (response.error_message) {
566                    errorMessage = response.error_message;
567                    errorCode = response.error_code || "";
568                } else if (response.message) {
569                    errorMessage = response.message;
570                } else if (typeof response.data === 'string') {
571                    errorMessage = response.data;
572                } else {
573                    // Fallback for any other unexpected response format
574                    errorMessage = "An error occurred. Please try again or contact support.";
575                }
576
577                // Handle session reset action (IP changed, session expired, etc.)
578                if (response.data && response.data.action === 'reset_session') {
579                    // Clear the old session and generate a new one
580                    resetChatSession(botId);
581                    // Remove the temporary loading message
582                    getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
583                    // Re-send the original message with the new session
584                    var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
585                    if (originalMessage) {
586                        getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
587                        // Re-add the user message and thinking indicator
588                        appendMessage("user", originalMessage, '', [], false, botId);
589                        appendThinkingMessage(botId);
590                        scrollToBottom(botId);
591                        // Determine whether to use streaming
592                        const currentModel = mxchatChat.model || 'gpt-4o';
593                        if (shouldUseStreaming(currentModel)) {
594                            callMxChatStream(originalMessage, function(response) {
595                                getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
596                            }, botId);
597                        } else {
598                            callMxChat(originalMessage, function(response) {
599                                replaceLastMessage("bot", response, '', [], botId);
600                            }, botId);
601                        }
602                    }
603                    return;
604                }
605
606                // Format user-friendly error message
607                let displayMessage = errorMessage;
608
609                // Customize message for admin users
610                if (mxchatChat.is_admin) {
611                    // For admin users, show more technical details including error code
612                    displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
613                }
614
615                replaceLastMessage("bot", displayMessage, '', [], botId);
616                return; // Exit early for errors
617            }
618
619            // NOW check if this is a successful response by looking for text, html, or message fields
620            // This preserves compatibility with your server response format
621            if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
622                (response.success === true && response.data && response.data.status === 'waiting_for_agent')) {
623
624                // Handle successful response - this is your original success handling code
625
626                // Handle other responses
627                let responseText = response.text || '';
628                let responseHtml = response.html || '';
629                let responseMessage = response.message || '';
630
631                // Add PDF filename handling
632                if (response.data && response.data.filename) {
633                    showActivePdf(response.data.filename, botId);
634                    var instance = MxChatInstances.get(botId);
635                    instance.activePdfFile = response.data.filename;
636                }
637
638                // Add redirect check here
639                if (response.redirect_url) {
640                    if (responseText) {
641                        replaceLastMessage("bot", responseText, '', [], botId);
642                    }
643                    setTimeout(() => {
644                        window.location.href = response.redirect_url;
645                    }, 1500);
646                    return;
647                }
648
649                // Check for live agent response
650                if (response.success && response.data && response.data.status === 'waiting_for_agent') {
651                    updateChatModeIndicator('agent', botId);
652                    return;
653                }
654
655                // Handle the message and show notification if chat is hidden
656                if (responseText || responseHtml || responseMessage) {
657
658                    // ADD RESPONSE HOOKS HERE - BEFORE DISPLAYING
659                    if (responseText && typeof customMxChatFilter === 'function') {
660                        responseText = customMxChatFilter(responseText, "response");
661                    }
662                    if (responseMessage && typeof customMxChatFilter === 'function') {
663                        responseMessage = customMxChatFilter(responseMessage, "response");
664                    }
665
666                    // Update the messages as before
667                    if (responseText && responseHtml) {
668                        replaceLastMessage("bot", responseText, responseHtml, [], botId);
669                    } else if (responseText) {
670                        replaceLastMessage("bot", responseText, '', [], botId);
671                    } else if (responseHtml) {
672                        replaceLastMessage("bot", "", responseHtml, [], botId);
673                    } else if (responseMessage) {
674                        replaceLastMessage("bot", responseMessage, '', [], botId);
675                    }
676
677                    // Check if chat is hidden and show notification
678                    var $floatingChatbot = getElement(botId, 'floating-chatbot');
679                    if ($floatingChatbot.hasClass('hidden')) {
680                        var $badge = getElement(botId, 'chat-notification-badge');
681                        if ($badge.length) {
682                            $badge.show();
683                        }
684                    }
685                } else {
686                    replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
687                }
688
689                if (response.message_id) {
690                    var instance = MxChatInstances.get(botId);
691                    instance.lastSeenMessageId = response.message_id;
692                }
693
694                return;
695            }
696
697            // Fallback for truly unexpected response formats
698            replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.", '', [], botId);
699        },
700        error: function(xhr, status, error) {
701            let errorMessage = "An unexpected error occurred.";
702
703            // Try to parse the response if it's JSON
704            try {
705                const responseJson = JSON.parse(xhr.responseText);
706
707                if (responseJson.data && responseJson.data.error_message) {
708                    errorMessage = responseJson.data.error_message;
709                } else if (responseJson.message) {
710                    errorMessage = responseJson.message;
711                }
712            } catch (e) {
713                // Not JSON or parsing failed, use HTTP status based messages
714                if (xhr.status === 0) {
715                    errorMessage = "Network error: Please check your internet connection.";
716                } else if (xhr.status === 403) {
717                    errorMessage = "Access denied: Your session may have expired. Please refresh the page.";
718                } else if (xhr.status === 404) {
719                    errorMessage = "API endpoint not found. Please contact support.";
720                } else if (xhr.status === 429) {
721                    errorMessage = "Too many requests. Please try again in a moment.";
722                } else if (xhr.status >= 500) {
723                    errorMessage = "Server error: The server encountered an issue. Please try again later.";
724                }
725            }
726
727            replaceLastMessage("bot", errorMessage, '', [], botId);
728        }
729    });
730}
731
732function callMxChatStream(message, callback, botId) {
733    botId = botId || getMxChatBotId();
734
735    // Store the message in case we need to retry after session reset
736    getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
737
738    const currentModel = mxchatChat.model || 'gpt-4o';
739    if (!isStreamingSupported(currentModel)) {
740        callMxChat(message, callback, botId);
741        return;
742    }
743
744    // Get page context if contextual awareness is enabled
745    const pageContext = getPageContext();
746
747    const formData = new FormData();
748    formData.append('action', 'mxchat_stream_chat');
749    formData.append('message', message);
750    formData.append('session_id', getChatSession(botId));
751    formData.append('nonce', mxchatChat.nonce);
752    formData.append('current_page_url', window.location.href);
753    formData.append('current_page_title', document.title);
754    formData.append('bot_id', botId);
755    
756    // Add page context if available
757    if (pageContext) {
758        formData.append('page_context', JSON.stringify(pageContext));
759    }
760
761    // CHECK FOR VISION FLAGS AND ADD THEM
762    if (window.mxchatVisionProcessed) {
763        formData.append('vision_processed', 'true');
764        formData.append('original_user_message', window.mxchatOriginalMessage || message);
765        formData.append('vision_images_count', window.mxchatVisionImagesCount || '0');
766        // Clear the flags after use
767        window.mxchatVisionProcessed = false;
768        window.mxchatOriginalMessage = null;
769        window.mxchatVisionImagesCount = 0;
770    }
771
772    let accumulatedContent = '';
773    let testingDataReceived = false;
774    let streamingStarted = false;
775
776    fetch(mxchatChat.ajax_url, {
777        method: 'POST',
778        body: formData,
779        credentials: 'same-origin'
780    })
781    .then(response => {
782        // Store the response for potential fallback handling
783        const responseClone = response.clone();
784
785        if (!response.ok) {
786            // Try to get error details from response
787            return responseClone.json().then(errorData => {
788                throw { isServerError: true, data: errorData };
789            }).catch(() => {
790                throw new Error('Network response was not ok');
791            });
792        }
793
794    // Check if response is JSON instead of streaming
795    const contentType = response.headers.get('content-type');
796    if (contentType && contentType.includes('application/json')) {
797        return responseClone.json().then(data => {
798            // IMMEDIATE CHAT MODE UPDATE for JSON response
799            if (data.chat_mode) {
800                updateChatModeIndicator(data.chat_mode, botId);
801            }
802
803            // Check for testing panel
804            if (window.mxchatTestPanelInstance && data.testing_data) {
805                window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
806            }
807
808            // Handle the JSON response directly
809            handleNonStreamResponse(data, callback, botId);
810            return Promise.resolve(); // Prevent further processing
811        });
812    }
813
814        // Continue with streaming processing
815        const reader = response.body.getReader();
816        const decoder = new TextDecoder();
817        let buffer = '';
818
819        function processStream() {
820            reader.read().then(({ done, value }) => {
821                if (done) {
822                    // If streaming completed but no content was received, try to get response as fallback
823                    if (!streamingStarted || !accumulatedContent) {
824                        // Try to read the response as JSON
825                        responseClone.text().then(text => {
826                            try {
827                                const data = JSON.parse(text);
828                                if (data.text || data.message || data.html) {
829                                    handleNonStreamResponse(data, callback, botId);
830                                } else {
831                                    // No valid data, fall back to regular call
832                                    getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
833                                    callMxChat(message, callback, botId);
834                                }
835                            } catch (e) {
836                                // Could not parse, fall back to regular call
837                                getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
838                                callMxChat(message, callback, botId);
839                            }
840                        }).catch(() => {
841                            getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
842                            callMxChat(message, callback, botId);
843                        });
844                        return;
845                    }
846
847                    // Re-enable chat input when stream ends with content
848                    enableChatInput(botId);
849
850                    if (callback) {
851                        callback(accumulatedContent);
852                    }
853                    return;
854                }
855
856                buffer += decoder.decode(value, { stream: true });
857                const lines = buffer.split('\n');
858                buffer = lines.pop() || '';
859
860                for (const line of lines) {
861                    if (line.startsWith('data: ')) {
862                        const data = line.substring(6);
863
864                        if (data === '[DONE]') {
865                            if (!accumulatedContent) {
866                                getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
867                                callMxChat(message, callback, botId);
868                                return;
869                            }
870
871                            // Re-enable chat input after streaming completes
872                            enableChatInput(botId);
873
874                            if (callback) {
875                                callback(accumulatedContent);
876                            }
877                            return;
878                        }
879
880                        try {
881                            const json = JSON.parse(data);
882
883                            // IMMEDIATE CHAT MODE UPDATE FOR STREAMING
884                            if (json.chat_mode) {
885                                updateChatModeIndicator(json.chat_mode, botId);
886                            }
887
888                            // Handle testing data
889                            if (json.testing_data && !testingDataReceived) {
890                                if (window.mxchatTestPanelInstance) {
891                                    window.mxchatTestPanelInstance.handleTestingData(json.testing_data);
892                                    testingDataReceived = true;
893                                }
894                            }
895                            // Handle content streaming
896                            else if (json.content) {
897                                streamingStarted = true;
898                                accumulatedContent += json.content;
899                                updateStreamingMessage(accumulatedContent, botId);
900                            }
901                            // Handle complete response in stream (fallback response)
902                            else if (json.text || json.message || json.html) {
903                                handleNonStreamResponse(json, callback, botId);
904                                return;
905                            }
906                            // Handle errors
907                            else if (json.error) {
908
909                                // Get error message from various possible fields
910                                let errorMessage = json.error_message || json.message || json.text ||
911                                    (typeof json.error === 'string' ? json.error : 'An error occurred. Please try again.');
912
913                                // Re-enable chat input on error
914                                enableChatInput(botId);
915
916                                // Display the error directly in the chat
917                                replaceLastMessage("bot", errorMessage, '', [], botId);
918
919                                if (callback) {
920                                    callback(errorMessage);
921                                }
922                                return;
923                            }
924                        } catch (e) {
925                            // SSE data parsing error - silently continue
926                        }
927                    }
928                }
929
930                processStream();
931            }).catch(streamError => {
932                getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
933                callMxChat(message, callback, botId);
934            });
935        }
936
937        processStream();
938    })
939        .catch(error => {
940            // Check if we have server error data with chat mode
941            if (error && error.isServerError && error.data) {
942                // Check for chat mode in error data
943                if (error.data.chat_mode) {
944                    updateChatModeIndicator(error.data.chat_mode, botId);
945                }
946
947                handleNonStreamResponse(error.data, callback, botId);
948            } else {
949                // Only fall back to regular call if we don't have any response data
950                getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
951                callMxChat(message, callback, botId);
952            }
953        });
954}
955
956// Helper function to handle non-streaming responses
957function handleNonStreamResponse(data, callback, botId) {
958    botId = botId || 'default';
959
960    // IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES
961    if (data.chat_mode) {
962        updateChatModeIndicator(data.chat_mode, botId);
963    }
964
965    // Also check in data property if response is wrapped
966    if (data.data && data.data.chat_mode) {
967        updateChatModeIndicator(data.data.chat_mode, botId);
968    }
969
970    // NOTE: Don't remove temporary message here - let replaceLastMessage handle it
971    // This prevents a visual gap between thinking dots disappearing and content appearing
972
973    // SECURITY FIX: Check for errors FIRST
974    if (data.success === false || (data.data && data.data.error_message)) {
975        let errorMessage = "";
976        let errorCode = "";
977
978        // Check various possible error locations
979        if (data.data && data.data.error_message) {
980            errorMessage = data.data.error_message;
981            errorCode = data.data.error_code || "";
982        } else if (data.error_message) {
983            errorMessage = data.error_message;
984            errorCode = data.error_code || "";
985        } else if (data.message) {
986            errorMessage = data.message;
987        } else if (typeof data.data === 'string') {
988            errorMessage = data.data;
989        } else {
990            errorMessage = "An error occurred. Please try again or contact support.";
991        }
992
993        // Handle session reset action (IP changed, session expired, etc.)
994        if (data.data && data.data.action === 'reset_session') {
995            // Clear the old session and generate a new one
996            resetChatSession(botId);
997            // Re-send the original message with the new session
998            var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
999            if (originalMessage) {
1000                getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1001                // Re-add the user message and thinking indicator
1002                appendMessage("user", originalMessage, '', [], false, botId);
1003                appendThinkingMessage(botId);
1004                scrollToBottom(botId);
1005                // Determine whether to use streaming
1006                const currentModel = mxchatChat.model || 'gpt-4o';
1007                if (shouldUseStreaming(currentModel)) {
1008                    callMxChatStream(originalMessage, callback, botId);
1009                } else {
1010                    callMxChat(originalMessage, callback, botId);
1011                }
1012            }
1013            return;
1014        }
1015
1016        // Format user-friendly error message
1017        let displayMessage = errorMessage;
1018        if (mxchatChat.is_admin) {
1019            displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
1020        }
1021
1022        replaceLastMessage("bot", displayMessage, '', [], botId);
1023
1024        if (callback) {
1025            callback('');
1026        }
1027        return; // Exit early for errors
1028    }
1029
1030    // Handle different response formats
1031    if (data.text || data.html || data.message) {
1032
1033        // Apply response hooks
1034        if (data.text && typeof customMxChatFilter === 'function') {
1035            data.text = customMxChatFilter(data.text, "response");
1036        }
1037        if (data.message && typeof customMxChatFilter === 'function') {
1038            data.message = customMxChatFilter(data.message, "response");
1039        }
1040
1041        // Display the response
1042        if (data.text && data.html) {
1043            replaceLastMessage("bot", data.text, data.html, [], botId);
1044        } else if (data.text) {
1045            replaceLastMessage("bot", data.text, '', [], botId);
1046        } else if (data.html) {
1047            replaceLastMessage("bot", "", data.html, [], botId);
1048        } else if (data.message) {
1049            replaceLastMessage("bot", data.message, '', [], botId);
1050        }
1051    }
1052
1053    // Handle other response properties
1054    if (data.data && data.data.filename) {
1055        showActivePdf(data.data.filename, botId);
1056        var instance = MxChatInstances.get(botId);
1057        instance.activePdfFile = data.data.filename;
1058    }
1059
1060    if (data.redirect_url) {
1061        setTimeout(() => {
1062            window.location.href = data.redirect_url;
1063        }, 1500);
1064    }
1065
1066    // Ensure chat input is re-enabled (safety net for edge cases)
1067    enableChatInput(botId);
1068
1069    if (callback) {
1070        callback(data.text || data.message || '');
1071    }
1072}
1073
1074// Enhanced updateChatModeIndicator function for immediate DOM updates
1075function updateChatModeIndicator(mode, botId) {
1076    console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1077    botId = botId || 'default';
1078    const indicator = getElementDOM(botId, 'chat-mode-indicator');
1079    console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1080    if (indicator) {
1081        const oldText = indicator.textContent;
1082        console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1083
1084        if (mode === 'agent') {
1085            indicator.textContent = 'Live Agent';
1086            console.log('[MxChat] Mode is agent, calling startPolling...');
1087            startPolling(botId);
1088        } else {
1089            // Everything else is AI mode
1090            const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1091            indicator.textContent = customAiText;
1092            stopPolling(botId);
1093        }
1094
1095        // Force immediate DOM update and reflow
1096        if (oldText !== indicator.textContent) {
1097            // Force a reflow to ensure the change is visible immediately
1098            indicator.style.display = 'none';
1099            indicator.offsetHeight; // Trigger reflow
1100            indicator.style.display = '';
1101
1102            // Double-check after a brief moment to ensure the change stuck
1103            setTimeout(() => {
1104                if (mode === 'agent' && indicator.textContent !== 'Live Agent') {
1105                    indicator.textContent = 'Live Agent';
1106                } else if (mode !== 'agent' && indicator.textContent === 'Live Agent') {
1107                    const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1108                    indicator.textContent = customAiText;
1109                }
1110            }, 50);
1111        }
1112    }
1113}
1114
1115// Function to update message during streaming
1116function updateStreamingMessage(content, botId) {
1117    botId = botId || 'default';
1118
1119    // ADD RESPONSE HOOK FOR REAL-TIME STREAMING
1120    if (typeof customMxChatFilter === 'function') {
1121        content = customMxChatFilter(content, "response");
1122    }
1123
1124    const formattedContent = linkify(content);
1125
1126    // Find the temporary message in this bot's chat box
1127    var $chatBox = getElement(botId, 'chat-box');
1128    const tempMessage = $chatBox.find('.bot-message.temporary-message').last();
1129
1130    if (tempMessage.length) {
1131        // Update existing message
1132        tempMessage.html(formattedContent);
1133    } else {
1134        // Create new temporary message if it doesn't exist
1135        appendMessage("bot", content, '', [], true, botId);
1136    }
1137}
1138
1139function isStreamingSupported(model) {
1140    if (!model) return false;
1141
1142    const modelPrefix = model.split('-')[0].toLowerCase();
1143
1144    // Support streaming for OpenAI, Claude, Grok, DeepSeek, and OpenRouter models
1145    const isSupported = modelPrefix === 'gpt' || 
1146                        modelPrefix === 'o1' || 
1147                        modelPrefix === 'claude' || 
1148                        modelPrefix === 'grok' || 
1149                        modelPrefix === 'deepseek' || 
1150                        model === 'openrouter';  // Add this line - check full model name for OpenRouter
1151    
1152    return isSupported;
1153}
1154
1155// Update the event handlers to use the correct function names (using event delegation)
1156// Use class-based selectors for multi-instance support
1157$(document).on('click', '.send-button', function() {
1158    var botId = getBotIdFromElement(this);
1159    disableChatInput(botId);
1160    sendMessage(botId);
1161});
1162
1163// Override enter key handler (using event delegation)
1164$(document).on('keypress', '.chat-input', function(e) {
1165    if (e.which == 13 && !e.shiftKey) {
1166        e.preventDefault();
1167        var botId = getBotIdFromElement(this);
1168        disableChatInput(botId);
1169        sendMessage(botId);
1170    }
1171});
1172
1173    
1174function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1175    try {
1176        // Determine styles based on sender type
1177        let messageClass, bgColor, fontColor;
1178
1179        if (sender === "user") {
1180            messageClass = "user-message";
1181            bgColor = userMessageBgColor;
1182            fontColor = userMessageFontColor;
1183            // Only sanitize user input
1184            messageText = sanitizeUserInput(messageText);
1185        } else if (sender === "agent") {
1186            messageClass = "agent-message";
1187            bgColor = liveAgentMessageBgColor;
1188            fontColor = liveAgentMessageFontColor;
1189        } else {
1190            messageClass = "bot-message";
1191            bgColor = botMessageBgColor;
1192            fontColor = botMessageFontColor;
1193        }
1194
1195        const messageDiv = $('<div>')
1196            .addClass(messageClass)
1197            .attr('dir', 'auto');
1198
1199        // Only apply inline colors if AI theme is not active (let CSS handle it)
1200        var skipColors = shouldSkipInlineColors(botId);
1201        if (skipColors) {
1202            messageDiv.css({
1203                'margin-bottom': '1em'
1204            });
1205        } else {
1206            messageDiv.css({
1207                'background': bgColor,
1208                'color': fontColor,
1209                'margin-bottom': '1em'
1210            });
1211        }
1212
1213        // Process the message content based on sender
1214        let fullMessage;
1215        if (sender === "user") {
1216            // For user messages, apply linkify after sanitization
1217            fullMessage = linkify(messageText);
1218        } else {
1219            // For bot/agent messages, preserve HTML
1220            fullMessage = messageText;
1221        }
1222
1223        // Add images if provided
1224        if (images && images.length > 0) {
1225            fullMessage += '<div class="image-gallery" dir="auto">';
1226            images.forEach(img => {
1227                const safeTitle = sanitizeUserInput(img.title);
1228                const safeUrl = encodeURI(img.image_url);
1229                const safeThumbnail = encodeURI(img.thumbnail_url);
1230
1231                fullMessage += `
1232                    <div style="margin-bottom: 10px;">
1233                        <strong>${safeTitle}</strong><br>
1234                        <a href="${safeUrl}" target="_blank">
1235                            <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
1236                        </a>
1237                    </div>`;
1238            });
1239            fullMessage += '</div>';
1240        }
1241
1242        // Append HTML content if provided
1243        if (messageHtml && sender !== "user") {
1244            // Only add line breaks if there's actual text content before the HTML
1245            if (fullMessage && fullMessage.trim()) {
1246                fullMessage += '<br><br>' + messageHtml;
1247            } else {
1248                fullMessage = messageHtml;
1249            }
1250        }
1251
1252        messageDiv.html(fullMessage);
1253
1254        if (isTemporary) {
1255            messageDiv.addClass('temporary-message');
1256        }
1257
1258        // Append to the correct chatbot instance's chat-box
1259        var $chatBox = getElement(botId, 'chat-box');
1260        messageDiv.hide().appendTo($chatBox).fadeIn(300, function() {
1261            // FIXED: Use event delegation for link tracking
1262            if (sender === "bot" || sender === "agent") {
1263                attachLinkTracking(messageDiv, messageText, botId);
1264            }
1265
1266            if (sender === "bot") {
1267                const lastUserMessage = $chatBox.find('.user-message').last();
1268                if (lastUserMessage.length) {
1269                    scrollElementToTop(lastUserMessage, botId);
1270                }
1271            }
1272        });
1273
1274        if (messageText.id) {
1275            var instance = MxChatInstances.get(botId);
1276            instance.lastSeenMessageId = messageText.id;
1277            hideNotification(botId);
1278        }
1279    } catch (error) {
1280        // Error rendering message - silently continue
1281    }
1282}
1283
1284//   Helper function to attach link tracking with proper event handling
1285function attachLinkTracking(messageDiv, messageText, botId) {
1286    botId = botId || 'default';
1287    // Use a slight delay to ensure DOM is ready
1288    setTimeout(function() {
1289        const links = messageDiv.find('a[href]').not('[data-tracked]');
1290
1291        links.each(function() {
1292            const $link = $(this);
1293            const originalHref = $link.attr('href');
1294
1295            // Mark as tracked to avoid duplicate handlers
1296            $link.attr('data-tracked', 'true');
1297
1298            // Only track external URLs
1299            if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
1300                // Remove any existing click handlers first
1301                $link.off('click.tracking');
1302
1303                // Add new click handler with namespace
1304                $link.on('click.tracking', function(e) {
1305                    e.preventDefault();
1306                    e.stopPropagation();
1307
1308                    const messageContext = typeof messageText === 'string'
1309                        ? messageText.substring(0, 200)
1310                        : '';
1311
1312                    // Track the click
1313                    $.ajax({
1314                        url: mxchatChat.ajax_url,
1315                        type: 'POST',
1316                        data: {
1317                            action: 'mxchat_track_url_click',
1318                            session_id: getChatSession(botId),
1319                            url: originalHref,
1320                            message_context: messageContext,
1321                            nonce: mxchatChat.nonce
1322                        },
1323                        complete: function() {
1324                            // Always redirect, even if tracking fails
1325                            if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
1326                                window.open(originalHref, '_blank');
1327                            } else {
1328                                window.location.href = originalHref;
1329                            }
1330                        }
1331                    });
1332
1333                    return false; // Extra insurance to prevent default
1334                });
1335            }
1336        });
1337    }, 100); // Small delay to ensure DOM is ready
1338}
1339
1340function replaceLastMessage(sender, responseText, responseHtml = '', images = [], botId = 'default') {
1341    var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
1342    var $chatBox = getElement(botId, 'chat-box');
1343    var lastMessageDiv = $chatBox.find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1344
1345    // Determine styles
1346    let bgColor, fontColor;
1347    if (sender === "user") {
1348        bgColor = userMessageBgColor;
1349        fontColor = userMessageFontColor;
1350    } else if (sender === "agent") {
1351        bgColor = liveAgentMessageBgColor;
1352        fontColor = liveAgentMessageFontColor;
1353    } else {
1354        bgColor = botMessageBgColor;
1355        fontColor = botMessageFontColor;
1356    }
1357
1358    // FIXED: Only linkify if response doesn't already contain HTML links or tags
1359    // This prevents double-processing of URLs that are already formatted as HTML
1360    var fullMessage;
1361    if (sender === "user") {
1362        // Always linkify user messages (they're plain text)
1363        fullMessage = linkify(responseText);
1364    } else {
1365        // For bot/agent messages, check if HTML already exists
1366        if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1367            responseText.includes('<img') || responseText.includes('<div') ||
1368            responseText.includes('<p>') || responseText.includes('<br>')) {
1369            // Response already has HTML, don't process it
1370            fullMessage = responseText;
1371        } else {
1372            // Plain text response, apply linkify
1373            fullMessage = linkify(responseText);
1374        }
1375    }
1376
1377    if (responseHtml) {
1378        // Only add line breaks if there's actual text content before the HTML
1379        if (fullMessage && fullMessage.trim()) {
1380            fullMessage += '<br><br>' + responseHtml;
1381        } else {
1382            fullMessage = responseHtml;
1383        }
1384    }
1385
1386    if (images.length > 0) {
1387        fullMessage += '<div class="image-gallery" dir="auto">';
1388        images.forEach(img => {
1389            fullMessage += `
1390                <div style="margin-bottom: 10px;">
1391                    <strong>${img.title}</strong><br>
1392                    <a href="${img.image_url}" target="_blank">
1393                        <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
1394                    </a>
1395                </div>`;
1396        });
1397        fullMessage += '</div>';
1398    }
1399
1400    if (lastMessageDiv.length) {
1401        // Replace content immediately to prevent visual gap between thinking dots and response
1402        lastMessageDiv
1403            .html(fullMessage)
1404            .removeClass('bot-message user-message temporary-message')
1405            .addClass(messageClass)
1406            .attr('dir', 'auto');
1407
1408        // Only apply inline colors if AI theme is not active (let CSS handle it)
1409        var skipColors = mxchatChat.skip_inline_colors || shouldSkipInlineColors(botId);
1410        if (!skipColors) {
1411            lastMessageDiv.css({
1412                'background-color': bgColor,
1413                'color': fontColor,
1414            });
1415        }
1416
1417        // Handle link tracking and scroll
1418        if (sender === "bot" || sender === "agent") {
1419            attachLinkTracking(lastMessageDiv, responseText, botId);
1420
1421            const lastUserMessage = $chatBox.find('.user-message').last();
1422            if (lastUserMessage.length) {
1423                scrollElementToTop(lastUserMessage, botId);
1424            }
1425            // Show notification if chat is hidden
1426            var $floatingChatbot = getElement(botId, 'floating-chatbot');
1427            if ($floatingChatbot.hasClass('hidden')) {
1428                showNotification(botId);
1429            }
1430        }
1431
1432        // Re-enable chat input after response is displayed
1433        enableChatInput(botId);
1434    } else {
1435        appendMessage(sender, responseText, responseHtml, images, false, botId);
1436        // Re-enable chat input after response is displayed
1437        enableChatInput(botId);
1438    }
1439}
1440
1441
1442    function appendThinkingMessage(botId) {
1443        botId = botId || 'default';
1444        var $chatBox = getElement(botId, 'chat-box');
1445
1446        // Remove any existing thinking dots in this bot's chat first
1447        $chatBox.find('.thinking-dots').remove();
1448
1449        // Check if we should skip inline colors (AI theme is active)
1450        var skipColors = shouldSkipInlineColors(botId);
1451
1452        // Retrieve the bot message font color and background color
1453        var botMessageFontColor = mxchatChat.bot_message_font_color;
1454        var botMessageBgColor = mxchatChat.bot_message_bg_color;
1455
1456        // Build thinking dots HTML - skip inline colors if AI theme is active
1457        var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
1458        var thinkingHtml = '<div class="thinking-dots-container">' +
1459                           '<div class="thinking-dots">' +
1460                           '<span class="dot"' + dotStyle + '></span>' +
1461                           '<span class="dot"' + dotStyle + '></span>' +
1462                           '<span class="dot"' + dotStyle + '></span>' +
1463                           '</div>' +
1464                           '</div>';
1465
1466        // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1467        var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
1468        $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1469        scrollToBottom(botId);
1470    }
1471
1472    function removeThinkingDots(botId) {
1473        botId = botId || 'default';
1474        var $chatBox = getElement(botId, 'chat-box');
1475        $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1476    }
1477
1478    // ====================================
1479    // TEXT FORMATTING & PROCESSING
1480    // ====================================
1481    
1482    function linkify(inputText) {
1483    if (!inputText) {
1484        return '';
1485    }
1486    
1487    // Helper function to check if URL is already encoded
1488    function isUrlEncoded(url) {
1489        // Check for % followed by exactly 2 hex digits
1490        return /%[0-9a-fA-F]{2}/.test(url);
1491    }
1492    
1493    // Helper function to safely encode URLs only if needed
1494    function safeEncodeUrl(url) {
1495        // If URL already contains encoded characters, return as-is
1496        if (isUrlEncoded(url)) {
1497            return url;
1498        }
1499        // Otherwise, encode it
1500        return encodeURI(url);
1501    }
1502    
1503    // Process markdown headers FIRST
1504    let processedText = formatMarkdownHeaders(inputText);
1505    
1506    // Process text styling (bold, italic, strikethrough)
1507    processedText = formatTextStyling(processedText);
1508    
1509    // Process code blocks BEFORE processing links
1510    processedText = formatCodeBlocks(processedText);
1511    
1512    // NOW convert to paragraphs
1513    processedText = convertNewlinesToBreaks(processedText);
1514    
1515    // IMPORTANT: Handle citation-style brackets FIRST [URL] 
1516    // This prevents them from being processed as markdown links
1517    // Match [URL] where URL is a complete URL in square brackets (common in AI citations)
1518    processedText = processedText.replace(/\[(https?:\/\/[^\]]+)\]/g, (match, url) => {
1519        // Clean the URL of any trailing punctuation
1520        let cleanUrl = url.replace(/[.,;!?]+$/, '');
1521        const safeUrl = safeEncodeUrl(cleanUrl);
1522        // Return as a proper link without the brackets
1523        return `<a href="${safeUrl}" target="${linkTarget}">
1523${cleanUrl}</a>`;
1524    });
1525    
1526    // Process proper markdown links with text: [text](url)
1527    // This MUST have non-empty text in the first brackets
1528    const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1529    processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1530        // Make sure we have actual text (not just whitespace)
1531        if (!text || !text.trim()) {
1532            // If no text, treat the URL as the text
1533            let cleanUrl = url.replace(/[.,;!?]+$/, '');
1534            const safeUrl = safeEncodeUrl(cleanUrl);
1535            return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1536        }
1537        
1538        // Clean the URL
1539        let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1540        const safeUrl = safeEncodeUrl(cleanUrl);
1541        const safeText = sanitizeUserInput(text);
1542        return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1543    });
1544    
1545    // Handle empty markdown links: [](url)
1546    // This is a specific case where there's no text
1547    const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1548    processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1549        let cleanUrl = url.replace(/[.,;!?]+$/, '');
1550        const safeUrl = safeEncodeUrl(cleanUrl);
1551        // Use the URL itself as the link text
1552        return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1553    });
1554    
1555    // Process phone numbers: [text](tel:number)
1556    const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1557    processedText = processedText.replace(phonePattern, (match, text, phone) => {
1558        const safePhone = safeEncodeUrl(phone);
1559        const safeText = sanitizeUserInput(text);
1560        return `<a href="${safePhone}">${safeText}</a>`;
1561    });
1562    
1563    // Process mailto links: [text](mailto:email)
1564    const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
1565    processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
1566        const safeMailto = safeEncodeUrl(mailto);
1567        const safeText = sanitizeUserInput(text);
1568        return `<a href="${safeMailto}">${safeText}</a>`;
1569    });
1570    
1571    // Process standalone URLs - but NOT if they're already in <a> tags or brackets
1572    // Updated pattern to be more careful about what it matches
1573    const urlPattern = /(^|[^">=\[\]])(https?:\/\/[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim;
1574    processedText = processedText.replace(urlPattern, (match, prefix, url) => {
1575        // Extra check: make sure this isn't already linked
1576        if (match.includes('href=') || match.includes('</a>')) {
1577            return match;
1578        }
1579        
1580        // Clean trailing punctuation
1581        let cleanUrl = url.replace(/[.,;!?)]+$/, '');
1582        const safeUrl = safeEncodeUrl(cleanUrl);
1583        return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1584    });
1585    
1586    // Process www. URLs - but NOT if they're already in <a> tags or brackets
1587    const wwwPattern = /(^|[^">/\[\]])(www\.[\S]+)(?![^<]*<\/a>)(?!\])/gim;
1588    processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
1589        // Extra check: make sure this isn't already linked
1590        if (match.includes('href=') || match.includes('</a>')) {
1591            return match;
1592        }
1593        
1594        // Clean trailing punctuation
1595        let cleanUrl = url.replace(/[.,;!?)]+$/, '');
1596        const safeUrl = safeEncodeUrl(`http://${cleanUrl}`);
1597        return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1598    });
1599    
1600    return processedText;
1601}
1602     
1603    function formatMarkdownHeaders(text) {
1604        // Handle h1 to h6 headers
1605        return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
1606            const level = hashes.length;
1607            return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`;
1608        });
1609    }
1610    
1611function formatTextStyling(text) {
1612    // IMPORTANT: Protect BOTH HTML href and Markdown URLs from formatting
1613    const protectedSegments = [];
1614    let protectedText = text;
1615    
1616    // Step 1a: Protect HTML href="..." attributes
1617    protectedText = protectedText.replace(/href\s*=\s*["']([^"']+)["']/gi, function(match) {
1618        const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1619        protectedSegments.push(match);
1620        return placeholder;
1621    });
1622    
1623    // Step 1b: Protect Markdown links [text](url)
1624    // This is crucial - we need to protect the URLs in markdown format
1625    protectedText = protectedText.replace(/\[([^\]]*)\]\(([^)]+)\)/g, function(match) {
1626        const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1627        protectedSegments.push(match);
1628        return placeholder;
1629    });
1630    
1631    // Step 1c: Also protect bare URLs that might exist
1632    protectedText = protectedText.replace(/(https?:\/\/[^\s<>"]+)/gi, function(match) {
1633        const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1634        protectedSegments.push(match);
1635        return placeholder;
1636    });
1637    
1638    // Step 2: Now apply text styling to the protected text
1639    // Handle bold text (**text**)
1640    protectedText = protectedText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1641
1642    // Handle italic text (*text* or _text_) - Safari-compatible (no lookbehind)
1643    // Match single asterisks that aren't part of bold (**) by checking they're not followed/preceded by another *
1644    protectedText = protectedText.replace(/(?!\*\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
1645
1646    // Handle underscores for italic - Safari-compatible (no lookbehind)
1647    // Exclude __PROTECTED_N__ placeholders by checking the content doesn't contain PROTECTED
1648    protectedText = protectedText.replace(/(?!__)_((?!PROTECTED)[^_\n]+)_(?!_)/g, '<em>$1</em>');
1649    
1650    // Handle strikethrough (~~text~~)
1651    protectedText = protectedText.replace(/~~(.*?)~~/g, '<del>$1</del>');
1652    
1653    // Step 3: Restore all protected segments
1654    protectedSegments.forEach((original, index) => {
1655        const placeholder = `__PROTECTED_${index}__`;
1656        protectedText = protectedText.replace(placeholder, original);
1657    });
1658    
1659    return protectedText;
1660}
1661    function formatBoldText(text) {
1662        // This function is kept for compatibility but now uses formatTextStyling
1663        return formatTextStyling(text);
1664    }
1665    
1666function convertNewlinesToBreaks(text) {
1667    // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
1668    const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
1669    
1670    // Filter out empty paragraphs and wrap each paragraph in <p> tags
1671    return paragraphs
1672        .map(para => para.trim())
1673        .filter(para => para.length > 0) // Remove empty paragraphs
1674        .map(para => `<p>${para}</p>`)
1675        .join('');
1676}
1677    function formatCodeBlocks(text) {
1678        // Handle fenced code blocks with language specification (```language)
1679        text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, language, code) => {
1680            const lang = language || 'text';
1681            const escapedCode = escapeHtml(code.trim());
1682            return `<div class="mxchat-code-block-container">
1683                <div class="mxchat-code-header">
1684                    <span class="mxchat-code-language">${lang}</span>
1685                    <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1686                </div>
1687                <pre class="mxchat-code-block"><code class="language-${lang}">${escapedCode}</code></pre>
1688            </div>`;
1689        });
1690
1691        // Handle inline code with single backticks
1692        text = text.replace(/`([^`\n]+)`/g, '<code class="mxchat-inline-code">$1</code>');
1693
1694        // Handle raw PHP tags (legacy support)
1695        text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
1696            const escapedCode = escapeHtml(match);
1697            return `<div class="mxchat-code-block-container">
1698                <div class="mxchat-code-header">
1699                    <span class="mxchat-code-language">php</span>
1700                    <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1701                </div>
1702                <pre class="mxchat-code-block"><code class="language-php">${escapedCode}</code></pre>
1703            </div>`;
1704        });
1705
1706        return text;
1707    }
1708    
1709    function sanitizeUserInput(text) {
1710        const div = document.createElement('div');
1711        div.textContent = text;
1712        return div.innerHTML;
1713    }
1714
1715    function escapeHtml(unsafe) {
1716        // Skip escaping if it's already escaped or contains HTML code block markup
1717        if (unsafe.includes('&lt;') || unsafe.includes('&gt;') || 
1718            unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
1719            return unsafe;
1720        }
1721        
1722        return unsafe
1723            .replace(/&/g, "&amp;")
1724            .replace(/</g, "&lt;")
1725            .replace(/>/g, "&gt;")
1726            .replace(/"/g, "&quot;")
1727            .replace(/'/g, "&#039;");
1728    }
1729    
1730    function decodeHTMLEntities(text) {
1731        var textArea = document.createElement('textarea');
1732        textArea.innerHTML = text;
1733        return textArea.value;
1734    }
1735
1736    // ====================================
1737    // UI & SCROLLING CONTROLS
1738    // ====================================
1739    
1740    function scrollToBottom(botIdOrInstant, instant) {
1741        // Handle backward compatibility: scrollToBottom() or scrollToBottom(true/false)
1742        var botId = 'default';
1743        if (typeof botIdOrInstant === 'string') {
1744            botId = botIdOrInstant;
1745            instant = instant || false;
1746        } else if (typeof botIdOrInstant === 'boolean') {
1747            instant = botIdOrInstant;
1748        } else {
1749            instant = false;
1750        }
1751
1752        var chatBox = getElement(botId, 'chat-box');
1753        if (instant) {
1754            // Instantly set the scroll position to the bottom
1755            chatBox.scrollTop(chatBox.prop("scrollHeight"));
1756        } else {
1757            // Use requestAnimationFrame for smoother scrolling if needed
1758            let start = null;
1759            const scrollHeight = chatBox.prop("scrollHeight");
1760            const initialScroll = chatBox.scrollTop();
1761            const distance = scrollHeight - initialScroll;
1762            const duration = 500; // Duration in ms
1763
1764            function smoothScroll(timestamp) {
1765                if (!start) start = timestamp;
1766                const progress = timestamp - start;
1767                const currentScroll = initialScroll + (distance * (progress / duration));
1768                chatBox.scrollTop(currentScroll);
1769
1770                if (progress < duration) {
1771                    requestAnimationFrame(smoothScroll);
1772                } else {
1773                    chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
1774                }
1775            }
1776
1777            requestAnimationFrame(smoothScroll);
1778        }
1779    }
1780
1781    function scrollElementToTop(element, botId) {
1782        botId = botId || 'default';
1783        var chatBox = getElement(botId, 'chat-box');
1784        var elementTop = element.position().top + chatBox.scrollTop();
1785        chatBox.animate({ scrollTop: elementTop }, 500);
1786    }
1787
1788    function showChatWidget(botId) {
1789        botId = botId || 'default';
1790        var $button = getElement(botId, 'floating-chatbot-button');
1791        // First ensure display is set
1792        $button.css('display', 'flex');
1793        // Then handle the fade
1794        $button.fadeTo(500, 1);
1795        // Force visibility
1796        $button.removeClass('hidden');
1797    }
1798
1799    function hideChatWidget(botId) {
1800        botId = botId || 'default';
1801        var $button = getElement(botId, 'floating-chatbot-button');
1802        $button.css('display', 'none');
1803        $button.addClass('hidden');
1804    }
1805    
1806    function disableScroll() {
1807        if (isMobile()) {
1808            $('body').css('overflow', 'hidden');
1809        }
1810    }
1811    
1812    function enableScroll() {
1813        if (isMobile()) {
1814            $('body').css('overflow', '');
1815        }
1816    }
1817    
1818    function isMobile() {
1819        // This can be a simple check, or more sophisticated detection of mobile devices
1820        return window.innerWidth <= 768; // Example threshold for mobile devices
1821    }
1822    
1823    function setFullHeight() {
1824        var vh = $(window).innerHeight() * 0.01;
1825        $(':root').css('--vh', vh + 'px');
1826    }
1827
1828
1829    // ====================================
1830    // NOTIFICATION SYSTEM
1831    // ====================================
1832    
1833    function createNotificationBadge() {
1834        const chatButton = document.getElementById('floating-chatbot-button');
1835
1836        if (!chatButton) return;
1837
1838        // Remove any existing badge first
1839        const existingBadge = chatButton.querySelector('.chat-notification-badge');
1840        if (existingBadge) {
1841            existingBadge.remove();
1842        }
1843    
1844        notificationBadge = document.createElement('div');
1845        notificationBadge.className = 'chat-notification-badge';
1846        notificationBadge.style.cssText = `
1847            display: none;
1848            position: absolute;
1849            top: -5px;
1850            right: -5px;
1851            background-color: red;
1852            color: white;
1853            border-radius: 50%;
1854            padding: 4px 8px;
1855            font-size: 12px;
1856            font-weight: bold;
1857            z-index: 10001;
1858        `;
1859        chatButton.style.position = 'relative';
1860        chatButton.appendChild(notificationBadge);
1861        
1862    }
1863    
1864    function showNotification(botId) {
1865        botId = botId || 'default';
1866        const badge = getElementDOM(botId, 'chat-notification-badge');
1867        var $floatingChatbot = getElement(botId, 'floating-chatbot');
1868        if (badge && $floatingChatbot.hasClass('hidden')) {
1869            badge.style.display = 'block';
1870            badge.textContent = '1';
1871        }
1872    }
1873
1874    function hideNotification(botId) {
1875        botId = botId || 'default';
1876        const badge = getElementDOM(botId, 'chat-notification-badge');
1877        if (badge) {
1878            badge.style.display = 'none';
1879        }
1880    }
1881
1882    function startNotificationChecking(botId) {
1883        botId = botId || 'default';
1884        const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1885        if (!chatPersistenceEnabled) return;
1886
1887        createNotificationBadge(botId);
1888        var instance = MxChatInstances.get(botId);
1889        instance.notificationCheckInterval = setInterval(function() {
1890            checkForNewMessages(botId);
1891        }, 30000); // Check every 30 seconds
1892    }
1893
1894    function stopNotificationChecking(botId) {
1895        botId = botId || 'default';
1896        var instance = MxChatInstances.get(botId);
1897        if (instance.notificationCheckInterval) {
1898            clearInterval(instance.notificationCheckInterval);
1899        }
1900    }
1901    
1902    function checkForNewMessages() {
1903        const sessionId = getChatSession();
1904        const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1905        
1906        if (!chatPersistenceEnabled) return;
1907    
1908        $.ajax({
1909            url: mxchatChat.ajax_url,
1910            type: 'POST',
1911            data: {
1912                action: 'mxchat_check_new_messages',
1913                session_id: sessionId,
1914                last_seen_id: lastSeenMessageId,
1915                nonce: mxchatChat.nonce
1916            },
1917            success: function(response) {
1918                if (response.success && response.data.hasNewMessages) {
1919                    showNotification();
1920                }
1921            }
1922        });
1923    }
1924
1925
1926// ====================================
1927// LIVE AGENT FUNCTIONALITY
1928// ====================================
1929
1930function startPolling(botId) {
1931    console.log('[MxChat] startPolling called for botId:', botId);
1932    botId = botId || 'default';
1933    var instance = MxChatInstances.get(botId);
1934    // Clear any existing interval first
1935    stopPolling(botId);
1936    // Start new polling interval
1937    console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
1938    instance.pollingInterval = setInterval(function() {
1939        checkForAgentMessages(botId);
1940    }, 5000);
1941}
1942
1943function stopPolling(botId) {
1944    console.log('[MxChat] stopPolling called for botId:', botId);
1945    botId = botId || 'default';
1946    var instance = MxChatInstances.get(botId);
1947    if (instance.pollingInterval) {
1948        clearInterval(instance.pollingInterval);
1949        instance.pollingInterval = null;
1950        console.log('[MxChat] Polling stopped for botId:', botId);
1951    }
1952}
1953
1954function checkForAgentMessages(botId) {
1955    console.log('[MxChat] checkForAgentMessages called for botId:', botId);
1956    botId = botId || 'default';
1957    var instance = MxChatInstances.get(botId);
1958    const sessionId = getChatSession(botId);
1959    $.ajax({
1960        url: mxchatChat.ajax_url,
1961        type: 'POST',
1962        dataType: 'json',
1963        data: {
1964            action: 'mxchat_fetch_new_messages',
1965            session_id: sessionId,
1966            last_seen_id: instance.lastSeenMessageId,
1967            persistence_enabled: 'true',
1968            nonce: mxchatChat.nonce
1969        },
1970        success: function (response) {
1971            if (response.success && response.data?.new_messages) {
1972                let hasNewMessage = false;
1973
1974                response.data.new_messages.forEach(function (message) {
1975                    if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) {
1976                        hasNewMessage = true;
1977                        appendMessage("agent", message.content, '', [], false, botId);
1978                        instance.lastSeenMessageId = message.id;
1979                        instance.processedMessageIds.add(message.id);
1980                    }
1981                });
1982
1983                var $floatingChatbot = getElement(botId, 'floating-chatbot');
1984                if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
1985                    showNotification(botId);
1986                }
1987
1988                scrollToBottom(botId, true);
1989            }
1990        },
1991        error: function (xhr, status, error) {
1992            // Polling error - silently continue
1993        }
1994    });
1995}
1996
1997    // ====================================
1998    // CHAT HISTORY & PERSISTENCE
1999    // ====================================
2000
2001function loadChatHistory(botId) {
2002    botId = botId || 'default';
2003    var instance = MxChatInstances.get(botId);
2004
2005    // Prevent duplicate loading
2006    if (instance.chatHistoryLoaded) {
2007        return;
2008    }
2009
2010    var sessionId = getChatSession(botId);
2011    var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2012
2013    if (chatPersistenceEnabled && sessionId) {
2014        $.ajax({
2015            url: mxchatChat.ajax_url,
2016            type: 'POST',
2017            dataType: 'json',
2018            data: {
2019                action: 'mxchat_fetch_conversation_history',
2020                session_id: sessionId
2021            },
2022            success: function(response) {
2023                // Handle session reset (IP changed while user was away)
2024                if (response.success === false && response.data && response.data.action === 'reset_session') {
2025                    // Silently reset session - user will start fresh
2026                    resetChatSession(botId);
2027                    instance.chatHistoryLoaded = true; // Prevent retry loop
2028                    return;
2029                }
2030
2031                // Check if the response indicates success
2032                if (response.success) {
2033                    // Handle case where conversation data exists and is an array
2034                    if (response.data && Array.isArray(response.data.conversation)) {
2035                        var $chatBox = getElement(botId, 'chat-box');
2036                        var $fragment = $(document.createDocumentFragment());
2037                        let highestMessageId = instance.lastSeenMessageId;
2038
2039                        // Update chat mode if provided
2040                        if (response.data.chat_mode) {
2041                            updateChatModeIndicator(response.data.chat_mode, botId);
2042                        }
2043
2044                        // Only process if there are actual messages
2045                        if (response.data.conversation.length > 0) {
2046                            // IMPORTANT: Clear existing messages before loading history
2047                            $chatBox.empty();
2048
2049                            $.each(response.data.conversation, function(index, message) {
2050                                // Skip agent messages if persistence is off
2051                                if (!chatPersistenceEnabled && message.role === 'agent') {
2052                                    return;
2053                                }
2054
2055                                var messageClass, messageBgColor, messageFontColor;
2056
2057                                switch (message.role) {
2058                                    case 'user':
2059                                        messageClass = 'user-message';
2060                                        messageBgColor = userMessageBgColor;
2061                                        messageFontColor = userMessageFontColor;
2062                                        break;
2063                                    case 'agent':
2064                                        messageClass = 'agent-message';
2065                                        messageBgColor = liveAgentMessageBgColor;
2066                                        messageFontColor = liveAgentMessageFontColor;
2067                                        break;
2068                                    default:
2069                                        messageClass = 'bot-message';
2070                                        messageBgColor = botMessageBgColor;
2071                                        messageFontColor = botMessageFontColor;
2072                                        break;
2073                                }
2074
2075                                var messageElement = $('<div>').addClass(messageClass)
2076                                    .css({
2077                                        'background': messageBgColor,
2078                                        'color': messageFontColor
2079                                    });
2080
2081                                var content = message.content;
2082                                content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2083                                content = decodeHTMLEntities(content);
2084
2085                                if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2086                                    messageElement.html(content);
2087                                } else {
2088                                    var formattedContent = linkify(content);
2089                                    messageElement.html(formattedContent);
2090                                }
2091
2092                                $fragment.append(messageElement);
2093
2094                                // Track message IDs
2095                                if (message.id) {
2096                                    highestMessageId = Math.max(highestMessageId, message.id);
2097                                    instance.processedMessageIds.add(message.id);
2098                                }
2099                            });
2100
2101                            // Only append messages and scroll if we have content
2102                            $chatBox.append($fragment);
2103                            scrollToBottom(botId, true);
2104
2105                            // Collapse quick questions if we have conversation history
2106                            // BUT skip auto-collapse for embedded bots (they should stay expanded)
2107                            if (hasQuickQuestions(botId) && !isEmbeddedBot(botId)) {
2108                                collapseQuickQuestions(botId);
2109                            }
2110
2111                            // Update lastSeenMessageId after history loads
2112                            instance.lastSeenMessageId = highestMessageId;
2113
2114                            // Only update chat mode if persistence is enabled and we have messages
2115                            if (chatPersistenceEnabled) {
2116                                var lastMessage = response.data.conversation[response.data.conversation.length - 1];
2117                                if (lastMessage.role === 'agent') {
2118                                    updateChatModeIndicator('agent', botId);
2119                                }
2120                            }
2121
2122                            // Mark as loaded ONLY after successful load
2123                            instance.chatHistoryLoaded = true;
2124                        }
2125                    }
2126                }
2127            },
2128            error: function(xhr, status, error) {
2129                // Error loading chat history - silently continue
2130            }
2131        });
2132    }
2133}
2134
2135
2136    // ====================================
2137    // FILE UPLOAD FUNCTIONALITY
2138    // ====================================
2139    
2140    function addSafeEventListener(elementId, eventType, handler) {
2141        const element = document.getElementById(elementId);
2142        if (element) {
2143            element.addEventListener(eventType, handler);
2144        }
2145    }
2146    
2147    function showActivePdf(filename, botId) {
2148        botId = botId || 'default';
2149        const container = getElementDOM(botId, 'active-pdf-container');
2150        const nameElement = getElementDOM(botId, 'active-pdf-name');
2151
2152        if (!container || !nameElement) {
2153            return;
2154        }
2155
2156        nameElement.textContent = filename;
2157        container.style.display = 'flex';
2158    }
2159
2160    function showActiveWord(filename, botId) {
2161        botId = botId || 'default';
2162        const container = getElementDOM(botId, 'active-word-container');
2163        const nameElement = getElementDOM(botId, 'active-word-name');
2164
2165        if (!container || !nameElement) {
2166            return;
2167        }
2168
2169        nameElement.textContent = filename;
2170        container.style.display = 'flex';
2171    }
2172
2173    function removeActivePdf(botId) {
2174        botId = botId || 'default';
2175        var instance = MxChatInstances.get(botId);
2176        const container = getElementDOM(botId, 'active-pdf-container');
2177        const nameElement = getElementDOM(botId, 'active-pdf-name');
2178
2179        if (!container || !nameElement || !instance.activePdfFile) return;
2180
2181        fetch(mxchatChat.ajax_url, {
2182            method: 'POST',
2183            headers: {
2184                'Content-Type': 'application/x-www-form-urlencoded',
2185            },
2186            body: new URLSearchParams({
2187                'action': 'mxchat_remove_pdf',
2188                'session_id': getChatSession(botId),
2189                'nonce': mxchatChat.nonce
2190            })
2191        })
2192        .then(response => response.json())
2193        .then(data => {
2194            if (data.success) {
2195                container.style.display = 'none';
2196                nameElement.textContent = '';
2197                activePdfFile = null;
2198                appendMessage('bot', 'PDF removed.');
2199            }
2200        })
2201        .catch(error => {
2202            // Error removing PDF - silently continue
2203        });
2204    }
2205
2206    function removeActiveWord() {
2207        const container = document.getElementById('active-word-container');
2208        const nameElement = document.getElementById('active-word-name');
2209        
2210        if (!container || !nameElement || !activeWordFile) return;
2211    
2212        fetch(mxchatChat.ajax_url, {
2213            method: 'POST',
2214            headers: {
2215                'Content-Type': 'application/x-www-form-urlencoded',
2216            },
2217            body: new URLSearchParams({
2218                'action': 'mxchat_remove_word',
2219                'session_id': sessionId,
2220                'nonce': mxchatChat.nonce
2221            })
2222        })
2223        .then(response => response.json())
2224        .then(data => {
2225            if (data.success) {
2226                container.style.display = 'none';
2227                nameElement.textContent = '';
2228                activeWordFile = null;
2229                appendMessage('bot', 'Word document removed.');
2230            }
2231        })
2232        .catch(error => {
2233            // Error removing Word document - silently continue
2234        });
2235    }
2236
2237    // ====================================
2238    // CONSENT & COMPLIANCE (GDPR)
2239    // ====================================
2240
2241    function initializeChatVisibility(botId) {
2242        botId = botId || 'default';
2243        const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
2244                                mxchatChat.complianz_toggle === '1' ||
2245                                mxchatChat.complianz_toggle === 1;
2246
2247        if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
2248            // Initial check
2249            checkConsentAndShowChat(botId);
2250
2251            // Listen for consent changes
2252            $(document).on('cmplz_status_change', function(event) {
2253                checkConsentAndShowChat(botId);
2254            });
2255        } else {
2256            // If Complianz is not enabled, always show
2257            getElement(botId, 'floating-chatbot-button')
2258                .css('display', 'flex')
2259                .removeClass('hidden no-consent')
2260                .fadeTo(500, 1);
2261
2262            // Also check pre-chat message when Complianz is not enabled
2263            checkPreChatDismissal(botId);
2264        }
2265    }
2266
2267
2268    function checkConsentAndShowChat(botId) {
2269        botId = botId || 'default';
2270        var consentStatus = cmplz_has_consent('marketing');
2271        var consentType = complianz.consenttype;
2272
2273        let $widget = getElement(botId, 'floating-chatbot-button');
2274        let $chatbot = getElement(botId, 'floating-chatbot');
2275        let $preChat = getElement(botId, 'pre-chat-message');
2276
2277        if (consentStatus === true) {
2278            $widget
2279                .removeClass('no-consent')
2280                .css('display', 'flex')
2281                .removeClass('hidden')
2282                .fadeTo(500, 1);
2283            $chatbot.removeClass('no-consent');
2284
2285            // Show pre-chat message if not dismissed
2286            checkPreChatDismissal(botId);
2287        } else {
2288            $widget
2289                .addClass('no-consent')
2290                .fadeTo(500, 0, function() {
2291                    $(this)
2292                        .css('display', 'none')
2293                        .addClass('hidden');
2294                });
2295            $chatbot.addClass('no-consent');
2296
2297            // Hide pre-chat message when no consent
2298            $preChat.hide();
2299        }
2300    }
2301
2302
2303    // ====================================
2304    // PRE-CHAT MESSAGE HANDLING
2305    // ====================================
2306
2307    function checkPreChatDismissal(botId) {
2308        botId = botId || 'default';
2309        $.ajax({
2310            url: mxchatChat.ajax_url,
2311            type: 'POST',
2312            data: {
2313                action: 'mxchat_check_pre_chat_message_status',
2314                _ajax_nonce: mxchatChat.nonce
2315            },
2316            success: function(response) {
2317                if (response.success && !response.data.dismissed) {
2318                    getElement(botId, 'pre-chat-message').fadeIn(250);
2319                } else {
2320                    getElement(botId, 'pre-chat-message').hide();
2321                }
2322            },
2323            error: function() {
2324                // Error checking pre-chat dismissal - silently continue
2325            }
2326        });
2327    }
2328
2329    function handlePreChatDismissal(botId) {
2330        botId = botId || 'default';
2331        getElement(botId, 'pre-chat-message').fadeOut(200);
2332        $.ajax({
2333            url: mxchatChat.ajax_url,
2334            type: 'POST',
2335            data: {
2336                action: 'mxchat_dismiss_pre_chat_message',
2337                _ajax_nonce: mxchatChat.nonce
2338            },
2339            success: function() {
2340                $('#pre-chat-message').hide();
2341            },
2342            error: function() {
2343                // Error dismissing pre-chat message - silently continue
2344            }
2345        });
2346    }
2347
2348
2349    // ====================================
2350    // UTILITY FUNCTIONS
2351    // ====================================
2352    
2353    function copyToClipboard(text) {
2354        var tempInput = $('<input>');
2355        $('body').append(tempInput);
2356        tempInput.val(text).select();
2357        document.execCommand('copy');
2358        tempInput.remove();
2359    }
2360
2361    
2362    function isImageHtml(str) {
2363        return str.startsWith('<img') && str.endsWith('>');
2364    }
2365
2366
2367    // ====================================
2368    // EVENT HANDLERS & INITIALIZATION
2369    // ====================================
2370
2371$(document).on('click', '.mxchat-popular-question', function () {
2372    var question = $(this).text();
2373    var botId = getBotIdFromElement(this);
2374
2375    // Append the question as if the user typed it
2376    appendMessage("user", question, '', [], false, botId);
2377
2378    // Only collapse if there are questions
2379    if (hasQuickQuestions(botId)) {
2380        collapseQuickQuestions(botId);
2381    }
2382
2383    // Send the question to the server
2384    sendMessageToChatbot(question, botId);
2385});
2386
2387$(document).on('click', '.questions-toggle-btn', function(e) {
2388    e.preventDefault();
2389    e.stopPropagation();
2390    var botId = getBotIdFromElement(this);
2391    expandQuickQuestions(botId);
2392});
2393
2394$(document).on('click', '.questions-collapse-btn', function(e) {
2395    e.preventDefault();
2396    e.stopPropagation();
2397    var botId = getBotIdFromElement(this);
2398    collapseQuickQuestions(botId);
2399});
2400    
2401    // Chatbot visibility toggle handlers - use class selector for multi-instance support
2402    $(document).on('click', '.floating-chatbot-button', function() {
2403        var botId = getBotIdFromElement(this);
2404        var $chatbot = getElement(botId, 'floating-chatbot');
2405        var $badge = getElement(botId, 'chat-notification-badge');
2406        var $preChat = getElement(botId, 'pre-chat-message');
2407
2408        if ($chatbot.hasClass('hidden')) {
2409            $chatbot.removeClass('hidden').addClass('visible');
2410            $(this).addClass('hidden');
2411            $badge.hide(); // Hide notification when opening chat
2412            disableScroll();
2413            $preChat.fadeOut(250);
2414        } else {
2415            $chatbot.removeClass('visible').addClass('hidden');
2416            $(this).removeClass('hidden');
2417            enableScroll();
2418            checkPreChatDismissal(botId);
2419        }
2420    });
2421
2422    // Allow clicking anywhere on the title bar to close the chatbot
2423    $(document).on('click', '.chatbot-top-bar', function() {
2424        var botId = getBotIdFromElement(this);
2425        getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2426        getElement(botId, 'floating-chatbot-button').removeClass('hidden');
2427        enableScroll();
2428    });
2429
2430    $(document).on('click', '.close-pre-chat-message', function(e) {
2431        e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2432        var botId = getBotIdFromElement(this);
2433        getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2434            $(this).remove();
2435        });
2436    });
2437
2438
2439    // PDF upload button handlers - use class selector
2440    $(document).on('click', '.pdf-upload-btn', function() {
2441        var botId = getBotIdFromElement(this);
2442        var pdfInput = getElementDOM(botId, 'pdf-upload');
2443        if (pdfInput) pdfInput.click();
2444    });
2445
2446    // Word upload button handlers - use class selector
2447    $(document).on('click', '.word-upload-btn', function() {
2448        var botId = getBotIdFromElement(this);
2449        var wordInput = getElementDOM(botId, 'word-upload');
2450        if (wordInput) wordInput.click();
2451    });
2452    
2453    // PDF file input change handler
2454    addSafeEventListener('pdf-upload', 'change', async function(e) {
2455        const file = e.target.files[0];
2456    
2457        if (!file || file.type !== 'application/pdf') {
2458            alert('Please select a valid PDF file.');
2459            return;
2460        }
2461    
2462        if (!sessionId) {
2463            alert('Error: No session ID found');
2464            return;
2465        }
2466
2467        if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
2468            alert('Error: Ajax configuration missing');
2469            return;
2470        }
2471    
2472        // Disable buttons and show loading state
2473        const uploadBtn = document.getElementById('pdf-upload-btn');
2474        const sendBtn = document.getElementById('send-button');
2475        const originalBtnContent = uploadBtn.innerHTML;
2476    
2477        try {
2478            const formData = new FormData();
2479            formData.append('action', 'mxchat_upload_pdf');
2480            formData.append('pdf_file', file);
2481            formData.append('session_id', sessionId);
2482            formData.append('nonce', mxchatChat.nonce);
2483    
2484            uploadBtn.disabled = true;
2485            sendBtn.disabled = true;
2486            uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2487                <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2488            </svg>`;
2489    
2490            const response = await fetch(mxchatChat.ajax_url, {
2491                method: 'POST',
2492                body: formData
2493            });
2494    
2495            const data = await response.json();
2496    
2497            if (data.success) {
2498                // Hide popular questions if they exist
2499                const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2500                if (hasQuickQuestions()) {
2501                    collapseQuickQuestions();
2502                }
2503                
2504                // Show the active PDF name
2505                showActivePdf(data.data.filename);
2506                
2507                appendMessage('bot', data.data.message);
2508                scrollToBottom();
2509                activePdfFile = data.data.filename;
2510            } else {
2511                alert('Failed to upload PDF. Please try again.');
2512            }
2513        } catch (error) {
2514            alert('Error uploading file. Please try again.');
2515        } finally {
2516            uploadBtn.disabled = false;
2517            sendBtn.disabled = false;
2518            uploadBtn.innerHTML = originalBtnContent;
2519            this.value = ''; // Reset file input
2520        }
2521    });
2522    
2523    // Word file input change handler
2524    addSafeEventListener('word-upload', 'change', async function(e) {
2525        const file = e.target.files[0];
2526    
2527        if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
2528            alert('Please select a valid Word document (.docx).');
2529            return;
2530        }
2531    
2532        if (!sessionId) {
2533            alert('Error: No session ID found');
2534            return;
2535        }
2536
2537        // Disable buttons and show loading state
2538        const uploadBtn = document.getElementById('word-upload-btn');
2539        const sendBtn = document.getElementById('send-button');
2540        const originalBtnContent = uploadBtn.innerHTML;
2541    
2542        try {
2543            const formData = new FormData();
2544            formData.append('action', 'mxchat_upload_word');
2545            formData.append('word_file', file);
2546            formData.append('session_id', sessionId);
2547            formData.append('nonce', mxchatChat.nonce);
2548    
2549            uploadBtn.disabled = true;
2550            sendBtn.disabled = true;
2551            uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2552                <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2553            </svg>`;
2554    
2555            const response = await fetch(mxchatChat.ajax_url, {
2556                method: 'POST',
2557                body: formData
2558            });
2559    
2560            const data = await response.json();
2561    
2562            if (data.success) {
2563                // Hide popular questions if they exist
2564                const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2565                if (hasQuickQuestions()) {
2566                    collapseQuickQuestions();
2567                }
2568    
2569                // Show the active Word document name
2570                showActiveWord(data.data.filename);
2571                
2572                appendMessage('bot', data.data.message);
2573                scrollToBottom();
2574                activeWordFile = data.data.filename;
2575            } else {
2576                alert('Failed to upload Word document. Please try again.');
2577            }
2578        } catch (error) {
2579            alert('Error uploading file. Please try again.');
2580        } finally {
2581            uploadBtn.disabled = false;
2582            sendBtn.disabled = false;
2583            uploadBtn.innerHTML = originalBtnContent;
2584            this.value = ''; // Reset file input
2585        }
2586    });
2587    
2588    // Remove button click handlers
2589    document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
2590        e.preventDefault();
2591        e.stopPropagation();
2592        removeActivePdf();
2593    });
2594    
2595    document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
2596        e.preventDefault();
2597        e.stopPropagation();
2598        removeActiveWord();
2599    });
2600    
2601    // Window resize handlers
2602    $(window).on('resize orientationchange', function() {
2603        setFullHeight();
2604    });
2605
2606
2607    // ====================================
2608    // TOOLBAR & STYLING SETUP
2609    // ====================================
2610    
2611    // Apply toolbar settings
2612    if (mxchatChat.chat_toolbar_toggle === 'on') {
2613        $('.chat-toolbar').show();
2614    } else {
2615        $('.chat-toolbar').hide();
2616    }
2617    
2618    // Apply toolbar icon colors
2619    const toolbarElements = [
2620        '#mxchat-chatbot .toolbar-btn svg',
2621        '#mxchat-chatbot .active-pdf-name',
2622        '#mxchat-chatbot .active-word-name',
2623        '#mxchat-chatbot .remove-pdf-btn svg',
2624        '#mxchat-chatbot .remove-word-btn svg',
2625        '#mxchat-chatbot .toolbar-perplexity svg'
2626    ];
2627    
2628    toolbarElements.forEach(selector => {
2629        $(selector).css({
2630            'fill': toolbarIconColor,
2631            'stroke': toolbarIconColor,
2632            'color': toolbarIconColor
2633        });
2634    });
2635
2636
2637// ====================================
2638//  EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2639// ====================================
2640// Only run email collection setup if it's enabled
2641if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2642
2643    // Track submitting state per bot
2644    const emailSubmittingState = {};
2645
2646    // Add CSS animations for email form (once globally)
2647    if (!document.getElementById('email-error-styles')) {
2648        const style = document.createElement('style');
2649        style.id = 'email-error-styles';
2650        style.textContent = `
2651            @keyframes fadeInError {
2652                from { opacity: 0; transform: translateY(-5px); }
2653                to { opacity: 1; transform: translateY(0); }
2654            }
2655            .email-input-shake {
2656                animation: shake 0.5s ease-in-out;
2657            }
2658            @keyframes shake {
2659                0%, 100% { transform: translateX(0); }
2660                25% { transform: translateX(-5px); }
2661                75% { transform: translateX(5px); }
2662            }
2663            @keyframes spin {
2664                from { transform: rotate(0deg); }
2665                to { transform: rotate(360deg); }
2666            }
2667            .email-spinner {
2668                display: inline-block;
2669                vertical-align: middle;
2670            }
2671        `;
2672        document.head.appendChild(style);
2673    }
2674
2675    // Helper functions for email collection (multi-instance aware)
2676    function showEmailFormForBot(botId) {
2677        var emailBlocker = getElementDOM(botId, 'email-blocker');
2678        var chatContainer = getElementDOM(botId, 'chat-container');
2679        if (emailBlocker) emailBlocker.style.display = 'flex';
2680        if (chatContainer) chatContainer.style.display = 'none';
2681    }
2682
2683    function showChatContainerForBot(botId) {
2684        var emailBlocker = getElementDOM(botId, 'email-blocker');
2685        var chatContainer = getElementDOM(botId, 'chat-container');
2686        if (emailBlocker) emailBlocker.style.display = 'none';
2687        if (chatContainer) chatContainer.style.display = 'flex';
2688
2689        // Load chat history for this bot
2690        if (typeof loadChatHistory === 'function') {
2691            loadChatHistory(botId);
2692        }
2693    }
2694
2695    function isValidEmailAddress(email) {
2696        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2697        return emailRegex.test(email.trim()) && email.length <= 254;
2698    }
2699
2700    function isValidNameInput(name) {
2701        return name && name.trim().length >= 2 && name.trim().length <= 100;
2702    }
2703
2704    function setEmailSubmissionState(botId, loading) {
2705        var submitButton = getElementDOM(botId, 'email-submit-button');
2706        var emailInput = getElementDOM(botId, 'user-email');
2707        var nameInput = getElementDOM(botId, 'user-name');
2708
2709        if (loading) {
2710            emailSubmittingState[botId] = true;
2711            if (submitButton) submitButton.disabled = true;
2712            if (emailInput) emailInput.disabled = true;
2713            if (nameInput) nameInput.disabled = true;
2714
2715            if (submitButton && !submitButton.getAttribute('data-original-html')) {
2716                submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2717                const originalText = submitButton.textContent;
2718                submitButton.innerHTML = `
2719                    <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2720                        <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2721                            <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;
27210 31.416" repeatCount="indefinite"/>
2722                            <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2723                        </circle>
2724                    </svg>
2725                    ${originalText}
2726                `;
2727                submitButton.style.opacity = '0.8';
2728            }
2729        } else {
2730            emailSubmittingState[botId] = false;
2731            if (submitButton) submitButton.disabled = false;
2732            if (emailInput) emailInput.disabled = false;
2733            if (nameInput) nameInput.disabled = false;
2734
2735            if (submitButton) {
2736                const originalHtml = submitButton.getAttribute('data-original-html');
2737                if (originalHtml) {
2738                    submitButton.innerHTML = originalHtml;
2739                }
2740                submitButton.style.opacity = '1';
2741            }
2742        }
2743    }
2744
2745    function showEmailError(botId, message) {
2746        clearEmailError(botId);
2747
2748        var emailForm = getElementDOM(botId, 'email-collection-form');
2749        if (!emailForm) return;
2750
2751        const errorDiv = document.createElement('div');
2752        errorDiv.className = 'email-error';
2753        errorDiv.style.cssText = `
2754            color: #e74c3c;
2755            font-size: 12px;
2756            margin-top: 8px;
2757            padding: 4px 0;
2758            animation: fadeInError 0.3s ease;
2759        `;
2760        errorDiv.textContent = message;
2761        emailForm.appendChild(errorDiv);
2762
2763        // Add shake animation to inputs
2764        var emailInput = getElementDOM(botId, 'user-email');
2765        var nameInput = getElementDOM(botId, 'user-name');
2766
2767        if (emailInput) {
2768            emailInput.classList.add('email-input-shake');
2769            setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
2770        }
2771        if (nameInput) {
2772            nameInput.classList.add('email-input-shake');
2773            setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
2774        }
2775    }
2776
2777    function clearEmailError(botId) {
2778        var emailForm = getElementDOM(botId, 'email-collection-form');
2779        if (emailForm) {
2780            const existingErrors = emailForm.querySelectorAll('.email-error');
2781            existingErrors.forEach(error => error.remove());
2782        }
2783    }
2784
2785    function checkSessionAndEmailForBot(botId) {
2786        const sessionId = getChatSession(botId);
2787
2788        fetch(mxchatChat.ajax_url, {
2789            method: 'POST',
2790            headers: {
2791                'Content-Type': 'application/x-www-form-urlencoded',
2792            },
2793            body: new URLSearchParams({
2794                action: 'mxchat_check_email_provided',
2795                session_id: sessionId,
2796                nonce: mxchatChat.nonce,
2797            })
2798        })
2799        .then((response) => {
2800            if (!response.ok) {
2801                throw new Error(`HTTP error! status: ${response.status}`);
2802            }
2803            return response.json();
2804        })
2805        .then((data) => {
2806            if (data.success) {
2807                if (data.data.logged_in || data.data.email) {
2808                    showChatContainerForBot(botId);
2809                } else {
2810                    showEmailFormForBot(botId);
2811                }
2812            } else {
2813                showEmailFormForBot(botId);
2814            }
2815        })
2816        .catch((error) => {
2817            showEmailFormForBot(botId);
2818        });
2819    }
2820
2821    // Event delegation for email form submission
2822    $(document).on('submit', '.email-collection-form', function(e) {
2823        e.preventDefault();
2824        e.stopPropagation();
2825
2826        var botId = getBotIdFromElement(this);
2827
2828        // Prevent double submission
2829        if (emailSubmittingState[botId]) {
2830            return false;
2831        }
2832
2833        var emailInput = getElementDOM(botId, 'user-email');
2834        var nameInput = getElementDOM(botId, 'user-name');
2835        var userEmail = emailInput ? emailInput.value.trim() : '';
2836        var userName = nameInput ? nameInput.value.trim() : '';
2837        var sessionId = getChatSession(botId);
2838
2839        // Validate email
2840        if (!userEmail) {
2841            showEmailError(botId, 'Please enter your email address.');
2842            return false;
2843        }
2844
2845        if (!isValidEmailAddress(userEmail)) {
2846            showEmailError(botId, 'Please enter a valid email address.');
2847            return false;
2848        }
2849
2850        // Validate name if field exists and has content
2851        if (nameInput && userName && !isValidNameInput(userName)) {
2852            showEmailError(botId, 'Please enter a valid name (2-100 characters).');
2853            return false;
2854        }
2855
2856        clearEmailError(botId);
2857        setEmailSubmissionState(botId, true);
2858
2859        // Prepare form data
2860        const formData = new URLSearchParams({
2861            action: 'mxchat_handle_save_email_and_response',
2862            email: userEmail,
2863            session_id: sessionId,
2864            nonce: mxchatChat.nonce,
2865        });
2866
2867        if (userName) {
2868            formData.append('name', userName);
2869        }
2870
2871        fetch(mxchatChat.ajax_url, {
2872            method: 'POST',
2873            headers: {
2874                'Content-Type': 'application/x-www-form-urlencoded',
2875            },
2876            body: formData
2877        })
2878        .then((response) => {
2879            if (!response.ok) {
2880                throw new Error(`HTTP error! status: ${response.status}`);
2881            }
2882            return response.json();
2883        })
2884        .then((data) => {
2885            setEmailSubmissionState(botId, false);
2886
2887            if (data.success) {
2888                showChatContainerForBot(botId);
2889
2890                if (data.message && typeof appendMessage === 'function') {
2891                    setTimeout(() => {
2892                        appendMessage('bot', data.message, '', [], false, botId);
2893                        if (typeof scrollToBottom === 'function') {
2894                            scrollToBottom(botId);
2895                        }
2896                    }, 100);
2897                }
2898            } else {
2899                showEmailError(botId, data.message || 'Failed to save email. Please try again.');
2900            }
2901        })
2902        .catch((error) => {
2903            setEmailSubmissionState(botId, false);
2904            showEmailError(botId, 'An error occurred. Please try again.');
2905        });
2906
2907        return false;
2908    });
2909
2910    // Real-time email validation using event delegation
2911    $(document).on('input', '.mxchat-email-input', function() {
2912        var botId = getBotIdFromElement(this);
2913        var $input = $(this);
2914
2915        // Clear previous timeout
2916        clearTimeout($input.data('validationTimeout'));
2917
2918        // Debounce validation
2919        var timeout = setTimeout(() => {
2920            var email = this.value.trim();
2921            clearEmailError(botId);
2922
2923            if (email && !isValidEmailAddress(email)) {
2924                showEmailError(botId, 'Please enter a valid email address.');
2925            }
2926        }, 500);
2927
2928        $input.data('validationTimeout', timeout);
2929    });
2930
2931    // Handle Enter key in email input
2932    $(document).on('keypress', '.mxchat-email-input', function(e) {
2933        if (e.key === 'Enter') {
2934            e.preventDefault();
2935            var botId = getBotIdFromElement(this);
2936            if (!emailSubmittingState[botId]) {
2937                $(this).closest('.email-collection-form').submit();
2938            }
2939        }
2940    });
2941
2942    // Handle Enter key in name input
2943    $(document).on('keypress', '.mxchat-name-input', function(e) {
2944        if (e.key === 'Enter') {
2945            e.preventDefault();
2946            var botId = getBotIdFromElement(this);
2947            if (!emailSubmittingState[botId]) {
2948                $(this).closest('.email-collection-form').submit();
2949            }
2950        }
2951    });
2952
2953    // Initialize email check for all bot instances
2954    $('.mxchat-chatbot-wrapper').each(function() {
2955        var botId = $(this).data('bot-id') || 'default';
2956        var emailBlocker = getElementDOM(botId, 'email-blocker');
2957
2958        // Only check if email blocker exists for this bot
2959        if (emailBlocker) {
2960            if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2961                if (mxchatChat.initial_email_state.show_email_form) {
2962                    showEmailFormForBot(botId);
2963                } else {
2964                    showChatContainerForBot(botId);
2965                }
2966            } else {
2967                setTimeout(function() {
2968                    checkSessionAndEmailForBot(botId);
2969                }, 100);
2970            }
2971        }
2972    });
2973}
2974
2975    // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
2976    $(document).on('click', '.pre-chat-message', function() {
2977        var botId = getBotIdFromElement(this);
2978        var $chatbot = getElement(botId, 'floating-chatbot');
2979        if ($chatbot.hasClass('hidden')) {
2980            $chatbot.removeClass('hidden').addClass('visible');
2981            getElement(botId, 'floating-chatbot-button').addClass('hidden');
2982            $(this).fadeOut(250); // Hide pre-chat message
2983            disableScroll(); // Disable scroll when chatbot opens
2984        }
2985    });
2986
2987    // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
2988    // This is a fallback for legacy support
2989    $(document).on('click', '.close-pre-chat-message', function() {
2990        var botId = getBotIdFromElement(this);
2991        var $preChat = getElement(botId, 'pre-chat-message');
2992        $preChat.fadeOut(200); // Hide the message
2993
2994        // Send an AJAX request to set the transient flag for 24 hours
2995        $.ajax({
2996            url: mxchatChat.ajax_url,
2997            type: 'POST',
2998            data: {
2999                action: 'mxchat_dismiss_pre_chat_message',
3000                _ajax_nonce: mxchatChat.nonce
3001            },
3002            success: function() {
3003                // Ensure the message is hidden after dismissal
3004                $preChat.hide();
3005            },
3006            error: function() {
3007                // Error dismissing pre-chat message - silently continue
3008            }
3009        });
3010    });
3011
3012
3013function hasQuickQuestions(botId) {
3014    botId = botId || 'default';
3015    var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3016    if (!questionsContainer) return false;
3017    const questionButtons = questionsContainer.querySelectorAll('.mxchat-popular-question');
3018    return questionButtons.length > 0;
3019}
3020
3021/**
3022 * Check if a bot is embedded (not floating)
3023 * Embedded bots don't have a .floating-chatbot wrapper
3024 */
3025function isEmbeddedBot(botId) {
3026    botId = botId || 'default';
3027    var floatingWrapper = document.getElementById('floating-chatbot-' + botId);
3028    return !floatingWrapper;
3029}
3030
3031function collapseQuickQuestions(botId) {
3032    botId = botId || 'default';
3033    const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3034    if (questionsContainer && hasQuickQuestions(botId)) {
3035        questionsContainer.classList.add('collapsed');
3036        questionsContainer.classList.add('has-been-collapsed');
3037        try {
3038            sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'true');
3039            sessionStorage.setItem('mxchat_questions_has_been_collapsed_' + botId, 'true');
3040        } catch (e) {
3041            // Ignore if sessionStorage is not available
3042        }
3043    }
3044}
3045
3046function expandQuickQuestions(botId) {
3047    botId = botId || 'default';
3048    const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3049    if (questionsContainer && hasQuickQuestions(botId)) {
3050        questionsContainer.classList.remove('collapsed');
3051        try {
3052            sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'false');
3053        } catch (e) {
3054            // Ignore if sessionStorage is not available
3055        }
3056    }
3057}
3058
3059function checkQuickQuestionsState(botId) {
3060    botId = botId || 'default';
3061    if (!hasQuickQuestions(botId)) {
3062        return; // Don't do anything if no questions exist
3063    }
3064
3065    // Skip restoring collapsed state for embedded bots - they should always start expanded
3066    if (isEmbeddedBot(botId)) {
3067        return;
3068    }
3069
3070    try {
3071        const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed_' + botId);
3072        const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed_' + botId);
3073
3074        const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3075        if (questionsContainer) {
3076            if (hasBeenCollapsed === 'true') {
3077                questionsContainer.classList.add('has-been-collapsed');
3078            }
3079            if (isCollapsed === 'true') {
3080                questionsContainer.classList.add('collapsed');
3081            }
3082        }
3083    } catch (e) {
3084        // Ignore if sessionStorage is not available
3085    }
3086}
3087
3088//   Global delegation for dynamically added links as fallback
3089// Use class selector for multi-instance support
3090$(document).on('click', '.chat-box a[href]:not([data-tracked])', function(e) {
3091    const $link = $(this);
3092    const messageDiv = $link.closest('.bot-message, .agent-message');
3093
3094    // Only process bot/agent message links
3095    if (messageDiv.length > 0) {
3096        const originalHref = $link.attr('href');
3097
3098        if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
3099            e.preventDefault();
3100            e.stopPropagation();
3101
3102            // Mark as tracked
3103            $link.attr('data-tracked', 'true');
3104
3105            // Get bot ID from the chat box context
3106            var botId = getBotIdFromElement(this);
3107
3108            // Get message context from the message div
3109            const messageText = messageDiv.text().substring(0, 200);
3110
3111            $.ajax({
3112                url: mxchatChat.ajax_url,
3113                type: 'POST',
3114                data: {
3115                    action: 'mxchat_track_url_click',
3116                    session_id: getChatSession(botId),
3117                    url: originalHref,
3118                    message_context: messageText,
3119                    nonce: mxchatChat.nonce
3120                },
3121                complete: function() {
3122                    if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
3123                        window.open(originalHref, '_blank');
3124                    } else {
3125                        window.location.href = originalHref;
3126                    }
3127                }
3128            });
3129
3130            return false;
3131        }
3132    }
3133});
3134
3135    // ====================================
3136    // MAIN INITIALIZATION
3137    // ====================================
3138
3139    // Initialize all chatbot instances on the page
3140    initializeAllInstances();
3141
3142    // Legacy initialization for single bot compatibility
3143    $('.floating-chatbot.hidden').each(function() {
3144        var botId = getBotIdFromElement(this);
3145        getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3146    });
3147
3148    // Initialize when document is ready
3149    setFullHeight();
3150    trackOriginatingPage();
3151
3152    // Only load chat history if email collection is disabled
3153    if (mxchatChat.email_collection_enabled !== 'on') {
3154        // Load history for all instances
3155        $('.mxchat-chatbot-wrapper').each(function() {
3156            var botId = $(this).data('bot-id') || 'default';
3157            loadChatHistory(botId);
3158        });
3159    }
3160
3161    // Initialize chat visibility for all instances
3162    $('.mxchat-chatbot-wrapper').each(function() {
3163        var botId = $(this).data('bot-id') || 'default';
3164        initializeChatVisibility(botId);
3165    });
3166
3167    // Make functions globally available for add-ons
3168    window.hasQuickQuestions = hasQuickQuestions;
3169    window.collapseQuickQuestions = collapseQuickQuestions;
3170    window.appendMessage = appendMessage;
3171    window.appendThinkingMessage = appendThinkingMessage;
3172    window.scrollToBottom = scrollToBottom;
3173    window.scrollElementToTop = scrollElementToTop;
3174    window.replaceLastMessage = replaceLastMessage;
3175    window.callMxChat = callMxChat;
3176    window.callMxChatStream = callMxChatStream;
3177    window.shouldUseStreaming = shouldUseStreaming;
3178    window.getChatSession = getChatSession;
3179    window.getPageContext = getPageContext;
3180    window.updateStreamingMessage = updateStreamingMessage;
3181    window.MxChatInstances = MxChatInstances;
3182    window.getElement = getElement;
3183    window.getElementDOM = getElementDOM;
3184    window.getBotIdFromElement = getBotIdFromElement;
3185
3186}); // End of jQuery ready
3187
3188
3189// ====================================
3190// GLOBAL EVENT LISTENERS (Outside jQuery)
3191// ====================================
3192
3193// Event listener for copy button (code blocks)
3194document.addEventListener("click", (e) => {
3195    if (e.target.classList.contains("mxchat-copy-button")) {
3196        const copyButton = e.target;
3197        const codeBlock = copyButton
3198            .closest(".mxchat-code-block-container")
3199            .querySelector(".mxchat-code-block code");
3200
3201        if (codeBlock) {
3202            // Preserve formatting using innerText
3203            navigator.clipboard.writeText(codeBlock.innerText).then(() => {
3204                copyButton.textContent = "Copied!";
3205                copyButton.setAttribute("aria-label", "Copied to clipboard");
3206
3207                setTimeout(() => {
3208                    copyButton.textContent = "Copy";
3209                    copyButton.setAttribute("aria-label", "Copy to clipboard");
3210                }, 2000);
3211            });
3212        }
3213    }
3214});
3215

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.