PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.6
MxChat – AI Chatbot & Content Generation for WordPress v3.0.6
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 +204 -753 3.2.53.0.6 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
@@ -33,9 +16,9 @@
33 16 var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
34 17
35 18 this.instances[botId] = {
36 19 botId: botId,
37 - sessionId: null,
20 + sessionId: this.getChatSession(botId),
38 21 lastSeenMessageId: '',
39 22 notificationCheckInterval: null,
40 23 pollingInterval: null,
41 24 processedMessageIds: new Set(),
@@ -60,77 +43,23 @@
60 43 return Object.keys(this.instances);
61 44 },
62 45
63 46 // 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 47 getChatSession: function(botId) {
67 48 var cookieName = 'mxchat_session_id_' + botId;
68 - var storageKey = 'mxchat_session_id_' + botId;
69 49 var sessionId = getCookie(cookieName);
70 50
71 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
72 51 if (!sessionId) {
73 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
52 + sessionId = generateSessionId();
53 + this.setChatSession(botId, sessionId);
74 54 }
75 55
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;
56 + return sessionId;
95 57 },
96 58
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 59 setChatSession: function(botId, sessionId) {
129 60 var cookieName = 'mxchat_session_id_' + botId;
130 - var storageKey = 'mxchat_session_id_' + botId;
131 61 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
132 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
133 62 if (this.instances[botId]) {
134 63 this.instances[botId].sessionId = sessionId;
135 64 }
136 65 },
@@ -135,10 +64,8 @@
135 64 }
136 65 },
137 66
138 67 resetChatSession: function(botId) {
139 - // Clear old session from localStorage before setting new one
140 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
141 68 var newSessionId = generateSessionId();
142 69 this.setChatSession(botId, newSessionId);
143 70 var $chatBox = getElement(botId, 'chat-box');
144 71 if ($chatBox.length) {
@@ -147,20 +74,8 @@
147 74 if (this.instances[botId]) {
148 75 this.instances[botId].chatHistoryLoaded = false;
149 76 this.instances[botId].processedMessageIds = new Set();
150 77 }
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 78 }
164 79 };
165 80
166 81 // ====================================
@@ -465,9 +380,8 @@
465 380
466 381 // Update your existing sendMessage function
467 382 function sendMessage(botId) {
468 383 botId = botId || 'default';
469 - MxChatInstances.ensureSession(botId);
470 384 var $chatInput = getElement(botId, 'chat-input');
471 385 var message = $chatInput.val();
472 386
473 387 // ADD PROMPT HOOK HERE
@@ -475,14 +389,10 @@
475 389 message = customMxChatFilter(message, "prompt");
476 390 }
477 391
478 392 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 - }
393 + // Disable input while waiting for response
394 + disableChatInput(botId);
485 395
486 396 appendMessage("user", message, '', [], false, botId);
487 397 $chatInput.val('');
488 398 $chatInput.css('height', 'auto');
@@ -510,9 +420,8 @@
510 420
511 421 // Update your existing sendMessageToChatbot function
512 422 function sendMessageToChatbot(message, botId) {
513 423 botId = botId || 'default';
514 - MxChatInstances.ensureSession(botId);
515 424
516 425 // ADD PROMPT HOOK HERE
517 426 if (typeof customMxChatFilter === 'function') {
518 427 message = customMxChatFilter(message, "prompt");
@@ -517,14 +426,10 @@
517 426 if (typeof customMxChatFilter === 'function') {
518 427 message = customMxChatFilter(message, "prompt");
519 428 }
520 429
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 - }
430 + // Disable input while waiting for response
431 + disableChatInput(botId);
527 432
528 433 var sessionId = getChatSession(botId);
529 434
530 435 if (hasQuickQuestions(botId)) {
@@ -612,23 +517,13 @@
612 517
613 518 // Get instance for session start timestamp (used when persistence is OFF)
614 519 var instance = MxChatInstances.get(botId);
615 520
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 521 // Prepare AJAX data
627 522 const ajaxData = {
628 523 action: 'mxchat_handle_chat_request',
629 524 message: message,
630 - session_id: sessionId,
525 + session_id: getChatSession(botId),
631 526 nonce: mxchatChat.nonce,
632 527 current_page_url: window.location.href,
633 528 current_page_title: document.title,
634 529 bot_id: botId,
@@ -690,16 +585,23 @@
690 585 errorMessage = "An error occurred. Please try again or contact support.";
691 586 }
692 587
693 588 // Handle session reset action (IP changed, session expired, etc.)
694 - // Silent reset — keep chat UI intact, just get a new session and retry
695 589 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)
590 + // Clear the old session and generate a new one
591 + resetChatSession(botId);
592 + // Remove the temporary loading message
593 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
594 + // Re-send the original message with the new session
698 595 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
699 596 if (originalMessage) {
700 597 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
701 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
598 + // Re-add the user message and thinking indicator
599 + appendMessage("user", originalMessage, '', [], false, botId);
600 + appendThinkingMessage(botId);
601 + scrollToBottom(botId);
602 + // Determine whether to use streaming
603 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
702 604 if (shouldUseStreaming(currentModel)) {
703 605 callMxChatStream(originalMessage, function(response) {
704 606 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
705 607 }, botId);
@@ -756,11 +658,9 @@
756 658 }
757 659
758 660 // Check for live agent response
759 661 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
760 - removeThinkingDots(botId);
761 662 updateChatModeIndicator('agent', botId);
762 - enableChatInput(botId);
763 663 return;
764 664 }
765 665
766 666 // Handle the message and show notification if chat is hidden
@@ -793,13 +693,9 @@
793 693 $badge.show();
794 694 }
795 695 }
796 696 } 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);
697 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
802 698 }
803 699
804 700 if (response.message_id) {
805 701 var instance = MxChatInstances.get(botId);
@@ -861,22 +757,12 @@
861 757
862 758 // Get instance for session start timestamp (used when persistence is OFF)
863 759 var instance = MxChatInstances.get(botId);
864 760
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 761 const formData = new FormData();
876 762 formData.append('action', 'mxchat_stream_chat');
877 763 formData.append('message', message);
878 - formData.append('session_id', streamSessionId);
764 + formData.append('session_id', getChatSession(botId));
879 765 formData.append('nonce', mxchatChat.nonce);
880 766 formData.append('current_page_url', window.location.href);
881 767 formData.append('current_page_title', document.title);
882 768 formData.append('bot_id', botId);
@@ -976,16 +862,8 @@
976 862
977 863 // Re-enable chat input when stream ends with content
978 864 enableChatInput(botId);
979 865
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 866 if (callback) {
989 867 callback(accumulatedContent);
990 868 }
991 869 return;
@@ -1008,16 +886,8 @@
1008 886
1009 887 // Re-enable chat input after streaming completes
1010 888 enableChatInput(botId);
1011 889
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 890 if (callback) {
1021 891 callback(accumulatedContent);
1022 892 }
1023 893 return;
@@ -1136,16 +1006,21 @@
1136 1006 errorMessage = "An error occurred. Please try again or contact support.";
1137 1007 }
1138 1008
1139 1009 // Handle session reset action (IP changed, session expired, etc.)
1140 - // Silent reset — keep chat UI intact, just get a new session and retry
1141 1010 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)
1011 + // Clear the old session and generate a new one
1012 + resetChatSession(botId);
1013 + // Re-send the original message with the new session
1144 1014 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1145 1015 if (originalMessage) {
1146 1016 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1147 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1017 + // Re-add the user message and thinking indicator
1018 + appendMessage("user", originalMessage, '', [], false, botId);
1019 + appendThinkingMessage(botId);
1020 + scrollToBottom(botId);
1021 + // Determine whether to use streaming
1022 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1148 1023 if (shouldUseStreaming(currentModel)) {
1149 1024 callMxChatStream(originalMessage, callback, botId);
1150 1025 } else {
1151 1026 callMxChat(originalMessage, callback, botId);
@@ -1167,22 +1042,8 @@
1167 1042 }
1168 1043 return; // Exit early for errors
1169 1044 }
1170 1045
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 1046 // Handle different response formats
1186 1047 if (data.text || data.html || data.message) {
1187 1048
1188 1049 // Apply response hooks
@@ -1227,15 +1088,19 @@
1227 1088 }
1228 1089
1229 1090 // Enhanced updateChatModeIndicator function for immediate DOM updates
1230 1091 function updateChatModeIndicator(mode, botId) {
1092 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1231 1093 botId = botId || 'default';
1232 1094 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1095 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1233 1096 if (indicator) {
1234 1097 const oldText = indicator.textContent;
1098 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1235 1099
1236 1100 if (mode === 'agent') {
1237 1101 indicator.textContent = 'Live Agent';
1102 + console.log('[MxChat] Mode is agent, calling startPolling...');
1238 1103 startPolling(botId);
1239 1104 } else {
1240 1105 // Everything else is AI mode
1241 1106 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1306,12 +1171,9 @@
1306 1171 // Update the event handlers to use the correct function names (using event delegation)
1307 1172 // Use class-based selectors for multi-instance support
1308 1173 $(document).on('click', '.send-button', function() {
1309 1174 var botId = getBotIdFromElement(this);
1310 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1311 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1312 - disableChatInput(botId);
1313 - }
1175 + disableChatInput(botId);
1314 1176 sendMessage(botId);
1315 1177 });
1316 1178
1317 1179 // Override enter key handler (using event delegation)
@@ -1318,256 +1180,14 @@
1318 1180 $(document).on('keypress', '.chat-input', function(e) {
1319 1181 if (e.which == 13 && !e.shiftKey) {
1320 1182 e.preventDefault();
1321 1183 var botId = getBotIdFromElement(this);
1322 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1323 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1324 - disableChatInput(botId);
1325 - }
1184 + disableChatInput(botId);
1326 1185 sendMessage(botId);
1327 1186 }
1328 1187 });
1329 1188
1330 1189
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 1190 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1571 1191 try {
1572 1192 // Determine styles based on sender type
1573 1193 let messageClass, bgColor, fontColor;
@@ -1605,12 +1225,17 @@
1605 1225 'margin-bottom': '1em'
1606 1226 });
1607 1227 }
1608 1228
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);
1229 + // Process the message content based on sender
1230 + let fullMessage;
1231 + if (sender === "user") {
1232 + // For user messages, apply linkify after sanitization
1233 + fullMessage = linkify(messageText);
1234 + } else {
1235 + // For bot/agent messages, preserve HTML
1236 + fullMessage = messageText;
1237 + }
1613 1238
1614 1239 // Add images if provided
1615 1240 if (images && images.length > 0) {
1616 1241 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1659,12 +1284,8 @@
1659 1284 if (lastUserMessage.length) {
1660 1285 scrollElementToTop(lastUserMessage, botId);
1661 1286 }
1662 1287 }
1663 -
1664 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1665 - mxchatEnsurePrintRoot(botId);
1666 - }
1667 1288 });
1668 1289
1669 1290 if (messageText.id) {
1670 1291 var instance = MxChatInstances.get(botId);
@@ -1749,12 +1370,26 @@
1749 1370 bgColor = botMessageBgColor;
1750 1371 fontColor = botMessageFontColor;
1751 1372 }
1752 1373
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);
1374 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1375 + // This prevents double-processing of URLs that are already formatted as HTML
1376 + var fullMessage;
1377 + if (sender === "user") {
1378 + // Always linkify user messages (they're plain text)
1379 + fullMessage = linkify(responseText);
1380 + } else {
1381 + // For bot/agent messages, check if HTML already exists
1382 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1383 + responseText.includes('<img') || responseText.includes('<div') ||
1384 + responseText.includes('<p>') || responseText.includes('<br>')) {
1385 + // Response already has HTML, don't process it
1386 + fullMessage = responseText;
1387 + } else {
1388 + // Plain text response, apply linkify
1389 + fullMessage = linkify(responseText);
1390 + }
1391 + }
1757 1392
1758 1393 if (responseHtml) {
1759 1394 // Only add line breaks if there's actual text content before the HTML
1760 1395 if (fullMessage && fullMessage.trim()) {
@@ -1811,12 +1446,8 @@
1811 1446 }
1812 1447
1813 1448 // Re-enable chat input after response is displayed
1814 1449 enableChatInput(botId);
1815 -
1816 - if (sender === "bot" || sender === "agent") {
1817 - mxchatEnsurePrintRoot(botId);
1818 - }
1819 1450 } else {
1820 1451 appendMessage(sender, responseText, responseHtml, images, false, botId);
1821 1452 // Re-enable chat input after response is displayed
1822 1453 enableChatInput(botId);
@@ -1825,15 +1456,8 @@
1825 1456
1826 1457
1827 1458 function appendThinkingMessage(botId) {
1828 1459 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 1460 var $chatBox = getElement(botId, 'chat-box');
1837 1461
1838 1462 // Remove any existing thinking dots in this bot's chat first
1839 1463 $chatBox.find('.thinking-dots').remove();
@@ -1855,9 +1479,9 @@
1855 1479 '</div>' +
1856 1480 '</div>';
1857 1481
1858 1482 // 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 + ';"';
1483 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
1860 1484 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1861 1485 scrollToBottom(botId);
1862 1486 }
1863 1487
@@ -1863,11 +1487,9 @@
1863 1487
1864 1488 function removeThinkingDots(botId) {
1865 1489 botId = botId || 'default';
1866 1490 var $chatBox = getElement(botId, 'chat-box');
1867 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1868 1491 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1869 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1870 1492 }
1871 1493
1872 1494 // ====================================
1873 1495 // TEXT FORMATTING & PROCESSING
@@ -1901,12 +1523,9 @@
1901 1523 processedText = formatTextStyling(processedText);
1902 1524
1903 1525 // Process code blocks BEFORE processing links
1904 1526 processedText = formatCodeBlocks(processedText);
1905 -
1906 - // Process markdown tables BEFORE converting newlines to paragraphs
1907 - processedText = formatMarkdownTables(processedText);
1908 -
1527 +
1909 1528 // NOW convert to paragraphs
1910 1529 processedText = convertNewlinesToBreaks(processedText);
1911 1530
1912 1531 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1919,63 +1538,37 @@
1919 1538 // Return as a proper link without the brackets
1920 1539 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1921 1540 });
1922 1541
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 - }
1542 + // Process proper markdown links with text: [text](url)
1543 + // This MUST have non-empty text in the first brackets
1544 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1545 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1546 + // Make sure we have actual text (not just whitespace)
1547 + if (!text || !text.trim()) {
1548 + // If no text, treat the URL as the text
1549 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1550 + const safeUrl = safeEncodeUrl(cleanUrl);
1551 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1974 1552 }
1975 - return result;
1976 - })(processedText);
1553 +
1554 + // Clean the URL
1555 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1556 + const safeUrl = safeEncodeUrl(cleanUrl);
1557 + const safeText = sanitizeUserInput(text);
1558 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1559 + });
1977 1560
1561 + // Handle empty markdown links: [](url)
1562 + // This is a specific case where there's no text
1563 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1564 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1565 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1566 + const safeUrl = safeEncodeUrl(cleanUrl);
1567 + // Use the URL itself as the link text
1568 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1569 + });
1570 +
1978 1571 // Process phone numbers: [text](tel:number)
1979 1572 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1980 1573 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1981 1574 const safePhone = safeEncodeUrl(phone);
@@ -2127,78 +1720,9 @@
2127 1720 });
2128 1721
2129 1722 return text;
2130 1723 }
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 -
1724 +
2201 1725 function sanitizeUserInput(text) {
2202 1726 const div = document.createElement('div');
2203 1727 div.textContent = text;
2204 1728 return div.innerHTML;
@@ -2269,14 +1793,13 @@
2269 1793 requestAnimationFrame(smoothScroll);
2270 1794 }
2271 1795 }
2272 1796
2273 - function scrollElementToTop(element, botId, topOffset) {
1797 + function scrollElementToTop(element, botId) {
2274 1798 botId = botId || 'default';
2275 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2276 1799 var chatBox = getElement(botId, 'chat-box');
2277 1800 var elementTop = element.position().top + chatBox.scrollTop();
2278 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1801 + chatBox.animate({ scrollTop: elementTop }, 500);
2279 1802 }
2280 1803
2281 1804 function showChatWidget(botId) {
2282 1805 botId = botId || 'default';
@@ -2420,12 +1943,15 @@
2420 1943 // LIVE AGENT FUNCTIONALITY
2421 1944 // ====================================
2422 1945
2423 1946 function startPolling(botId) {
1947 + console.log('[MxChat] startPolling called for botId:', botId);
2424 1948 botId = botId || 'default';
2425 1949 var instance = MxChatInstances.get(botId);
2426 1950 // Clear any existing interval first
2427 1951 stopPolling(botId);
1952 + // Start new polling interval
1953 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2428 1954 instance.pollingInterval = setInterval(function() {
2429 1955 checkForAgentMessages(botId);
2430 1956 }, 5000);
2431 1957 }
@@ -2430,17 +1956,20 @@
2430 1956 }, 5000);
2431 1957 }
2432 1958
2433 1959 function stopPolling(botId) {
1960 + console.log('[MxChat] stopPolling called for botId:', botId);
2434 1961 botId = botId || 'default';
2435 1962 var instance = MxChatInstances.get(botId);
2436 1963 if (instance.pollingInterval) {
2437 1964 clearInterval(instance.pollingInterval);
2438 1965 instance.pollingInterval = null;
1966 + console.log('[MxChat] Polling stopped for botId:', botId);
2439 1967 }
2440 1968 }
2441 1969
2442 1970 function checkForAgentMessages(botId) {
1971 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2443 1972 botId = botId || 'default';
2444 1973 var instance = MxChatInstances.get(botId);
2445 1974 const sessionId = getChatSession(botId);
2446 1975 $.ajax({
@@ -2466,12 +1995,8 @@
2466 1995 instance.processedMessageIds.add(message.id);
2467 1996 }
2468 1997 });
2469 1998
2470 - if (hasNewMessage) {
2471 - enableChatInput(botId);
2472 - }
2473 -
2474 1999 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2475 2000 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2476 2001 showNotification(botId);
2477 2002 }
@@ -2477,13 +2002,8 @@
2477 2002 }
2478 2003
2479 2004 scrollToBottom(botId, true);
2480 2005 }
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 2006 },
2487 2007 error: function (xhr, status, error) {
2488 2008 // Polling error - silently continue
2489 2009 }
@@ -2493,29 +2013,20 @@
2493 2013 // ====================================
2494 2014 // CHAT HISTORY & PERSISTENCE
2495 2015 // ====================================
2496 2016
2497 -function loadChatHistory(botId, onComplete) {
2017 +function loadChatHistory(botId) {
2498 2018 botId = botId || 'default';
2499 2019 var instance = MxChatInstances.get(botId);
2500 2020
2501 2021 // Prevent duplicate loading
2502 2022 if (instance.chatHistoryLoaded) {
2503 - if (onComplete) onComplete();
2504 2023 return;
2505 2024 }
2506 2025
2507 - // Use getChatSession which returns null if no session exists (does NOT create one)
2508 2026 var sessionId = getChatSession(botId);
2509 2027 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2510 2028
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 2029 if (chatPersistenceEnabled && sessionId) {
2519 2030 $.ajax({
2520 2031 url: mxchatChat.ajax_url,
2521 2032 type: 'POST',
@@ -2526,12 +2037,11 @@
2526 2037 },
2527 2038 success: function(response) {
2528 2039 // Handle session reset (IP changed while user was away)
2529 2040 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);
2041 + // Silently reset session - user will start fresh
2042 + resetChatSession(botId);
2532 2043 instance.chatHistoryLoaded = true; // Prevent retry loop
2533 - if (onComplete) onComplete();
2534 2044 return;
2535 2045 }
2536 2046
2537 2047 // Check if the response indicates success
@@ -2587,19 +2097,9 @@
2587 2097 var content = message.content;
2588 2098 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2589 2099 content = decodeHTMLEntities(content);
2590 2100
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")) {
2101 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2602 2102 messageElement.html(content);
2603 2103 } else {
2604 2104 var formattedContent = linkify(content);
2605 2105 messageElement.html(formattedContent);
@@ -2639,17 +2139,13 @@
2639 2139 instance.chatHistoryLoaded = true;
2640 2140 }
2641 2141 }
2642 2142 }
2643 - if (onComplete) onComplete();
2644 2143 },
2645 2144 error: function(xhr, status, error) {
2646 2145 // Error loading chat history - silently continue
2647 - if (onComplete) onComplete();
2648 2146 }
2649 2147 });
2650 - } else {
2651 - if (onComplete) onComplete();
2652 2148 }
2653 2149 }
2654 2150
2655 2151
@@ -2825,35 +2321,45 @@
2825 2321 // ====================================
2826 2322
2827 2323 function checkPreChatDismissal(botId) {
2828 2324 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) {
2325 + $.ajax({
2326 + url: mxchatChat.ajax_url,
2327 + type: 'POST',
2328 + data: {
2329 + action: 'mxchat_check_pre_chat_message_status',
2330 + _ajax_nonce: mxchatChat.nonce
2331 + },
2332 + success: function(response) {
2333 + if (response.success && !response.data.dismissed) {
2334 + getElement(botId, 'pre-chat-message').fadeIn(250);
2335 + } else {
2835 2336 getElement(botId, 'pre-chat-message').hide();
2836 - return;
2837 2337 }
2838 - // Expired — clear and show again
2839 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2338 + },
2339 + error: function() {
2340 + // Error checking pre-chat dismissal - silently continue
2840 2341 }
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 - }
2342 + });
2846 2343 }
2847 2344
2848 2345 function handlePreChatDismissal(botId) {
2849 2346 botId = botId || 'default';
2850 2347 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 - }
2348 + $.ajax({
2349 + url: mxchatChat.ajax_url,
2350 + type: 'POST',
2351 + data: {
2352 + action: 'mxchat_dismiss_pre_chat_message',
2353 + _ajax_nonce: mxchatChat.nonce
2354 + },
2355 + success: function() {
2356 + $('#pre-chat-message').hide();
2357 + },
2358 + error: function() {
2359 + // Error dismissing pre-chat message - silently continue
2360 + }
2361 + });
2856 2362 }
2857 2363
2858 2364
2859 2365 // ====================================
@@ -2920,26 +2426,8 @@
2920 2426 $(this).addClass('hidden');
2921 2427 $badge.hide(); // Hide notification when opening chat
2922 2428 disableScroll();
2923 2429 $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 2430 } else {
2943 2431 $chatbot.removeClass('visible').addClass('hidden');
2944 2432 $(this).removeClass('hidden');
2945 2433 enableScroll();
@@ -2957,9 +2445,11 @@
2957 2445
2958 2446 $(document).on('click', '.close-pre-chat-message', function(e) {
2959 2447 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2960 2448 var botId = getBotIdFromElement(this);
2961 - handlePreChatDismissal(botId);
2449 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2450 + $(this).remove();
2451 + });
2962 2452 });
2963 2453
2964 2454
2965 2455 // PDF upload button handlers - use class selector
@@ -3160,59 +2650,8 @@
3160 2650 });
3161 2651
3162 2652
3163 2653 // ====================================
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 2654 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3216 2655 // ====================================
3217 2656 // Only run email collection setup if it's enabled
3218 2657 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3248,8 +2687,28 @@
3248 2687 `;
3249 2688 document.head.appendChild(style);
3250 2689 }
3251 2690
2691 + // Helper functions for email collection (multi-instance aware)
2692 + function showEmailFormForBot(botId) {
2693 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2694 + var chatContainer = getElementDOM(botId, 'chat-container');
2695 + if (emailBlocker) emailBlocker.style.display = 'flex';
2696 + if (chatContainer) chatContainer.style.display = 'none';
2697 + }
2698 +
2699 + function showChatContainerForBot(botId) {
2700 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2701 + var chatContainer = getElementDOM(botId, 'chat-container');
2702 + if (emailBlocker) emailBlocker.style.display = 'none';
2703 + if (chatContainer) chatContainer.style.display = 'flex';
2704 +
2705 + // Load chat history for this bot
2706 + if (typeof loadChatHistory === 'function') {
2707 + loadChatHistory(botId);
2708 + }
2709 + }
2710 +
3252 2711 function isValidEmailAddress(email) {
3253 2712 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3254 2713 return emailRegex.test(email.trim()) && email.length <= 254;
3255 2714 }
@@ -3371,31 +2830,11 @@
3371 2830 existingErrors.forEach(error => error.remove());
3372 2831 }
3373 2832 }
3374 2833
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 2834 function checkSessionAndEmailForBot(botId) {
3389 - const sessionId = MxChatInstances.ensureSession(botId);
2835 + const sessionId = getChatSession(botId);
3390 2836
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 2837 fetch(mxchatChat.ajax_url, {
3399 2838 method: 'POST',
3400 2839 headers: {
3401 2840 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3443,9 +2882,9 @@
3443 2882 var emailInput = getElementDOM(botId, 'user-email');
3444 2883 var nameInput = getElementDOM(botId, 'user-name');
3445 2884 var userEmail = emailInput ? emailInput.value.trim() : '';
3446 2885 var userName = nameInput ? nameInput.value.trim() : '';
3447 - var sessionId = MxChatInstances.ensureSession(botId);
2886 + var sessionId = getChatSession(botId);
3448 2887
3449 2888 // Validate email
3450 2889 if (!userEmail) {
3451 2890 showEmailError(botId, 'Please enter your email address.');
@@ -3568,27 +3007,25 @@
3568 3007 }
3569 3008 });
3570 3009
3571 3010 // 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 3011 $('.mxchat-chatbot-wrapper').each(function() {
3575 3012 var botId = $(this).data('bot-id') || 'default';
3576 3013 var emailBlocker = getElementDOM(botId, 'email-blocker');
3577 3014
3015 + // Only check if email blocker exists for this bot
3578 3016 if (emailBlocker) {
3579 - if (isEmbeddedBot(botId)) {
3580 - // Embedded bots are always visible — check now
3581 - resolveEmailState(botId);
3017 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3018 + if (mxchatChat.initial_email_state.show_email_form) {
3019 + showEmailFormForBot(botId);
3020 + } else {
3021 + showChatContainerForBot(botId);
3022 + }
3023 + } else {
3024 + setTimeout(function() {
3025 + checkSessionAndEmailForBot(botId);
3026 + }, 100);
3582 3027 }
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 3028 }
3592 3029 });
3593 3030 }
3594 3031
@@ -3598,32 +3035,39 @@
3598 3035 var $chatbot = getElement(botId, 'floating-chatbot');
3599 3036 if ($chatbot.hasClass('hidden')) {
3600 3037 $chatbot.removeClass('hidden').addClass('visible');
3601 3038 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3602 - handlePreChatDismissal(botId);
3039 + $(this).fadeOut(250); // Hide pre-chat message
3603 3040 disableScroll(); // Disable scroll when chatbot opens
3041 + }
3042 + });
3604 3043
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 - }
3044 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
3045 + // This is a fallback for legacy support
3046 + $(document).on('click', '.close-pre-chat-message', function() {
3047 + var botId = getBotIdFromElement(this);
3048 + var $preChat = getElement(botId, 'pre-chat-message');
3049 + $preChat.fadeOut(200); // Hide the message
3610 3050
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);
3051 + // Send an AJAX request to set the transient flag for 24 hours
3052 + $.ajax({
3053 + url: mxchatChat.ajax_url,
3054 + type: 'POST',
3055 + data: {
3056 + action: 'mxchat_dismiss_pre_chat_message',
3057 + _ajax_nonce: mxchatChat.nonce
3058 + },
3059 + success: function() {
3060 + // Ensure the message is hidden after dismissal
3061 + $preChat.hide();
3062 + },
3063 + error: function() {
3064 + // Error dismissing pre-chat message - silently continue
3619 3065 }
3620 - }
3066 + });
3621 3067 });
3622 3068
3623 - // Legacy duplicate close handler removed — handled by single event delegation above
3624 3069
3625 -
3626 3070 function hasQuickQuestions(botId) {
3627 3071 botId = botId || 'default';
3628 3072 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3629 3073 if (!questionsContainer) return false;
@@ -3759,11 +3203,18 @@
3759 3203 });
3760 3204
3761 3205 // Initialize when document is ready
3762 3206 setFullHeight();
3207 + trackOriginatingPage();
3763 3208
3764 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3765 - // until the user's first interaction via MxChatInstances.ensureSession()
3209 + // Only load chat history if email collection is disabled
3210 + if (mxchatChat.email_collection_enabled !== 'on') {
3211 + // Load history for all instances
3212 + $('.mxchat-chatbot-wrapper').each(function() {
3213 + var botId = $(this).data('bot-id') || 'default';
3214 + loadChatHistory(botId);
3215 + });
3216 + }
3766 3217
3767 3218 // Initialize chat visibility for all instances
3768 3219 $('.mxchat-chatbot-wrapper').each(function() {
3769 3220 var botId = $(this).data('bot-id') || 'default';