PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.5
MxChat – AI Chatbot & Content Generation for WordPress v3.0.5
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/chat-script.js +210 -816 3.2.53.0.5 View file →
@@ -1,23 +1,6 @@
1 1 jQuery(document).ready(function($) {
2 2
3 - // Nonce refresh is deferred until first user interaction (ensureSession)
4 - // to avoid admin-ajax calls on passive page loads.
5 - var nonceRefreshed = false;
6 - function refreshNonceIfNeeded(callback) {
7 - if (nonceRefreshed || typeof mxchatChat === 'undefined' || !mxchatChat.ajax_url) {
8 - if (callback) callback();
9 - return;
10 - }
11 - nonceRefreshed = true;
12 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' }, function(res) {
13 - if (res && res.success && res.data && res.data.nonce) {
14 - mxchatChat.nonce = res.data.nonce;
15 - }
16 - if (callback) callback();
17 - });
18 - }
19 -
20 3 // ====================================
21 4 // MULTI-INSTANCE MANAGEMENT SYSTEM
22 5 // ====================================
23 6
@@ -27,15 +10,11 @@
27 10
28 11 // Initialize an instance for a bot
29 12 init: function(botId) {
30 13 if (!this.instances[botId]) {
31 - // When persistence is OFF, track when this session started
32 - // so the AI only sees messages from this page load
33 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
34 -
35 14 this.instances[botId] = {
36 15 botId: botId,
37 - sessionId: null,
16 + sessionId: this.getChatSession(botId),
38 17 lastSeenMessageId: '',
39 18 notificationCheckInterval: null,
40 19 pollingInterval: null,
41 20 processedMessageIds: new Set(),
@@ -41,11 +20,9 @@
41 20 processedMessageIds: new Set(),
42 21 activePdfFile: null,
43 22 activeWordFile: null,
44 23 chatHistoryLoaded: false,
45 - isStreaming: false,
46 - // Fresh context timestamp - only used when persistence is OFF
47 - sessionStartTimestamp: chatPersistenceEnabled ? 0 : Date.now()
24 + isStreaming: false
48 25 };
49 26 }
50 27 return this.instances[botId];
51 28 },
@@ -60,77 +37,23 @@
60 37 return Object.keys(this.instances);
61 38 },
62 39
63 40 // Session management per bot
64 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
65 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
66 41 getChatSession: function(botId) {
67 42 var cookieName = 'mxchat_session_id_' + botId;
68 - var storageKey = 'mxchat_session_id_' + botId;
69 43 var sessionId = getCookie(cookieName);
70 44
71 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
72 45 if (!sessionId) {
73 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
46 + sessionId = generateSessionId();
47 + this.setChatSession(botId, sessionId);
74 48 }
75 49
76 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
77 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
78 - // storage). Without this, ensureSession() can generate and store an ID that
79 - // getChatSession() then can't read back, causing null session_ids on send.
80 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
81 - sessionId = this.instances[botId].sessionId;
82 - }
83 -
84 - // Guard against stored sentinel values that indicate earlier broken writes.
85 - if (sessionId === 'null' || sessionId === 'undefined') {
86 - sessionId = null;
87 - }
88 -
89 - // Re-sync cookie from localStorage if cookie was lost
90 - if (sessionId && !getCookie(cookieName)) {
91 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
92 - }
93 -
94 - return sessionId || null;
50 + return sessionId;
95 51 },
96 52
97 - // Lazy session initializer — called on first user interaction
98 - ensureSession: function(botId) {
99 - botId = botId || 'default';
100 - var instance = this.instances[botId] || this.init(botId);
101 -
102 - if (instance.sessionId) {
103 - return instance.sessionId;
104 - }
105 -
106 - // Check for existing session from cookie or localStorage
107 - var existingSession = this.getChatSession(botId);
108 -
109 - if (existingSession) {
110 - instance.sessionId = existingSession;
111 - } else {
112 - // Brand new session
113 - var newId = generateSessionId();
114 - this.setChatSession(botId, newId);
115 - instance.sessionId = newId;
116 - }
117 -
118 - // Now that we have a session, do the deferred work
119 - refreshNonceIfNeeded();
120 - trackOriginatingPage();
121 -
122 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
123 - // so we do NOT call it here to avoid a race condition.
124 -
125 - return instance.sessionId;
126 - },
127 -
128 53 setChatSession: function(botId, sessionId) {
129 54 var cookieName = 'mxchat_session_id_' + botId;
130 - var storageKey = 'mxchat_session_id_' + botId;
131 55 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
132 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
133 56 if (this.instances[botId]) {
134 57 this.instances[botId].sessionId = sessionId;
135 58 }
136 59 },
@@ -135,10 +58,8 @@
135 58 }
136 59 },
137 60
138 61 resetChatSession: function(botId) {
139 - // Clear old session from localStorage before setting new one
140 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
141 62 var newSessionId = generateSessionId();
142 63 this.setChatSession(botId, newSessionId);
143 64 var $chatBox = getElement(botId, 'chat-box');
144 65 if ($chatBox.length) {
@@ -147,20 +68,8 @@
147 68 if (this.instances[botId]) {
148 69 this.instances[botId].chatHistoryLoaded = false;
149 70 this.instances[botId].processedMessageIds = new Set();
150 71 }
151 - },
152 -
153 - // Silent reset — new session ID without clearing the chat UI
154 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
155 - silentResetSession: function(botId) {
156 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
157 - var newSessionId = generateSessionId();
158 - this.setChatSession(botId, newSessionId);
159 - if (this.instances[botId]) {
160 - this.instances[botId].sessionId = newSessionId;
161 - }
162 - return newSessionId;
163 72 }
164 73 };
165 74
166 75 // ====================================
@@ -465,9 +374,8 @@
465 374
466 375 // Update your existing sendMessage function
467 376 function sendMessage(botId) {
468 377 botId = botId || 'default';
469 - MxChatInstances.ensureSession(botId);
470 378 var $chatInput = getElement(botId, 'chat-input');
471 379 var message = $chatInput.val();
472 380
473 381 // ADD PROMPT HOOK HERE
@@ -475,14 +383,10 @@
475 383 message = customMxChatFilter(message, "prompt");
476 384 }
477 385
478 386 if (message) {
479 - // Don't disable input in live agent mode - let users chat freely
480 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
481 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
482 - if (!isAgentMode) {
483 - disableChatInput(botId);
484 - }
387 + // Disable input while waiting for response
388 + disableChatInput(botId);
485 389
486 390 appendMessage("user", message, '', [], false, botId);
487 391 $chatInput.val('');
488 392 $chatInput.css('height', 'auto');
@@ -492,9 +396,9 @@
492 396 }
493 397 appendThinkingMessage(botId);
494 398 scrollToBottom(botId);
495 399
496 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
400 + const currentModel = mxchatChat.model || 'gpt-4o';
497 401
498 402 // Check if streaming is enabled AND supported for this model
499 403 if (shouldUseStreaming(currentModel)) {
500 404 callMxChatStream(message, function(response) {
@@ -510,9 +414,8 @@
510 414
511 415 // Update your existing sendMessageToChatbot function
512 416 function sendMessageToChatbot(message, botId) {
513 417 botId = botId || 'default';
514 - MxChatInstances.ensureSession(botId);
515 418
516 419 // ADD PROMPT HOOK HERE
517 420 if (typeof customMxChatFilter === 'function') {
518 421 message = customMxChatFilter(message, "prompt");
@@ -517,14 +420,10 @@
517 420 if (typeof customMxChatFilter === 'function') {
518 421 message = customMxChatFilter(message, "prompt");
519 422 }
520 423
521 - // Don't disable input in live agent mode - let users chat freely
522 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
523 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
524 - if (!isAgentMode) {
525 - disableChatInput(botId);
526 - }
424 + // Disable input while waiting for response
425 + disableChatInput(botId);
527 426
528 427 var sessionId = getChatSession(botId);
529 428
530 429 if (hasQuickQuestions(botId)) {
@@ -532,9 +431,9 @@
532 431 }
533 432 appendThinkingMessage(botId);
534 433 scrollToBottom(botId);
535 434
536 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
435 + const currentModel = mxchatChat.model || 'gpt-4o';
537 436
538 437 // Check if streaming is enabled AND supported for this model
539 438 if (shouldUseStreaming(currentModel)) {
540 439 callMxChatStream(message, function(response) {
@@ -609,32 +508,17 @@
609 508
610 509 // Get page context if contextual awareness is enabled
611 510 const pageContext = getPageContext();
612 511
613 - // Get instance for session start timestamp (used when persistence is OFF)
614 - var instance = MxChatInstances.get(botId);
615 -
616 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
617 - // and returns the guaranteed-present session id from the in-memory instance even when
618 - // cookie/localStorage writes are silently blocked by the browser.
619 - var sessionId = MxChatInstances.ensureSession(botId);
620 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
621 - // Last-resort generation to ensure we never POST a null marker.
622 - sessionId = generateSessionId();
623 - MxChatInstances.setChatSession(botId, sessionId);
624 - }
625 -
626 512 // Prepare AJAX data
627 513 const ajaxData = {
628 514 action: 'mxchat_handle_chat_request',
629 515 message: message,
630 - session_id: sessionId,
516 + session_id: getChatSession(botId),
631 517 nonce: mxchatChat.nonce,
632 518 current_page_url: window.location.href,
633 519 current_page_title: document.title,
634 - bot_id: botId,
635 - // Pass session start timestamp so AI context matches what user sees
636 - session_start_timestamp: instance.sessionStartTimestamp || 0
520 + bot_id: botId
637 521 };
638 522
639 523 // Add page context if available
640 524 if (pageContext) {
@@ -690,16 +574,23 @@
690 574 errorMessage = "An error occurred. Please try again or contact support.";
691 575 }
692 576
693 577 // Handle session reset action (IP changed, session expired, etc.)
694 - // Silent reset — keep chat UI intact, just get a new session and retry
695 578 if (response.data && response.data.action === 'reset_session') {
696 - MxChatInstances.silentResetSession(botId);
697 - // Re-send the original message with the new session (user message is already displayed)
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
698 584 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
699 585 if (originalMessage) {
700 586 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
701 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
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';
702 593 if (shouldUseStreaming(currentModel)) {
703 594 callMxChatStream(originalMessage, function(response) {
704 595 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
705 596 }, botId);
@@ -756,11 +647,9 @@
756 647 }
757 648
758 649 // Check for live agent response
759 650 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
760 - removeThinkingDots(botId);
761 651 updateChatModeIndicator('agent', botId);
762 - enableChatInput(botId);
763 652 return;
764 653 }
765 654
766 655 // Handle the message and show notification if chat is hidden
@@ -793,13 +682,9 @@
793 682 $badge.show();
794 683 }
795 684 }
796 685 } else {
797 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
798 - if (response.vectorstore_error) {
799 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
800 - }
801 - replaceLastMessage("bot", emptyMsg, '', [], botId);
686 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
802 687 }
803 688
804 689 if (response.message_id) {
805 690 var instance = MxChatInstances.get(botId);
@@ -849,9 +734,9 @@
849 734
850 735 // Store the message in case we need to retry after session reset
851 736 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
852 737
853 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
738 + const currentModel = mxchatChat.model || 'gpt-4o';
854 739 if (!isStreamingSupported(currentModel)) {
855 740 callMxChat(message, callback, botId);
856 741 return;
857 742 }
@@ -858,32 +743,17 @@
858 743
859 744 // Get page context if contextual awareness is enabled
860 745 const pageContext = getPageContext();
861 746
862 - // Get instance for session start timestamp (used when persistence is OFF)
863 - var instance = MxChatInstances.get(botId);
864 -
865 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
866 - // non-string value via String(), so passing `null` would POST the literal string "null"
867 - // and land in the transcripts table as a ghost session. ensureSession() always returns
868 - // a real string even when cookies/localStorage are blocked.
869 - var streamSessionId = MxChatInstances.ensureSession(botId);
870 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
871 - streamSessionId = generateSessionId();
872 - MxChatInstances.setChatSession(botId, streamSessionId);
873 - }
874 -
875 747 const formData = new FormData();
876 748 formData.append('action', 'mxchat_stream_chat');
877 749 formData.append('message', message);
878 - formData.append('session_id', streamSessionId);
750 + formData.append('session_id', getChatSession(botId));
879 751 formData.append('nonce', mxchatChat.nonce);
880 752 formData.append('current_page_url', window.location.href);
881 753 formData.append('current_page_title', document.title);
882 754 formData.append('bot_id', botId);
883 - // Pass session start timestamp so AI context matches what user sees
884 - formData.append('session_start_timestamp', instance.sessionStartTimestamp || 0);
885 -
755 +
886 756 // Add page context if available
887 757 if (pageContext) {
888 758 formData.append('page_context', JSON.stringify(pageContext));
889 759 }
@@ -976,16 +846,8 @@
976 846
977 847 // Re-enable chat input when stream ends with content
978 848 enableChatInput(botId);
979 849
980 - // Scroll the user's last message to the top now that the
981 - // bot's full reply has rendered (gives max reading room).
982 - var $chatBoxDone = getElement(botId, 'chat-box');
983 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
984 - if ($lastUserMsgDone.length) {
985 - scrollElementToTop($lastUserMsgDone, botId);
986 - }
987 -
988 850 if (callback) {
989 851 callback(accumulatedContent);
990 852 }
991 853 return;
@@ -1008,16 +870,8 @@
1008 870
1009 871 // Re-enable chat input after streaming completes
1010 872 enableChatInput(botId);
1011 873
1012 - // Scroll the user's last message to the top now
1013 - // that the bot's full reply has rendered.
1014 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1015 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1016 - if ($lastUserMsgStreamDone.length) {
1017 - scrollElementToTop($lastUserMsgStreamDone, botId);
1018 - }
1019 -
1020 874 if (callback) {
1021 875 callback(accumulatedContent);
1022 876 }
1023 877 return;
@@ -1136,16 +990,21 @@
1136 990 errorMessage = "An error occurred. Please try again or contact support.";
1137 991 }
1138 992
1139 993 // Handle session reset action (IP changed, session expired, etc.)
1140 - // Silent reset — keep chat UI intact, just get a new session and retry
1141 994 if (data.data && data.data.action === 'reset_session') {
1142 - MxChatInstances.silentResetSession(botId);
1143 - // Re-send the original message with the new session (user message is already displayed)
995 + // Clear the old session and generate a new one
996 + resetChatSession(botId);
997 + // Re-send the original message with the new session
1144 998 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1145 999 if (originalMessage) {
1146 1000 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1147 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
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';
1148 1007 if (shouldUseStreaming(currentModel)) {
1149 1008 callMxChatStream(originalMessage, callback, botId);
1150 1009 } else {
1151 1010 callMxChat(originalMessage, callback, botId);
@@ -1167,22 +1026,8 @@
1167 1026 }
1168 1027 return; // Exit early for errors
1169 1028 }
1170 1029
1171 - // Check for live agent response
1172 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1173 - removeThinkingDots(botId);
1174 - // Also remove any leftover bot-message that lost its temporary-message class
1175 - var $chatBox = getElement(botId, 'chat-box');
1176 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1177 - updateChatModeIndicator('agent', botId);
1178 - enableChatInput(botId);
1179 - if (callback) {
1180 - callback('');
1181 - }
1182 - return;
1183 - }
1184 -
1185 1030 // Handle different response formats
1186 1031 if (data.text || data.html || data.message) {
1187 1032
1188 1033 // Apply response hooks
@@ -1227,15 +1072,19 @@
1227 1072 }
1228 1073
1229 1074 // Enhanced updateChatModeIndicator function for immediate DOM updates
1230 1075 function updateChatModeIndicator(mode, botId) {
1076 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1231 1077 botId = botId || 'default';
1232 1078 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1079 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1233 1080 if (indicator) {
1234 1081 const oldText = indicator.textContent;
1082 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1235 1083
1236 1084 if (mode === 'agent') {
1237 1085 indicator.textContent = 'Live Agent';
1086 + console.log('[MxChat] Mode is agent, calling startPolling...');
1238 1087 startPolling(botId);
1239 1088 } else {
1240 1089 // Everything else is AI mode
1241 1090 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1306,12 +1155,9 @@
1306 1155 // Update the event handlers to use the correct function names (using event delegation)
1307 1156 // Use class-based selectors for multi-instance support
1308 1157 $(document).on('click', '.send-button', function() {
1309 1158 var botId = getBotIdFromElement(this);
1310 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1311 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1312 - disableChatInput(botId);
1313 - }
1159 + disableChatInput(botId);
1314 1160 sendMessage(botId);
1315 1161 });
1316 1162
1317 1163 // Override enter key handler (using event delegation)
@@ -1318,256 +1164,14 @@
1318 1164 $(document).on('keypress', '.chat-input', function(e) {
1319 1165 if (e.which == 13 && !e.shiftKey) {
1320 1166 e.preventDefault();
1321 1167 var botId = getBotIdFromElement(this);
1322 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1323 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1324 - disableChatInput(botId);
1325 - }
1168 + disableChatInput(botId);
1326 1169 sendMessage(botId);
1327 1170 }
1328 1171 });
1329 1172
1330 1173
1331 -// Tags the chat-box transcript so the print stylesheet can target it,
1332 -// and lazily initializes the header overflow menu for this bot if the
1333 -// markup is present but not yet wired (covers dynamically-rendered widgets).
1334 -// Idempotent; safe to call on every appended message.
1335 -function mxchatEnsurePrintRoot(botId) {
1336 - var $chatBox = getElement(botId, 'chat-box');
1337 - if (!$chatBox || !$chatBox.length) return;
1338 - $chatBox.addClass('mxchat-conversation-print-root');
1339 - if (!$chatBox.attr('data-print-title')) {
1340 - var nowStr = new Date().toLocaleString();
1341 - var headerTitle = ((typeof mxchatChat !== 'undefined' && mxchatChat.print_header_title) || 'Chat transcript') + ' — ' + nowStr;
1342 - $chatBox.attr('data-print-title', headerTitle);
1343 - }
1344 - if (typeof mxchatInitHeaderMenu === 'function') {
1345 - mxchatInitHeaderMenu(botId);
1346 - }
1347 -}
1348 -
1349 -// Builds the list of overflow-menu items for a given bot.
1350 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1351 -function mxchatGetHeaderMenuItems(botId) {
1352 - var items = [];
1353 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1354 -
1355 - // The `print_button_*` keys still gate this item for back-compat with
1356 - // existing user options. The action is now a transcript download, not print.
1357 - if (settings.print_button_enabled === 'on') {
1358 - items.push({
1359 - id: 'download-transcript',
1360 - label: settings.print_button_label || 'Download Transcript',
1361 - icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
1362 - action: function() {
1363 - mxchatDownloadTranscript(botId);
1364 - }
1365 - });
1366 - }
1367 -
1368 - return items;
1369 -}
1370 -
1371 -// Builds a clean markdown transcript of the current conversation and triggers
1372 -// a file download. Used by the "Download Transcript" menu item.
1373 -function mxchatDownloadTranscript(botId) {
1374 - var $chatBox = getElement(botId, 'chat-box');
1375 - if (!$chatBox || !$chatBox.length) return;
1376 -
1377 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1378 - var headerTitle = settings.print_header_title || 'Chat transcript';
1379 - var now = new Date();
1380 - var stamp = now.toLocaleString();
1381 -
1382 - var lines = [];
1383 - lines.push('# ' + headerTitle);
1384 - lines.push('');
1385 - lines.push('Exported: ' + stamp);
1386 - lines.push('');
1387 - lines.push('---');
1388 - lines.push('');
1389 -
1390 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1391 - var $msg = $(this);
1392 - // Skip thinking placeholders and any in-flight temporary messages.
1393 - if ($msg.find('.thinking-dots').length) return;
1394 - if ($msg.hasClass('temporary-message')) return;
1395 -
1396 - var sender;
1397 - if ($msg.hasClass('user-message')) sender = 'User';
1398 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1399 - else sender = 'AI Agent';
1400 -
1401 - // Strip interactive UI from the cloned message so we get the conversation text.
1402 - var $clone = $msg.clone();
1403 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1404 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1405 - if (!text) return;
1406 -
1407 - lines.push('**' + sender + '**');
1408 - lines.push('');
1409 - lines.push(text);
1410 - lines.push('');
1411 - });
1412 -
1413 - var content = lines.join('\n');
1414 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1415 - var fname = 'mxchat-transcript-' + iso + '.md';
1416 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1417 - var url = URL.createObjectURL(blob);
1418 - var a = document.createElement('a');
1419 - a.href = url;
1420 - a.download = fname;
1421 - a.style.display = 'none';
1422 - document.body.appendChild(a);
1423 - a.click();
1424 - setTimeout(function() {
1425 - if (a.parentNode) a.parentNode.removeChild(a);
1426 - URL.revokeObjectURL(url);
1427 - }, 100);
1428 -}
1429 -
1430 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1431 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1432 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1433 -function mxchatSyncMenuColors(botId, $wrap) {
1434 - if (!$wrap || !$wrap.length) return;
1435 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1436 - if (!$bot.length) return;
1437 - var cs = window.getComputedStyle($bot[0]);
1438 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1439 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1440 - }
1441 - // Bot text color usually lives on a child div, not .bot-message itself.
1442 - var $textChild = $bot.find('[style*="color"]').first();
1443 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1444 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1445 -}
1446 -
1447 -// One-time per-widget init: renders menu items, wires open/close,
1448 -// outside-click, Escape, and arrow-key navigation. If no items, hides the trigger.
1449 -function mxchatInitHeaderMenu(botId) {
1450 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1451 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1452 -
1453 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1454 - var $menu = $wrap.find('.mxchat-header-menu');
1455 - var items = mxchatGetHeaderMenuItems(botId);
1456 -
1457 - // Initial color sync — covers normal page load.
1458 - mxchatSyncMenuColors(botId, $wrap);
1459 -
1460 - if (!items.length) {
1461 - $trigger.hide();
1462 - $menu.hide();
1463 - $wrap.data('mxchatMenuReady', true);
1464 - return;
1465 - }
1466 -
1467 - // Build the menu items.
1468 - $menu.empty();
1469 - items.forEach(function(item, idx) {
1470 - var $btn = $('<button>', {
1471 - type: 'button',
1472 - 'class': 'mxchat-menu-item',
1473 - 'role': 'menuitem',
1474 - 'tabindex': '-1',
1475 - 'data-menu-id': item.id,
1476 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1477 - '<span class="mxchat-menu-item-label"></span>'
1478 - });
1479 - $btn.find('.mxchat-menu-item-label').text(item.label);
1480 - $btn.on('click', function(e) {
1481 - e.preventDefault();
1482 - e.stopPropagation();
1483 - closeMenu();
1484 - try { item.action(); } catch (err) { /* no-op */ }
1485 - });
1486 - $menu.append($btn);
1487 - });
1488 -
1489 - function openMenu() {
1490 - // Re-sync each open in case the active theme changed since init.
1491 - mxchatSyncMenuColors(botId, $wrap);
1492 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1493 - $trigger.attr('aria-expanded', 'true');
1494 - // Focus the first item for keyboard users
1495 - setTimeout(function() {
1496 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1497 - }, 0);
1498 - }
1499 - function closeMenu(returnFocus) {
1500 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1501 - $trigger.attr('aria-expanded', 'false');
1502 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1503 - if (returnFocus) $trigger.trigger('focus');
1504 - }
1505 -
1506 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1507 - // click-to-collapse handler does not fire.
1508 - $trigger.on('click', function(e) {
1509 - e.preventDefault();
1510 - e.stopPropagation();
1511 - if ($menu.hasClass('is-open')) closeMenu();
1512 - else openMenu();
1513 - });
1514 -
1515 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1516 - $menu.on('click', function(e) {
1517 - e.stopPropagation();
1518 - });
1519 -
1520 - // Outside click closes the menu.
1521 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1522 - if (!$menu.hasClass('is-open')) return;
1523 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1524 - closeMenu();
1525 - });
1526 -
1527 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1528 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1529 - var $items = $menu.find('.mxchat-menu-item');
1530 - var idx = $items.index(this);
1531 - if (e.key === 'Escape') {
1532 - e.preventDefault();
1533 - closeMenu(true);
1534 - } else if (e.key === 'ArrowDown') {
1535 - e.preventDefault();
1536 - var $next = $items.eq((idx + 1) % $items.length);
1537 - $items.attr('tabindex', '-1');
1538 - $next.attr('tabindex', '0').trigger('focus');
1539 - } else if (e.key === 'ArrowUp') {
1540 - e.preventDefault();
1541 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1542 - $items.attr('tabindex', '-1');
1543 - $prev.attr('tabindex', '0').trigger('focus');
1544 - } else if (e.key === 'Enter' || e.key === ' ') {
1545 - e.preventDefault();
1546 - $(this).trigger('click');
1547 - }
1548 - });
1549 - $trigger.on('keydown', function(e) {
1550 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1551 - e.preventDefault();
1552 - closeMenu(true);
1553 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1554 - e.preventDefault();
1555 - openMenu();
1556 - }
1557 - });
1558 -
1559 - $wrap.data('mxchatMenuReady', true);
1560 -}
1561 -
1562 -// Initialize header menus for every rendered widget on DOM ready.
1563 -$(function() {
1564 - $('.mxchat-header-menu-wrap').each(function() {
1565 - var botId = $(this).data('bot-id');
1566 - if (botId) mxchatInitHeaderMenu(botId);
1567 - });
1568 -});
1569 -
1570 1174 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1571 1175 try {
1572 1176 // Determine styles based on sender type
1573 1177 let messageClass, bgColor, fontColor;
@@ -1605,12 +1209,17 @@
1605 1209 'margin-bottom': '1em'
1606 1210 });
1607 1211 }
1608 1212
1609 - // Process the message content - always run linkify to convert markdown
1610 - // links and format text. linkify() handles existing HTML safely via
1611 - // negative lookaheads that skip URLs already inside <a> tags.
1612 - let fullMessage = linkify(messageText);
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 + }
1613 1222
1614 1223 // Add images if provided
1615 1224 if (images && images.length > 0) {
1616 1225 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1659,12 +1268,8 @@
1659 1268 if (lastUserMessage.length) {
1660 1269 scrollElementToTop(lastUserMessage, botId);
1661 1270 }
1662 1271 }
1663 -
1664 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1665 - mxchatEnsurePrintRoot(botId);
1666 - }
1667 1272 });
1668 1273
1669 1274 if (messageText.id) {
1670 1275 var instance = MxChatInstances.get(botId);
@@ -1749,12 +1354,26 @@
1749 1354 bgColor = botMessageBgColor;
1750 1355 fontColor = botMessageFontColor;
1751 1356 }
1752 1357
1753 - // Always run linkify to convert markdown links and format text.
1754 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1755 - // to avoid double-processing URLs that are already inside <a> tags).
1756 - var fullMessage = linkify(responseText);
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 + }
1757 1376
1758 1377 if (responseHtml) {
1759 1378 // Only add line breaks if there's actual text content before the HTML
1760 1379 if (fullMessage && fullMessage.trim()) {
@@ -1811,12 +1430,8 @@
1811 1430 }
1812 1431
1813 1432 // Re-enable chat input after response is displayed
1814 1433 enableChatInput(botId);
1815 -
1816 - if (sender === "bot" || sender === "agent") {
1817 - mxchatEnsurePrintRoot(botId);
1818 - }
1819 1434 } else {
1820 1435 appendMessage(sender, responseText, responseHtml, images, false, botId);
1821 1436 // Re-enable chat input after response is displayed
1822 1437 enableChatInput(botId);
@@ -1825,15 +1440,8 @@
1825 1440
1826 1441
1827 1442 function appendThinkingMessage(botId) {
1828 1443 botId = botId || 'default';
1829 -
1830 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
1831 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
1832 - if (indicator && indicator.textContent === 'Live Agent') {
1833 - return;
1834 - }
1835 -
1836 1444 var $chatBox = getElement(botId, 'chat-box');
1837 1445
1838 1446 // Remove any existing thinking dots in this bot's chat first
1839 1447 $chatBox.find('.thinking-dots').remove();
@@ -1855,9 +1463,9 @@
1855 1463 '</div>' +
1856 1464 '</div>';
1857 1465
1858 1466 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1859 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1467 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
1860 1468 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1861 1469 scrollToBottom(botId);
1862 1470 }
1863 1471
@@ -1863,11 +1471,9 @@
1863 1471
1864 1472 function removeThinkingDots(botId) {
1865 1473 botId = botId || 'default';
1866 1474 var $chatBox = getElement(botId, 'chat-box');
1867 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1868 1475 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1869 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1870 1476 }
1871 1477
1872 1478 // ====================================
1873 1479 // TEXT FORMATTING & PROCESSING
@@ -1901,12 +1507,9 @@
1901 1507 processedText = formatTextStyling(processedText);
1902 1508
1903 1509 // Process code blocks BEFORE processing links
1904 1510 processedText = formatCodeBlocks(processedText);
1905 -
1906 - // Process markdown tables BEFORE converting newlines to paragraphs
1907 - processedText = formatMarkdownTables(processedText);
1908 -
1511 +
1909 1512 // NOW convert to paragraphs
1910 1513 processedText = convertNewlinesToBreaks(processedText);
1911 1514
1912 1515 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1919,63 +1522,37 @@
1919 1522 // Return as a proper link without the brackets
1920 1523 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1921 1524 });
1922 1525
1923 - // Process markdown links: [text](url) and [](url)
1924 - // Uses balanced parenthesis matching to handle URLs containing parens
1925 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
1926 - processedText = (function(input) {
1927 - var result = '';
1928 - var i = 0;
1929 - while (i < input.length) {
1930 - // Look for [ at current position
1931 - if (input[i] === '[') {
1932 - // Find closing ]
1933 - var closeBracket = input.indexOf(']', i + 1);
1934 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
1935 - result += input[i];
1936 - i++;
1937 - continue;
1938 - }
1939 - var linkText = input.substring(i + 1, closeBracket);
1940 - // Check if URL starts with http
1941 - var urlStart = closeBracket + 2;
1942 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
1943 - result += input[i];
1944 - i++;
1945 - continue;
1946 - }
1947 - // Find balanced closing paren
1948 - var depth = 1;
1949 - var j = urlStart;
1950 - while (j < input.length && depth > 0) {
1951 - if (input[j] === '(') depth++;
1952 - else if (input[j] === ')') depth--;
1953 - if (depth > 0) j++;
1954 - }
1955 - if (depth !== 0) {
1956 - result += input[i];
1957 - i++;
1958 - continue;
1959 - }
1960 - var url = input.substring(urlStart, j);
1961 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
1962 - var encodedUrl = safeEncodeUrl(cleanUrl);
1963 - if (!linkText || !linkText.trim()) {
1964 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
1965 - } else {
1966 - var safeText = sanitizeUserInput(linkText);
1967 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
1968 - }
1969 - i = j + 1; // Skip past the closing )
1970 - } else {
1971 - result += input[i];
1972 - i++;
1973 - }
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>`;
1974 1536 }
1975 - return result;
1976 - })(processedText);
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 + });
1977 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 +
1978 1555 // Process phone numbers: [text](tel:number)
1979 1556 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1980 1557 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1981 1558 const safePhone = safeEncodeUrl(phone);
@@ -2127,78 +1704,9 @@
2127 1704 });
2128 1705
2129 1706 return text;
2130 1707 }
2131 -
2132 - function formatMarkdownTables(text) {
2133 - var lines = text.split('\n');
2134 - var result = [];
2135 - var i = 0;
2136 -
2137 - while (i < lines.length) {
2138 - // Check for a table: current line has pipes AND next line is a separator row
2139 - if (i + 1 < lines.length &&
2140 - lines[i].indexOf('|') !== -1 &&
2141 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2142 -
2143 - var tableLines = [];
2144 - var headerLine = lines[i];
2145 - var separatorLine = lines[i + 1];
2146 - tableLines.push(headerLine);
2147 - tableLines.push(separatorLine);
2148 -
2149 - // Collect remaining table rows
2150 - var j = i + 2;
2151 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2152 - tableLines.push(lines[j]);
2153 - j++;
2154 - }
2155 -
2156 - // Parse alignment from separator row
2157 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2158 - var alignments = sepCells.map(function(cell) {
2159 - var trimmed = cell.trim();
2160 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2161 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2162 - return 'left';
2163 - });
2164 -
2165 - // Build HTML table
2166 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2167 -
2168 - // Header row
2169 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2170 - html += '<thead><tr>';
2171 - headerCells.forEach(function(cell, idx) {
2172 - var align = alignments[idx] || 'left';
2173 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2174 - });
2175 - html += '</tr></thead>';
2176 -
2177 - // Body rows
2178 - html += '<tbody>';
2179 - for (var r = 2; r < tableLines.length; r++) {
2180 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2181 - html += '<tr>';
2182 - rowCells.forEach(function(cell, idx) {
2183 - var align = alignments[idx] || 'left';
2184 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2185 - });
2186 - html += '</tr>';
2187 - }
2188 - html += '</tbody></table></div>';
2189 -
2190 - result.push(html);
2191 - i = j;
2192 - } else {
2193 - result.push(lines[i]);
2194 - i++;
2195 - }
2196 - }
2197 -
2198 - return result.join('\n');
2199 - }
2200 -
1708 +
2201 1709 function sanitizeUserInput(text) {
2202 1710 const div = document.createElement('div');
2203 1711 div.textContent = text;
2204 1712 return div.innerHTML;
@@ -2269,14 +1777,13 @@
2269 1777 requestAnimationFrame(smoothScroll);
2270 1778 }
2271 1779 }
2272 1780
2273 - function scrollElementToTop(element, botId, topOffset) {
1781 + function scrollElementToTop(element, botId) {
2274 1782 botId = botId || 'default';
2275 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2276 1783 var chatBox = getElement(botId, 'chat-box');
2277 1784 var elementTop = element.position().top + chatBox.scrollTop();
2278 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1785 + chatBox.animate({ scrollTop: elementTop }, 500);
2279 1786 }
2280 1787
2281 1788 function showChatWidget(botId) {
2282 1789 botId = botId || 'default';
@@ -2420,12 +1927,15 @@
2420 1927 // LIVE AGENT FUNCTIONALITY
2421 1928 // ====================================
2422 1929
2423 1930 function startPolling(botId) {
1931 + console.log('[MxChat] startPolling called for botId:', botId);
2424 1932 botId = botId || 'default';
2425 1933 var instance = MxChatInstances.get(botId);
2426 1934 // Clear any existing interval first
2427 1935 stopPolling(botId);
1936 + // Start new polling interval
1937 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2428 1938 instance.pollingInterval = setInterval(function() {
2429 1939 checkForAgentMessages(botId);
2430 1940 }, 5000);
2431 1941 }
@@ -2430,17 +1940,20 @@
2430 1940 }, 5000);
2431 1941 }
2432 1942
2433 1943 function stopPolling(botId) {
1944 + console.log('[MxChat] stopPolling called for botId:', botId);
2434 1945 botId = botId || 'default';
2435 1946 var instance = MxChatInstances.get(botId);
2436 1947 if (instance.pollingInterval) {
2437 1948 clearInterval(instance.pollingInterval);
2438 1949 instance.pollingInterval = null;
1950 + console.log('[MxChat] Polling stopped for botId:', botId);
2439 1951 }
2440 1952 }
2441 1953
2442 1954 function checkForAgentMessages(botId) {
1955 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2443 1956 botId = botId || 'default';
2444 1957 var instance = MxChatInstances.get(botId);
2445 1958 const sessionId = getChatSession(botId);
2446 1959 $.ajax({
@@ -2466,12 +1979,8 @@
2466 1979 instance.processedMessageIds.add(message.id);
2467 1980 }
2468 1981 });
2469 1982
2470 - if (hasNewMessage) {
2471 - enableChatInput(botId);
2472 - }
2473 -
2474 1983 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2475 1984 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2476 1985 showNotification(botId);
2477 1986 }
@@ -2477,13 +1986,8 @@
2477 1986 }
2478 1987
2479 1988 scrollToBottom(botId, true);
2480 1989 }
2481 -
2482 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2483 - if (response.success && response.data?.chat_mode) {
2484 - updateChatModeIndicator(response.data.chat_mode, botId);
2485 - }
2486 1990 },
2487 1991 error: function (xhr, status, error) {
2488 1992 // Polling error - silently continue
2489 1993 }
@@ -2493,29 +1997,20 @@
2493 1997 // ====================================
2494 1998 // CHAT HISTORY & PERSISTENCE
2495 1999 // ====================================
2496 2000
2497 -function loadChatHistory(botId, onComplete) {
2001 +function loadChatHistory(botId) {
2498 2002 botId = botId || 'default';
2499 2003 var instance = MxChatInstances.get(botId);
2500 2004
2501 2005 // Prevent duplicate loading
2502 2006 if (instance.chatHistoryLoaded) {
2503 - if (onComplete) onComplete();
2504 2007 return;
2505 2008 }
2506 2009
2507 - // Use getChatSession which returns null if no session exists (does NOT create one)
2508 2010 var sessionId = getChatSession(botId);
2509 2011 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2510 2012
2511 - // No session yet — nothing to load. History will load after first message via ensureSession.
2512 - if (!sessionId) {
2513 - instance.chatHistoryLoaded = true;
2514 - if (onComplete) onComplete();
2515 - return;
2516 - }
2517 -
2518 2013 if (chatPersistenceEnabled && sessionId) {
2519 2014 $.ajax({
2520 2015 url: mxchatChat.ajax_url,
2521 2016 type: 'POST',
@@ -2526,12 +2021,11 @@
2526 2021 },
2527 2022 success: function(response) {
2528 2023 // Handle session reset (IP changed while user was away)
2529 2024 if (response.success === false && response.data && response.data.action === 'reset_session') {
2530 - // Silent reset — new session but don't clear UI
2531 - MxChatInstances.silentResetSession(botId);
2025 + // Silently reset session - user will start fresh
2026 + resetChatSession(botId);
2532 2027 instance.chatHistoryLoaded = true; // Prevent retry loop
2533 - if (onComplete) onComplete();
2534 2028 return;
2535 2029 }
2536 2030
2537 2031 // Check if the response indicates success
@@ -2587,19 +2081,9 @@
2587 2081 var content = message.content;
2588 2082 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2589 2083 content = decodeHTMLEntities(content);
2590 2084
2591 - // Skip linkify for messages containing structured HTML
2592 - // (forms, product cards, galleries, etc.) to avoid
2593 - // markdown formatting corrupting HTML attributes
2594 - // (e.g. underscores in name="field_name" becoming <em> tags)
2595 - if (content.includes("mxchat-product-card") ||
2596 - content.includes("mxchat-image-gallery") ||
2597 - content.includes("mxchat-featured-products") ||
2598 - content.includes("<form") ||
2599 - content.includes("<input") ||
2600 - content.includes("<select") ||
2601 - content.includes("<textarea")) {
2085 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2602 2086 messageElement.html(content);
2603 2087 } else {
2604 2088 var formattedContent = linkify(content);
2605 2089 messageElement.html(formattedContent);
@@ -2639,17 +2123,13 @@
2639 2123 instance.chatHistoryLoaded = true;
2640 2124 }
2641 2125 }
2642 2126 }
2643 - if (onComplete) onComplete();
2644 2127 },
2645 2128 error: function(xhr, status, error) {
2646 2129 // Error loading chat history - silently continue
2647 - if (onComplete) onComplete();
2648 2130 }
2649 2131 });
2650 - } else {
2651 - if (onComplete) onComplete();
2652 2132 }
2653 2133 }
2654 2134
2655 2135
@@ -2825,35 +2305,45 @@
2825 2305 // ====================================
2826 2306
2827 2307 function checkPreChatDismissal(botId) {
2828 2308 botId = botId || 'default';
2829 - try {
2830 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2831 - if (dismissedAt) {
2832 - // Re-show after 24 hours
2833 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2834 - if (elapsed < 86400000) {
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 {
2835 2320 getElement(botId, 'pre-chat-message').hide();
2836 - return;
2837 2321 }
2838 - // Expired — clear and show again
2839 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2322 + },
2323 + error: function() {
2324 + // Error checking pre-chat dismissal - silently continue
2840 2325 }
2841 - getElement(botId, 'pre-chat-message').fadeIn(250);
2842 - } catch (e) {
2843 - // localStorage unavailable — show the message
2844 - getElement(botId, 'pre-chat-message').fadeIn(250);
2845 - }
2326 + });
2846 2327 }
2847 2328
2848 2329 function handlePreChatDismissal(botId) {
2849 2330 botId = botId || 'default';
2850 2331 getElement(botId, 'pre-chat-message').fadeOut(200);
2851 - try {
2852 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2853 - } catch (e) {
2854 - // localStorage unavailable — dismissal won't persist
2855 - }
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 + });
2856 2346 }
2857 2347
2858 2348
2859 2349 // ====================================
@@ -2920,26 +2410,8 @@
2920 2410 $(this).addClass('hidden');
2921 2411 $badge.hide(); // Hide notification when opening chat
2922 2412 disableScroll();
2923 2413 $preChat.fadeOut(250);
2924 -
2925 - // Load chat history for returning visitors (persistence)
2926 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
2927 - if (chatPersistenceEnabled) {
2928 - MxChatInstances.ensureSession(botId);
2929 - }
2930 -
2931 - // Deferred email check — only on first widget open
2932 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2933 - var instance = MxChatInstances.get(botId);
2934 - if (emailBlocker && !instance.emailCheckDone) {
2935 - instance.emailCheckDone = true;
2936 - resolveEmailState(botId);
2937 - } else if (!emailBlocker) {
2938 - // No email collection — still route through showChatContainerForBot
2939 - // so the loader is shown while chat history loads
2940 - showChatContainerForBot(botId);
2941 - }
2942 2414 } else {
2943 2415 $chatbot.removeClass('visible').addClass('hidden');
2944 2416 $(this).removeClass('hidden');
2945 2417 enableScroll();
@@ -2957,9 +2429,11 @@
2957 2429
2958 2430 $(document).on('click', '.close-pre-chat-message', function(e) {
2959 2431 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2960 2432 var botId = getBotIdFromElement(this);
2961 - handlePreChatDismissal(botId);
2433 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2434 + $(this).remove();
2435 + });
2962 2436 });
2963 2437
2964 2438
2965 2439 // PDF upload button handlers - use class selector
@@ -3160,59 +2634,8 @@
3160 2634 });
3161 2635
3162 2636
3163 2637 // ====================================
3164 -// INIT LOADER & CHAT CONTAINER HELPERS
3165 -// ====================================
3166 -// These must be outside the email collection block so they're always available
3167 -// (used by persistence loading even when email collection is off)
3168 -
3169 -function showInitLoader(botId) {
3170 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3171 - if (loader) loader.style.display = 'flex';
3172 -}
3173 -
3174 -function hideInitLoader(botId) {
3175 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3176 - if (loader) loader.style.display = 'none';
3177 -}
3178 -
3179 -function showEmailFormForBot(botId) {
3180 - hideInitLoader(botId);
3181 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3182 - var chatContainer = getElementDOM(botId, 'chat-container');
3183 - if (emailBlocker) emailBlocker.style.display = 'flex';
3184 - if (chatContainer) chatContainer.style.display = 'none';
3185 -}
3186 -
3187 -function showChatContainerForBot(botId) {
3188 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3189 - var chatContainer = getElementDOM(botId, 'chat-container');
3190 - if (emailBlocker) emailBlocker.style.display = 'none';
3191 -
3192 - var instance = MxChatInstances.get(botId);
3193 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3194 -
3195 - // If persistence is on and history hasn't loaded yet, show loader
3196 - // while history loads to prevent flash of empty chat
3197 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3198 - if (chatContainer) chatContainer.style.display = 'none';
3199 - showInitLoader(botId);
3200 - loadChatHistory(botId, function() {
3201 - hideInitLoader(botId);
3202 - if (chatContainer) chatContainer.style.display = 'flex';
3203 - scrollToBottom(botId, true);
3204 - });
3205 - } else {
3206 - hideInitLoader(botId);
3207 - if (chatContainer) chatContainer.style.display = 'flex';
3208 - if (typeof loadChatHistory === 'function') {
3209 - loadChatHistory(botId);
3210 - }
3211 - }
3212 -}
3213 -
3214 -// ====================================
3215 2638 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3216 2639 // ====================================
3217 2640 // Only run email collection setup if it's enabled
3218 2641 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3248,8 +2671,28 @@
3248 2671 `;
3249 2672 document.head.appendChild(style);
3250 2673 }
3251 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 +
3252 2695 function isValidEmailAddress(email) {
3253 2696 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3254 2697 return emailRegex.test(email.trim()) && email.length <= 254;
3255 2698 }
@@ -3257,41 +2700,8 @@
3257 2700 function isValidNameInput(name) {
3258 2701 return name && name.trim().length >= 2 && name.trim().length <= 100;
3259 2702 }
3260 2703
3261 - /**
3262 - * Replace {visitor_name} placeholder in intro message with actual visitor name
3263 - * @param {string} botId - The bot instance ID
3264 - * @param {string} visitorName - The visitor's name to insert
3265 - */
3266 - function replaceVisitorNamePlaceholder(botId, visitorName) {
3267 - var chatBox = getElementDOM(botId, 'chat-box');
3268 - if (!chatBox) return;
3269 -
3270 - // Find the first bot message (intro message)
3271 - var introMessage = chatBox.querySelector('.bot-message');
3272 - if (!introMessage) return;
3273 -
3274 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3275 - if (!messageContent) return;
3276 -
3277 - var html = messageContent.innerHTML;
3278 -
3279 - // Replace {visitor_name} placeholder (case-insensitive)
3280 - if (visitorName && visitorName.trim()) {
3281 - // Escape HTML to prevent XSS
3282 - var safeName = $('<div>').text(visitorName.trim()).html();
3283 - html = html.replace(/\{visitor_name\}/gi, safeName);
3284 - } else {
3285 - // Remove placeholder and clean up spacing if no name provided
3286 - html = html.replace(/\{visitor_name\}/gi, '');
3287 - // Clean up any double spaces that might result
3288 - html = html.replace(/\s{2,}/g, ' ').trim();
3289 - }
3290 -
3291 - messageContent.innerHTML = html;
3292 - }
3293 -
3294 2704 function setEmailSubmissionState(botId, loading) {
3295 2705 var submitButton = getElementDOM(botId, 'email-submit-button');
3296 2706 var emailInput = getElementDOM(botId, 'user-email');
3297 2707 var nameInput = getElementDOM(botId, 'user-name');
@@ -3371,31 +2781,11 @@
3371 2781 existingErrors.forEach(error => error.remove());
3372 2782 }
3373 2783 }
3374 2784
3375 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3376 - function resolveEmailState(botId) {
3377 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3378 - if (mxchatChat.initial_email_state.show_email_form) {
3379 - showEmailFormForBot(botId);
3380 - } else {
3381 - showChatContainerForBot(botId);
3382 - }
3383 - } else {
3384 - checkSessionAndEmailForBot(botId);
3385 - }
3386 - }
3387 -
3388 2785 function checkSessionAndEmailForBot(botId) {
3389 - const sessionId = MxChatInstances.ensureSession(botId);
2786 + const sessionId = getChatSession(botId);
3390 2787
3391 - // Hide both panels while we check — show loader instead
3392 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3393 - var chatContainer = getElementDOM(botId, 'chat-container');
3394 - if (emailBlocker) emailBlocker.style.display = 'none';
3395 - if (chatContainer) chatContainer.style.display = 'none';
3396 - showInitLoader(botId);
3397 -
3398 2788 fetch(mxchatChat.ajax_url, {
3399 2789 method: 'POST',
3400 2790 headers: {
3401 2791 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3443,9 +2833,9 @@
3443 2833 var emailInput = getElementDOM(botId, 'user-email');
3444 2834 var nameInput = getElementDOM(botId, 'user-name');
3445 2835 var userEmail = emailInput ? emailInput.value.trim() : '';
3446 2836 var userName = nameInput ? nameInput.value.trim() : '';
3447 - var sessionId = MxChatInstances.ensureSession(botId);
2837 + var sessionId = getChatSession(botId);
3448 2838
3449 2839 // Validate email
3450 2840 if (!userEmail) {
3451 2841 showEmailError(botId, 'Please enter your email address.');
@@ -3496,16 +2886,8 @@
3496 2886
3497 2887 if (data.success) {
3498 2888 showChatContainerForBot(botId);
3499 2889
3500 - // Replace {visitor_name} placeholder in intro message with actual name
3501 - if (userName) {
3502 - replaceVisitorNamePlaceholder(botId, userName);
3503 - } else {
3504 - // Remove placeholder if no name provided
3505 - replaceVisitorNamePlaceholder(botId, '');
3506 - }
3507 -
3508 2890 if (data.message && typeof appendMessage === 'function') {
3509 2891 setTimeout(() => {
3510 2892 appendMessage('bot', data.message, '', [], false, botId);
3511 2893 if (typeof scrollToBottom === 'function') {
@@ -3568,27 +2950,25 @@
3568 2950 }
3569 2951 });
3570 2952
3571 2953 // Initialize email check for all bot instances
3572 - // For floating bots: defer until widget is opened (zero passive AJAX)
3573 - // For embedded bots: check immediately since the form is visible
3574 2954 $('.mxchat-chatbot-wrapper').each(function() {
3575 2955 var botId = $(this).data('bot-id') || 'default';
3576 2956 var emailBlocker = getElementDOM(botId, 'email-blocker');
3577 2957
2958 + // Only check if email blocker exists for this bot
3578 2959 if (emailBlocker) {
3579 - if (isEmbeddedBot(botId)) {
3580 - // Embedded bots are always visible — check now
3581 - resolveEmailState(botId);
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);
3582 2970 }
3583 - // Floating bots: handled in the widget open handler
3584 - } else if (isEmbeddedBot(botId)) {
3585 - // Embedded bot, no email collection — load history with loader
3586 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3587 - if (chatPersistenceEnabled) {
3588 - MxChatInstances.ensureSession(botId);
3589 - showChatContainerForBot(botId);
3590 - }
3591 2971 }
3592 2972 });
3593 2973 }
3594 2974
@@ -3598,32 +2978,39 @@
3598 2978 var $chatbot = getElement(botId, 'floating-chatbot');
3599 2979 if ($chatbot.hasClass('hidden')) {
3600 2980 $chatbot.removeClass('hidden').addClass('visible');
3601 2981 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3602 - handlePreChatDismissal(botId);
2982 + $(this).fadeOut(250); // Hide pre-chat message
3603 2983 disableScroll(); // Disable scroll when chatbot opens
2984 + }
2985 + });
3604 2986
3605 - // Load chat history for returning visitors (persistence)
3606 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3607 - if (chatPersistenceEnabled) {
3608 - MxChatInstances.ensureSession(botId);
3609 - }
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
3610 2993
3611 - // Deferred email check — only on first widget open
3612 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3613 - var instance = MxChatInstances.get(botId);
3614 - if (emailBlocker && !instance.emailCheckDone) {
3615 - instance.emailCheckDone = true;
3616 - resolveEmailState(botId);
3617 - } else if (!emailBlocker) {
3618 - showChatContainerForBot(botId);
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
3619 3008 }
3620 - }
3009 + });
3621 3010 });
3622 3011
3623 - // Legacy duplicate close handler removed — handled by single event delegation above
3624 3012
3625 -
3626 3013 function hasQuickQuestions(botId) {
3627 3014 botId = botId || 'default';
3628 3015 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3629 3016 if (!questionsContainer) return false;
@@ -3759,11 +3146,18 @@
3759 3146 });
3760 3147
3761 3148 // Initialize when document is ready
3762 3149 setFullHeight();
3150 + trackOriginatingPage();
3763 3151
3764 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3765 - // until the user's first interaction via MxChatInstances.ensureSession()
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 + }
3766 3160
3767 3161 // Initialize chat visibility for all instances
3768 3162 $('.mxchat-chatbot-wrapper').each(function() {
3769 3163 var botId = $(this).data('bot-id') || 'default';