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 -569 3.2.43.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,12 +1164,9 @@
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
@@ -1366,12 +1209,17 @@
1366 1209 'margin-bottom': '1em'
1367 1210 });
1368 1211 }
1369 1212
1370 - // Process the message content - always run linkify to convert markdown
1371 - // links and format text. linkify() handles existing HTML safely via
1372 - // negative lookaheads that skip URLs already inside <a> tags.
1373 - 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 + }
1374 1222
1375 1223 // Add images if provided
1376 1224 if (images && images.length > 0) {
1377 1225 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1506,12 +1354,26 @@
1506 1354 bgColor = botMessageBgColor;
1507 1355 fontColor = botMessageFontColor;
1508 1356 }
1509 1357
1510 - // Always run linkify to convert markdown links and format text.
1511 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1512 - // to avoid double-processing URLs that are already inside <a> tags).
1513 - 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 + }
1514 1376
1515 1377 if (responseHtml) {
1516 1378 // Only add line breaks if there's actual text content before the HTML
1517 1379 if (fullMessage && fullMessage.trim()) {
@@ -1578,15 +1440,8 @@
1578 1440
1579 1441
1580 1442 function appendThinkingMessage(botId) {
1581 1443 botId = botId || 'default';
1582 -
1583 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
1584 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
1585 - if (indicator && indicator.textContent === 'Live Agent') {
1586 - return;
1587 - }
1588 -
1589 1444 var $chatBox = getElement(botId, 'chat-box');
1590 1445
1591 1446 // Remove any existing thinking dots in this bot's chat first
1592 1447 $chatBox.find('.thinking-dots').remove();
@@ -1608,9 +1463,9 @@
1608 1463 '</div>' +
1609 1464 '</div>';
1610 1465
1611 1466 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1612 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1467 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
1613 1468 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1614 1469 scrollToBottom(botId);
1615 1470 }
1616 1471
@@ -1616,11 +1471,9 @@
1616 1471
1617 1472 function removeThinkingDots(botId) {
1618 1473 botId = botId || 'default';
1619 1474 var $chatBox = getElement(botId, 'chat-box');
1620 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1621 1475 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1622 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1623 1476 }
1624 1477
1625 1478 // ====================================
1626 1479 // TEXT FORMATTING & PROCESSING
@@ -1654,12 +1507,9 @@
1654 1507 processedText = formatTextStyling(processedText);
1655 1508
1656 1509 // Process code blocks BEFORE processing links
1657 1510 processedText = formatCodeBlocks(processedText);
1658 -
1659 - // Process markdown tables BEFORE converting newlines to paragraphs
1660 - processedText = formatMarkdownTables(processedText);
1661 -
1511 +
1662 1512 // NOW convert to paragraphs
1663 1513 processedText = convertNewlinesToBreaks(processedText);
1664 1514
1665 1515 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1672,63 +1522,37 @@
1672 1522 // Return as a proper link without the brackets
1673 1523 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1674 1524 });
1675 1525
1676 - // Process markdown links: [text](url) and [](url)
1677 - // Uses balanced parenthesis matching to handle URLs containing parens
1678 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
1679 - processedText = (function(input) {
1680 - var result = '';
1681 - var i = 0;
1682 - while (i < input.length) {
1683 - // Look for [ at current position
1684 - if (input[i] === '[') {
1685 - // Find closing ]
1686 - var closeBracket = input.indexOf(']', i + 1);
1687 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
1688 - result += input[i];
1689 - i++;
1690 - continue;
1691 - }
1692 - var linkText = input.substring(i + 1, closeBracket);
1693 - // Check if URL starts with http
1694 - var urlStart = closeBracket + 2;
1695 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
1696 - result += input[i];
1697 - i++;
1698 - continue;
1699 - }
1700 - // Find balanced closing paren
1701 - var depth = 1;
1702 - var j = urlStart;
1703 - while (j < input.length && depth > 0) {
1704 - if (input[j] === '(') depth++;
1705 - else if (input[j] === ')') depth--;
1706 - if (depth > 0) j++;
1707 - }
1708 - if (depth !== 0) {
1709 - result += input[i];
1710 - i++;
1711 - continue;
1712 - }
1713 - var url = input.substring(urlStart, j);
1714 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
1715 - var encodedUrl = safeEncodeUrl(cleanUrl);
1716 - if (!linkText || !linkText.trim()) {
1717 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
1718 - } else {
1719 - var safeText = sanitizeUserInput(linkText);
1720 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
1721 - }
1722 - i = j + 1; // Skip past the closing )
1723 - } else {
1724 - result += input[i];
1725 - i++;
1726 - }
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>`;
1727 1536 }
1728 - return result;
1729 - })(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 + });
1730 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 +
1731 1555 // Process phone numbers: [text](tel:number)
1732 1556 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1733 1557 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1734 1558 const safePhone = safeEncodeUrl(phone);
@@ -1880,78 +1704,9 @@
1880 1704 });
1881 1705
1882 1706 return text;
1883 1707 }
1884 -
1885 - function formatMarkdownTables(text) {
1886 - var lines = text.split('\n');
1887 - var result = [];
1888 - var i = 0;
1889 -
1890 - while (i < lines.length) {
1891 - // Check for a table: current line has pipes AND next line is a separator row
1892 - if (i + 1 < lines.length &&
1893 - lines[i].indexOf('|') !== -1 &&
1894 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
1895 -
1896 - var tableLines = [];
1897 - var headerLine = lines[i];
1898 - var separatorLine = lines[i + 1];
1899 - tableLines.push(headerLine);
1900 - tableLines.push(separatorLine);
1901 -
1902 - // Collect remaining table rows
1903 - var j = i + 2;
1904 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
1905 - tableLines.push(lines[j]);
1906 - j++;
1907 - }
1908 -
1909 - // Parse alignment from separator row
1910 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
1911 - var alignments = sepCells.map(function(cell) {
1912 - var trimmed = cell.trim();
1913 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
1914 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
1915 - return 'left';
1916 - });
1917 -
1918 - // Build HTML table
1919 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
1920 -
1921 - // Header row
1922 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
1923 - html += '<thead><tr>';
1924 - headerCells.forEach(function(cell, idx) {
1925 - var align = alignments[idx] || 'left';
1926 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
1927 - });
1928 - html += '</tr></thead>';
1929 -
1930 - // Body rows
1931 - html += '<tbody>';
1932 - for (var r = 2; r < tableLines.length; r++) {
1933 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
1934 - html += '<tr>';
1935 - rowCells.forEach(function(cell, idx) {
1936 - var align = alignments[idx] || 'left';
1937 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
1938 - });
1939 - html += '</tr>';
1940 - }
1941 - html += '</tbody></table></div>';
1942 -
1943 - result.push(html);
1944 - i = j;
1945 - } else {
1946 - result.push(lines[i]);
1947 - i++;
1948 - }
1949 - }
1950 -
1951 - return result.join('\n');
1952 - }
1953 -
1708 +
1954 1709 function sanitizeUserInput(text) {
1955 1710 const div = document.createElement('div');
1956 1711 div.textContent = text;
1957 1712 return div.innerHTML;
@@ -2022,14 +1777,13 @@
2022 1777 requestAnimationFrame(smoothScroll);
2023 1778 }
2024 1779 }
2025 1780
2026 - function scrollElementToTop(element, botId, topOffset) {
1781 + function scrollElementToTop(element, botId) {
2027 1782 botId = botId || 'default';
2028 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2029 1783 var chatBox = getElement(botId, 'chat-box');
2030 1784 var elementTop = element.position().top + chatBox.scrollTop();
2031 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1785 + chatBox.animate({ scrollTop: elementTop }, 500);
2032 1786 }
2033 1787
2034 1788 function showChatWidget(botId) {
2035 1789 botId = botId || 'default';
@@ -2173,12 +1927,15 @@
2173 1927 // LIVE AGENT FUNCTIONALITY
2174 1928 // ====================================
2175 1929
2176 1930 function startPolling(botId) {
1931 + console.log('[MxChat] startPolling called for botId:', botId);
2177 1932 botId = botId || 'default';
2178 1933 var instance = MxChatInstances.get(botId);
2179 1934 // Clear any existing interval first
2180 1935 stopPolling(botId);
1936 + // Start new polling interval
1937 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2181 1938 instance.pollingInterval = setInterval(function() {
2182 1939 checkForAgentMessages(botId);
2183 1940 }, 5000);
2184 1941 }
@@ -2183,17 +1940,20 @@
2183 1940 }, 5000);
2184 1941 }
2185 1942
2186 1943 function stopPolling(botId) {
1944 + console.log('[MxChat] stopPolling called for botId:', botId);
2187 1945 botId = botId || 'default';
2188 1946 var instance = MxChatInstances.get(botId);
2189 1947 if (instance.pollingInterval) {
2190 1948 clearInterval(instance.pollingInterval);
2191 1949 instance.pollingInterval = null;
1950 + console.log('[MxChat] Polling stopped for botId:', botId);
2192 1951 }
2193 1952 }
2194 1953
2195 1954 function checkForAgentMessages(botId) {
1955 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2196 1956 botId = botId || 'default';
2197 1957 var instance = MxChatInstances.get(botId);
2198 1958 const sessionId = getChatSession(botId);
2199 1959 $.ajax({
@@ -2219,12 +1979,8 @@
2219 1979 instance.processedMessageIds.add(message.id);
2220 1980 }
2221 1981 });
2222 1982
2223 - if (hasNewMessage) {
2224 - enableChatInput(botId);
2225 - }
2226 -
2227 1983 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2228 1984 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2229 1985 showNotification(botId);
2230 1986 }
@@ -2230,13 +1986,8 @@
2230 1986 }
2231 1987
2232 1988 scrollToBottom(botId, true);
2233 1989 }
2234 -
2235 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2236 - if (response.success && response.data?.chat_mode) {
2237 - updateChatModeIndicator(response.data.chat_mode, botId);
2238 - }
2239 1990 },
2240 1991 error: function (xhr, status, error) {
2241 1992 // Polling error - silently continue
2242 1993 }
@@ -2246,29 +1997,20 @@
2246 1997 // ====================================
2247 1998 // CHAT HISTORY & PERSISTENCE
2248 1999 // ====================================
2249 2000
2250 -function loadChatHistory(botId, onComplete) {
2001 +function loadChatHistory(botId) {
2251 2002 botId = botId || 'default';
2252 2003 var instance = MxChatInstances.get(botId);
2253 2004
2254 2005 // Prevent duplicate loading
2255 2006 if (instance.chatHistoryLoaded) {
2256 - if (onComplete) onComplete();
2257 2007 return;
2258 2008 }
2259 2009
2260 - // Use getChatSession which returns null if no session exists (does NOT create one)
2261 2010 var sessionId = getChatSession(botId);
2262 2011 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2263 2012
2264 - // No session yet — nothing to load. History will load after first message via ensureSession.
2265 - if (!sessionId) {
2266 - instance.chatHistoryLoaded = true;
2267 - if (onComplete) onComplete();
2268 - return;
2269 - }
2270 -
2271 2013 if (chatPersistenceEnabled && sessionId) {
2272 2014 $.ajax({
2273 2015 url: mxchatChat.ajax_url,
2274 2016 type: 'POST',
@@ -2279,12 +2021,11 @@
2279 2021 },
2280 2022 success: function(response) {
2281 2023 // Handle session reset (IP changed while user was away)
2282 2024 if (response.success === false && response.data && response.data.action === 'reset_session') {
2283 - // Silent reset — new session but don't clear UI
2284 - MxChatInstances.silentResetSession(botId);
2025 + // Silently reset session - user will start fresh
2026 + resetChatSession(botId);
2285 2027 instance.chatHistoryLoaded = true; // Prevent retry loop
2286 - if (onComplete) onComplete();
2287 2028 return;
2288 2029 }
2289 2030
2290 2031 // Check if the response indicates success
@@ -2340,19 +2081,9 @@
2340 2081 var content = message.content;
2341 2082 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2342 2083 content = decodeHTMLEntities(content);
2343 2084
2344 - // Skip linkify for messages containing structured HTML
2345 - // (forms, product cards, galleries, etc.) to avoid
2346 - // markdown formatting corrupting HTML attributes
2347 - // (e.g. underscores in name="field_name" becoming <em> tags)
2348 - if (content.includes("mxchat-product-card") ||
2349 - content.includes("mxchat-image-gallery") ||
2350 - content.includes("mxchat-featured-products") ||
2351 - content.includes("<form") ||
2352 - content.includes("<input") ||
2353 - content.includes("<select") ||
2354 - content.includes("<textarea")) {
2085 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2355 2086 messageElement.html(content);
2356 2087 } else {
2357 2088 var formattedContent = linkify(content);
2358 2089 messageElement.html(formattedContent);
@@ -2392,17 +2123,13 @@
2392 2123 instance.chatHistoryLoaded = true;
2393 2124 }
2394 2125 }
2395 2126 }
2396 - if (onComplete) onComplete();
2397 2127 },
2398 2128 error: function(xhr, status, error) {
2399 2129 // Error loading chat history - silently continue
2400 - if (onComplete) onComplete();
2401 2130 }
2402 2131 });
2403 - } else {
2404 - if (onComplete) onComplete();
2405 2132 }
2406 2133 }
2407 2134
2408 2135
@@ -2578,35 +2305,45 @@
2578 2305 // ====================================
2579 2306
2580 2307 function checkPreChatDismissal(botId) {
2581 2308 botId = botId || 'default';
2582 - try {
2583 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2584 - if (dismissedAt) {
2585 - // Re-show after 24 hours
2586 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2587 - 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 {
2588 2320 getElement(botId, 'pre-chat-message').hide();
2589 - return;
2590 2321 }
2591 - // Expired — clear and show again
2592 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2322 + },
2323 + error: function() {
2324 + // Error checking pre-chat dismissal - silently continue
2593 2325 }
2594 - getElement(botId, 'pre-chat-message').fadeIn(250);
2595 - } catch (e) {
2596 - // localStorage unavailable — show the message
2597 - getElement(botId, 'pre-chat-message').fadeIn(250);
2598 - }
2326 + });
2599 2327 }
2600 2328
2601 2329 function handlePreChatDismissal(botId) {
2602 2330 botId = botId || 'default';
2603 2331 getElement(botId, 'pre-chat-message').fadeOut(200);
2604 - try {
2605 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2606 - } catch (e) {
2607 - // localStorage unavailable — dismissal won't persist
2608 - }
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 + });
2609 2346 }
2610 2347
2611 2348
2612 2349 // ====================================
@@ -2673,26 +2410,8 @@
2673 2410 $(this).addClass('hidden');
2674 2411 $badge.hide(); // Hide notification when opening chat
2675 2412 disableScroll();
2676 2413 $preChat.fadeOut(250);
2677 -
2678 - // Load chat history for returning visitors (persistence)
2679 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
2680 - if (chatPersistenceEnabled) {
2681 - MxChatInstances.ensureSession(botId);
2682 - }
2683 -
2684 - // Deferred email check — only on first widget open
2685 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2686 - var instance = MxChatInstances.get(botId);
2687 - if (emailBlocker && !instance.emailCheckDone) {
2688 - instance.emailCheckDone = true;
2689 - resolveEmailState(botId);
2690 - } else if (!emailBlocker) {
2691 - // No email collection — still route through showChatContainerForBot
2692 - // so the loader is shown while chat history loads
2693 - showChatContainerForBot(botId);
2694 - }
2695 2414 } else {
2696 2415 $chatbot.removeClass('visible').addClass('hidden');
2697 2416 $(this).removeClass('hidden');
2698 2417 enableScroll();
@@ -2710,9 +2429,11 @@
2710 2429
2711 2430 $(document).on('click', '.close-pre-chat-message', function(e) {
2712 2431 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2713 2432 var botId = getBotIdFromElement(this);
2714 - handlePreChatDismissal(botId);
2433 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2434 + $(this).remove();
2435 + });
2715 2436 });
2716 2437
2717 2438
2718 2439 // PDF upload button handlers - use class selector
@@ -2913,59 +2634,8 @@
2913 2634 });
2914 2635
2915 2636
2916 2637 // ====================================
2917 -// INIT LOADER & CHAT CONTAINER HELPERS
2918 -// ====================================
2919 -// These must be outside the email collection block so they're always available
2920 -// (used by persistence loading even when email collection is off)
2921 -
2922 -function showInitLoader(botId) {
2923 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2924 - if (loader) loader.style.display = 'flex';
2925 -}
2926 -
2927 -function hideInitLoader(botId) {
2928 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2929 - if (loader) loader.style.display = 'none';
2930 -}
2931 -
2932 -function showEmailFormForBot(botId) {
2933 - hideInitLoader(botId);
2934 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2935 - var chatContainer = getElementDOM(botId, 'chat-container');
2936 - if (emailBlocker) emailBlocker.style.display = 'flex';
2937 - if (chatContainer) chatContainer.style.display = 'none';
2938 -}
2939 -
2940 -function showChatContainerForBot(botId) {
2941 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2942 - var chatContainer = getElementDOM(botId, 'chat-container');
2943 - if (emailBlocker) emailBlocker.style.display = 'none';
2944 -
2945 - var instance = MxChatInstances.get(botId);
2946 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2947 -
2948 - // If persistence is on and history hasn't loaded yet, show loader
2949 - // while history loads to prevent flash of empty chat
2950 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
2951 - if (chatContainer) chatContainer.style.display = 'none';
2952 - showInitLoader(botId);
2953 - loadChatHistory(botId, function() {
2954 - hideInitLoader(botId);
2955 - if (chatContainer) chatContainer.style.display = 'flex';
2956 - scrollToBottom(botId, true);
2957 - });
2958 - } else {
2959 - hideInitLoader(botId);
2960 - if (chatContainer) chatContainer.style.display = 'flex';
2961 - if (typeof loadChatHistory === 'function') {
2962 - loadChatHistory(botId);
2963 - }
2964 - }
2965 -}
2966 -
2967 -// ====================================
2968 2638 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2969 2639 // ====================================
2970 2640 // Only run email collection setup if it's enabled
2971 2641 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3001,8 +2671,28 @@
3001 2671 `;
3002 2672 document.head.appendChild(style);
3003 2673 }
3004 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 +
3005 2695 function isValidEmailAddress(email) {
3006 2696 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3007 2697 return emailRegex.test(email.trim()) && email.length <= 254;
3008 2698 }
@@ -3010,41 +2700,8 @@
3010 2700 function isValidNameInput(name) {
3011 2701 return name && name.trim().length >= 2 && name.trim().length <= 100;
3012 2702 }
3013 2703
3014 - /**
3015 - * Replace {visitor_name} placeholder in intro message with actual visitor name
3016 - * @param {string} botId - The bot instance ID
3017 - * @param {string} visitorName - The visitor's name to insert
3018 - */
3019 - function replaceVisitorNamePlaceholder(botId, visitorName) {
3020 - var chatBox = getElementDOM(botId, 'chat-box');
3021 - if (!chatBox) return;
3022 -
3023 - // Find the first bot message (intro message)
3024 - var introMessage = chatBox.querySelector('.bot-message');
3025 - if (!introMessage) return;
3026 -
3027 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3028 - if (!messageContent) return;
3029 -
3030 - var html = messageContent.innerHTML;
3031 -
3032 - // Replace {visitor_name} placeholder (case-insensitive)
3033 - if (visitorName && visitorName.trim()) {
3034 - // Escape HTML to prevent XSS
3035 - var safeName = $('<div>').text(visitorName.trim()).html();
3036 - html = html.replace(/\{visitor_name\}/gi, safeName);
3037 - } else {
3038 - // Remove placeholder and clean up spacing if no name provided
3039 - html = html.replace(/\{visitor_name\}/gi, '');
3040 - // Clean up any double spaces that might result
3041 - html = html.replace(/\s{2,}/g, ' ').trim();
3042 - }
3043 -
3044 - messageContent.innerHTML = html;
3045 - }
3046 -
3047 2704 function setEmailSubmissionState(botId, loading) {
3048 2705 var submitButton = getElementDOM(botId, 'email-submit-button');
3049 2706 var emailInput = getElementDOM(botId, 'user-email');
3050 2707 var nameInput = getElementDOM(botId, 'user-name');
@@ -3124,31 +2781,11 @@
3124 2781 existingErrors.forEach(error => error.remove());
3125 2782 }
3126 2783 }
3127 2784
3128 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3129 - function resolveEmailState(botId) {
3130 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3131 - if (mxchatChat.initial_email_state.show_email_form) {
3132 - showEmailFormForBot(botId);
3133 - } else {
3134 - showChatContainerForBot(botId);
3135 - }
3136 - } else {
3137 - checkSessionAndEmailForBot(botId);
3138 - }
3139 - }
3140 -
3141 2785 function checkSessionAndEmailForBot(botId) {
3142 - const sessionId = MxChatInstances.ensureSession(botId);
2786 + const sessionId = getChatSession(botId);
3143 2787
3144 - // Hide both panels while we check — show loader instead
3145 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3146 - var chatContainer = getElementDOM(botId, 'chat-container');
3147 - if (emailBlocker) emailBlocker.style.display = 'none';
3148 - if (chatContainer) chatContainer.style.display = 'none';
3149 - showInitLoader(botId);
3150 -
3151 2788 fetch(mxchatChat.ajax_url, {
3152 2789 method: 'POST',
3153 2790 headers: {
3154 2791 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3196,9 +2833,9 @@
3196 2833 var emailInput = getElementDOM(botId, 'user-email');
3197 2834 var nameInput = getElementDOM(botId, 'user-name');
3198 2835 var userEmail = emailInput ? emailInput.value.trim() : '';
3199 2836 var userName = nameInput ? nameInput.value.trim() : '';
3200 - var sessionId = MxChatInstances.ensureSession(botId);
2837 + var sessionId = getChatSession(botId);
3201 2838
3202 2839 // Validate email
3203 2840 if (!userEmail) {
3204 2841 showEmailError(botId, 'Please enter your email address.');
@@ -3249,16 +2886,8 @@
3249 2886
3250 2887 if (data.success) {
3251 2888 showChatContainerForBot(botId);
3252 2889
3253 - // Replace {visitor_name} placeholder in intro message with actual name
3254 - if (userName) {
3255 - replaceVisitorNamePlaceholder(botId, userName);
3256 - } else {
3257 - // Remove placeholder if no name provided
3258 - replaceVisitorNamePlaceholder(botId, '');
3259 - }
3260 -
3261 2890 if (data.message && typeof appendMessage === 'function') {
3262 2891 setTimeout(() => {
3263 2892 appendMessage('bot', data.message, '', [], false, botId);
3264 2893 if (typeof scrollToBottom === 'function') {
@@ -3321,27 +2950,25 @@
3321 2950 }
3322 2951 });
3323 2952
3324 2953 // Initialize email check for all bot instances
3325 - // For floating bots: defer until widget is opened (zero passive AJAX)
3326 - // For embedded bots: check immediately since the form is visible
3327 2954 $('.mxchat-chatbot-wrapper').each(function() {
3328 2955 var botId = $(this).data('bot-id') || 'default';
3329 2956 var emailBlocker = getElementDOM(botId, 'email-blocker');
3330 2957
2958 + // Only check if email blocker exists for this bot
3331 2959 if (emailBlocker) {
3332 - if (isEmbeddedBot(botId)) {
3333 - // Embedded bots are always visible — check now
3334 - 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);
3335 2970 }
3336 - // Floating bots: handled in the widget open handler
3337 - } else if (isEmbeddedBot(botId)) {
3338 - // Embedded bot, no email collection — load history with loader
3339 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3340 - if (chatPersistenceEnabled) {
3341 - MxChatInstances.ensureSession(botId);
3342 - showChatContainerForBot(botId);
3343 - }
3344 2971 }
3345 2972 });
3346 2973 }
3347 2974
@@ -3351,32 +2978,39 @@
3351 2978 var $chatbot = getElement(botId, 'floating-chatbot');
3352 2979 if ($chatbot.hasClass('hidden')) {
3353 2980 $chatbot.removeClass('hidden').addClass('visible');
3354 2981 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3355 - handlePreChatDismissal(botId);
2982 + $(this).fadeOut(250); // Hide pre-chat message
3356 2983 disableScroll(); // Disable scroll when chatbot opens
2984 + }
2985 + });
3357 2986
3358 - // Load chat history for returning visitors (persistence)
3359 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3360 - if (chatPersistenceEnabled) {
3361 - MxChatInstances.ensureSession(botId);
3362 - }
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
3363 2993
3364 - // Deferred email check — only on first widget open
3365 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3366 - var instance = MxChatInstances.get(botId);
3367 - if (emailBlocker && !instance.emailCheckDone) {
3368 - instance.emailCheckDone = true;
3369 - resolveEmailState(botId);
3370 - } else if (!emailBlocker) {
3371 - 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
3372 3008 }
3373 - }
3009 + });
3374 3010 });
3375 3011
3376 - // Legacy duplicate close handler removed — handled by single event delegation above
3377 3012
3378 -
3379 3013 function hasQuickQuestions(botId) {
3380 3014 botId = botId || 'default';
3381 3015 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3382 3016 if (!questionsContainer) return false;
@@ -3512,11 +3146,18 @@
3512 3146 });
3513 3147
3514 3148 // Initialize when document is ready
3515 3149 setFullHeight();
3150 + trackOriginatingPage();
3516 3151
3517 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3518 - // 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 + }
3519 3160
3520 3161 // Initialize chat visibility for all instances
3521 3162 $('.mxchat-chatbot-wrapper').each(function() {
3522 3163 var botId = $(this).data('bot-id') || 'default';