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 +208 -550 3.2.33.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 }
@@ -1120,16 +990,21 @@
1120 990 errorMessage = "An error occurred. Please try again or contact support.";
1121 991 }
1122 992
1123 993 // Handle session reset action (IP changed, session expired, etc.)
1124 - // Silent reset — keep chat UI intact, just get a new session and retry
1125 994 if (data.data && data.data.action === 'reset_session') {
1126 - MxChatInstances.silentResetSession(botId);
1127 - // 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
1128 998 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1129 999 if (originalMessage) {
1130 1000 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1131 - 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';
1132 1007 if (shouldUseStreaming(currentModel)) {
1133 1008 callMxChatStream(originalMessage, callback, botId);
1134 1009 } else {
1135 1010 callMxChat(originalMessage, callback, botId);
@@ -1151,22 +1026,8 @@
1151 1026 }
1152 1027 return; // Exit early for errors
1153 1028 }
1154 1029
1155 - // Check for live agent response
1156 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1157 - removeThinkingDots(botId);
1158 - // Also remove any leftover bot-message that lost its temporary-message class
1159 - var $chatBox = getElement(botId, 'chat-box');
1160 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1161 - updateChatModeIndicator('agent', botId);
1162 - enableChatInput(botId);
1163 - if (callback) {
1164 - callback('');
1165 - }
1166 - return;
1167 - }
1168 -
1169 1030 // Handle different response formats
1170 1031 if (data.text || data.html || data.message) {
1171 1032
1172 1033 // Apply response hooks
@@ -1211,15 +1072,19 @@
1211 1072 }
1212 1073
1213 1074 // Enhanced updateChatModeIndicator function for immediate DOM updates
1214 1075 function updateChatModeIndicator(mode, botId) {
1076 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1215 1077 botId = botId || 'default';
1216 1078 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1079 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1217 1080 if (indicator) {
1218 1081 const oldText = indicator.textContent;
1082 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1219 1083
1220 1084 if (mode === 'agent') {
1221 1085 indicator.textContent = 'Live Agent';
1086 + console.log('[MxChat] Mode is agent, calling startPolling...');
1222 1087 startPolling(botId);
1223 1088 } else {
1224 1089 // Everything else is AI mode
1225 1090 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1290,12 +1155,9 @@
1290 1155 // Update the event handlers to use the correct function names (using event delegation)
1291 1156 // Use class-based selectors for multi-instance support
1292 1157 $(document).on('click', '.send-button', function() {
1293 1158 var botId = getBotIdFromElement(this);
1294 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1295 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1296 - disableChatInput(botId);
1297 - }
1159 + disableChatInput(botId);
1298 1160 sendMessage(botId);
1299 1161 });
1300 1162
1301 1163 // Override enter key handler (using event delegation)
@@ -1302,12 +1164,9 @@
1302 1164 $(document).on('keypress', '.chat-input', function(e) {
1303 1165 if (e.which == 13 && !e.shiftKey) {
1304 1166 e.preventDefault();
1305 1167 var botId = getBotIdFromElement(this);
1306 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1307 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1308 - disableChatInput(botId);
1309 - }
1168 + disableChatInput(botId);
1310 1169 sendMessage(botId);
1311 1170 }
1312 1171 });
1313 1172
@@ -1350,12 +1209,17 @@
1350 1209 'margin-bottom': '1em'
1351 1210 });
1352 1211 }
1353 1212
1354 - // Process the message content - always run linkify to convert markdown
1355 - // links and format text. linkify() handles existing HTML safely via
1356 - // negative lookaheads that skip URLs already inside <a> tags.
1357 - 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 + }
1358 1222
1359 1223 // Add images if provided
1360 1224 if (images && images.length > 0) {
1361 1225 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1490,12 +1354,26 @@
1490 1354 bgColor = botMessageBgColor;
1491 1355 fontColor = botMessageFontColor;
1492 1356 }
1493 1357
1494 - // Always run linkify to convert markdown links and format text.
1495 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1496 - // to avoid double-processing URLs that are already inside <a> tags).
1497 - 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 + }
1498 1376
1499 1377 if (responseHtml) {
1500 1378 // Only add line breaks if there's actual text content before the HTML
1501 1379 if (fullMessage && fullMessage.trim()) {
@@ -1562,15 +1440,8 @@
1562 1440
1563 1441
1564 1442 function appendThinkingMessage(botId) {
1565 1443 botId = botId || 'default';
1566 -
1567 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
1568 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
1569 - if (indicator && indicator.textContent === 'Live Agent') {
1570 - return;
1571 - }
1572 -
1573 1444 var $chatBox = getElement(botId, 'chat-box');
1574 1445
1575 1446 // Remove any existing thinking dots in this bot's chat first
1576 1447 $chatBox.find('.thinking-dots').remove();
@@ -1592,9 +1463,9 @@
1592 1463 '</div>' +
1593 1464 '</div>';
1594 1465
1595 1466 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1596 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1467 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
1597 1468 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1598 1469 scrollToBottom(botId);
1599 1470 }
1600 1471
@@ -1600,11 +1471,9 @@
1600 1471
1601 1472 function removeThinkingDots(botId) {
1602 1473 botId = botId || 'default';
1603 1474 var $chatBox = getElement(botId, 'chat-box');
1604 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1605 1475 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1606 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1607 1476 }
1608 1477
1609 1478 // ====================================
1610 1479 // TEXT FORMATTING & PROCESSING
@@ -1638,12 +1507,9 @@
1638 1507 processedText = formatTextStyling(processedText);
1639 1508
1640 1509 // Process code blocks BEFORE processing links
1641 1510 processedText = formatCodeBlocks(processedText);
1642 -
1643 - // Process markdown tables BEFORE converting newlines to paragraphs
1644 - processedText = formatMarkdownTables(processedText);
1645 -
1511 +
1646 1512 // NOW convert to paragraphs
1647 1513 processedText = convertNewlinesToBreaks(processedText);
1648 1514
1649 1515 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1656,63 +1522,37 @@
1656 1522 // Return as a proper link without the brackets
1657 1523 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1658 1524 });
1659 1525
1660 - // Process markdown links: [text](url) and [](url)
1661 - // Uses balanced parenthesis matching to handle URLs containing parens
1662 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
1663 - processedText = (function(input) {
1664 - var result = '';
1665 - var i = 0;
1666 - while (i < input.length) {
1667 - // Look for [ at current position
1668 - if (input[i] === '[') {
1669 - // Find closing ]
1670 - var closeBracket = input.indexOf(']', i + 1);
1671 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
1672 - result += input[i];
1673 - i++;
1674 - continue;
1675 - }
1676 - var linkText = input.substring(i + 1, closeBracket);
1677 - // Check if URL starts with http
1678 - var urlStart = closeBracket + 2;
1679 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
1680 - result += input[i];
1681 - i++;
1682 - continue;
1683 - }
1684 - // Find balanced closing paren
1685 - var depth = 1;
1686 - var j = urlStart;
1687 - while (j < input.length && depth > 0) {
1688 - if (input[j] === '(') depth++;
1689 - else if (input[j] === ')') depth--;
1690 - if (depth > 0) j++;
1691 - }
1692 - if (depth !== 0) {
1693 - result += input[i];
1694 - i++;
1695 - continue;
1696 - }
1697 - var url = input.substring(urlStart, j);
1698 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
1699 - var encodedUrl = safeEncodeUrl(cleanUrl);
1700 - if (!linkText || !linkText.trim()) {
1701 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
1702 - } else {
1703 - var safeText = sanitizeUserInput(linkText);
1704 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
1705 - }
1706 - i = j + 1; // Skip past the closing )
1707 - } else {
1708 - result += input[i];
1709 - i++;
1710 - }
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>`;
1711 1536 }
1712 - return result;
1713 - })(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 + });
1714 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 +
1715 1555 // Process phone numbers: [text](tel:number)
1716 1556 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1717 1557 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1718 1558 const safePhone = safeEncodeUrl(phone);
@@ -1864,78 +1704,9 @@
1864 1704 });
1865 1705
1866 1706 return text;
1867 1707 }
1868 -
1869 - function formatMarkdownTables(text) {
1870 - var lines = text.split('\n');
1871 - var result = [];
1872 - var i = 0;
1873 -
1874 - while (i < lines.length) {
1875 - // Check for a table: current line has pipes AND next line is a separator row
1876 - if (i + 1 < lines.length &&
1877 - lines[i].indexOf('|') !== -1 &&
1878 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
1879 -
1880 - var tableLines = [];
1881 - var headerLine = lines[i];
1882 - var separatorLine = lines[i + 1];
1883 - tableLines.push(headerLine);
1884 - tableLines.push(separatorLine);
1885 -
1886 - // Collect remaining table rows
1887 - var j = i + 2;
1888 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
1889 - tableLines.push(lines[j]);
1890 - j++;
1891 - }
1892 -
1893 - // Parse alignment from separator row
1894 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
1895 - var alignments = sepCells.map(function(cell) {
1896 - var trimmed = cell.trim();
1897 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
1898 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
1899 - return 'left';
1900 - });
1901 -
1902 - // Build HTML table
1903 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
1904 -
1905 - // Header row
1906 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
1907 - html += '<thead><tr>';
1908 - headerCells.forEach(function(cell, idx) {
1909 - var align = alignments[idx] || 'left';
1910 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
1911 - });
1912 - html += '</tr></thead>';
1913 -
1914 - // Body rows
1915 - html += '<tbody>';
1916 - for (var r = 2; r < tableLines.length; r++) {
1917 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
1918 - html += '<tr>';
1919 - rowCells.forEach(function(cell, idx) {
1920 - var align = alignments[idx] || 'left';
1921 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
1922 - });
1923 - html += '</tr>';
1924 - }
1925 - html += '</tbody></table></div>';
1926 -
1927 - result.push(html);
1928 - i = j;
1929 - } else {
1930 - result.push(lines[i]);
1931 - i++;
1932 - }
1933 - }
1934 -
1935 - return result.join('\n');
1936 - }
1937 -
1708 +
1938 1709 function sanitizeUserInput(text) {
1939 1710 const div = document.createElement('div');
1940 1711 div.textContent = text;
1941 1712 return div.innerHTML;
@@ -2156,12 +1927,15 @@
2156 1927 // LIVE AGENT FUNCTIONALITY
2157 1928 // ====================================
2158 1929
2159 1930 function startPolling(botId) {
1931 + console.log('[MxChat] startPolling called for botId:', botId);
2160 1932 botId = botId || 'default';
2161 1933 var instance = MxChatInstances.get(botId);
2162 1934 // Clear any existing interval first
2163 1935 stopPolling(botId);
1936 + // Start new polling interval
1937 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2164 1938 instance.pollingInterval = setInterval(function() {
2165 1939 checkForAgentMessages(botId);
2166 1940 }, 5000);
2167 1941 }
@@ -2166,17 +1940,20 @@
2166 1940 }, 5000);
2167 1941 }
2168 1942
2169 1943 function stopPolling(botId) {
1944 + console.log('[MxChat] stopPolling called for botId:', botId);
2170 1945 botId = botId || 'default';
2171 1946 var instance = MxChatInstances.get(botId);
2172 1947 if (instance.pollingInterval) {
2173 1948 clearInterval(instance.pollingInterval);
2174 1949 instance.pollingInterval = null;
1950 + console.log('[MxChat] Polling stopped for botId:', botId);
2175 1951 }
2176 1952 }
2177 1953
2178 1954 function checkForAgentMessages(botId) {
1955 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2179 1956 botId = botId || 'default';
2180 1957 var instance = MxChatInstances.get(botId);
2181 1958 const sessionId = getChatSession(botId);
2182 1959 $.ajax({
@@ -2202,12 +1979,8 @@
2202 1979 instance.processedMessageIds.add(message.id);
2203 1980 }
2204 1981 });
2205 1982
2206 - if (hasNewMessage) {
2207 - enableChatInput(botId);
2208 - }
2209 -
2210 1983 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2211 1984 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2212 1985 showNotification(botId);
2213 1986 }
@@ -2213,13 +1986,8 @@
2213 1986 }
2214 1987
2215 1988 scrollToBottom(botId, true);
2216 1989 }
2217 -
2218 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2219 - if (response.success && response.data?.chat_mode) {
2220 - updateChatModeIndicator(response.data.chat_mode, botId);
2221 - }
2222 1990 },
2223 1991 error: function (xhr, status, error) {
2224 1992 // Polling error - silently continue
2225 1993 }
@@ -2229,29 +1997,20 @@
2229 1997 // ====================================
2230 1998 // CHAT HISTORY & PERSISTENCE
2231 1999 // ====================================
2232 2000
2233 -function loadChatHistory(botId, onComplete) {
2001 +function loadChatHistory(botId) {
2234 2002 botId = botId || 'default';
2235 2003 var instance = MxChatInstances.get(botId);
2236 2004
2237 2005 // Prevent duplicate loading
2238 2006 if (instance.chatHistoryLoaded) {
2239 - if (onComplete) onComplete();
2240 2007 return;
2241 2008 }
2242 2009
2243 - // Use getChatSession which returns null if no session exists (does NOT create one)
2244 2010 var sessionId = getChatSession(botId);
2245 2011 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2246 2012
2247 - // No session yet — nothing to load. History will load after first message via ensureSession.
2248 - if (!sessionId) {
2249 - instance.chatHistoryLoaded = true;
2250 - if (onComplete) onComplete();
2251 - return;
2252 - }
2253 -
2254 2013 if (chatPersistenceEnabled && sessionId) {
2255 2014 $.ajax({
2256 2015 url: mxchatChat.ajax_url,
2257 2016 type: 'POST',
@@ -2262,12 +2021,11 @@
2262 2021 },
2263 2022 success: function(response) {
2264 2023 // Handle session reset (IP changed while user was away)
2265 2024 if (response.success === false && response.data && response.data.action === 'reset_session') {
2266 - // Silent reset — new session but don't clear UI
2267 - MxChatInstances.silentResetSession(botId);
2025 + // Silently reset session - user will start fresh
2026 + resetChatSession(botId);
2268 2027 instance.chatHistoryLoaded = true; // Prevent retry loop
2269 - if (onComplete) onComplete();
2270 2028 return;
2271 2029 }
2272 2030
2273 2031 // Check if the response indicates success
@@ -2323,19 +2081,9 @@
2323 2081 var content = message.content;
2324 2082 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2325 2083 content = decodeHTMLEntities(content);
2326 2084
2327 - // Skip linkify for messages containing structured HTML
2328 - // (forms, product cards, galleries, etc.) to avoid
2329 - // markdown formatting corrupting HTML attributes
2330 - // (e.g. underscores in name="field_name" becoming <em> tags)
2331 - if (content.includes("mxchat-product-card") ||
2332 - content.includes("mxchat-image-gallery") ||
2333 - content.includes("mxchat-featured-products") ||
2334 - content.includes("<form") ||
2335 - content.includes("<input") ||
2336 - content.includes("<select") ||
2337 - content.includes("<textarea")) {
2085 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2338 2086 messageElement.html(content);
2339 2087 } else {
2340 2088 var formattedContent = linkify(content);
2341 2089 messageElement.html(formattedContent);
@@ -2375,17 +2123,13 @@
2375 2123 instance.chatHistoryLoaded = true;
2376 2124 }
2377 2125 }
2378 2126 }
2379 - if (onComplete) onComplete();
2380 2127 },
2381 2128 error: function(xhr, status, error) {
2382 2129 // Error loading chat history - silently continue
2383 - if (onComplete) onComplete();
2384 2130 }
2385 2131 });
2386 - } else {
2387 - if (onComplete) onComplete();
2388 2132 }
2389 2133 }
2390 2134
2391 2135
@@ -2561,35 +2305,45 @@
2561 2305 // ====================================
2562 2306
2563 2307 function checkPreChatDismissal(botId) {
2564 2308 botId = botId || 'default';
2565 - try {
2566 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2567 - if (dismissedAt) {
2568 - // Re-show after 24 hours
2569 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2570 - 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 {
2571 2320 getElement(botId, 'pre-chat-message').hide();
2572 - return;
2573 2321 }
2574 - // Expired — clear and show again
2575 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2322 + },
2323 + error: function() {
2324 + // Error checking pre-chat dismissal - silently continue
2576 2325 }
2577 - getElement(botId, 'pre-chat-message').fadeIn(250);
2578 - } catch (e) {
2579 - // localStorage unavailable — show the message
2580 - getElement(botId, 'pre-chat-message').fadeIn(250);
2581 - }
2326 + });
2582 2327 }
2583 2328
2584 2329 function handlePreChatDismissal(botId) {
2585 2330 botId = botId || 'default';
2586 2331 getElement(botId, 'pre-chat-message').fadeOut(200);
2587 - try {
2588 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2589 - } catch (e) {
2590 - // localStorage unavailable — dismissal won't persist
2591 - }
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 + });
2592 2346 }
2593 2347
2594 2348
2595 2349 // ====================================
@@ -2656,26 +2410,8 @@
2656 2410 $(this).addClass('hidden');
2657 2411 $badge.hide(); // Hide notification when opening chat
2658 2412 disableScroll();
2659 2413 $preChat.fadeOut(250);
2660 -
2661 - // Load chat history for returning visitors (persistence)
2662 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
2663 - if (chatPersistenceEnabled) {
2664 - MxChatInstances.ensureSession(botId);
2665 - }
2666 -
2667 - // Deferred email check — only on first widget open
2668 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2669 - var instance = MxChatInstances.get(botId);
2670 - if (emailBlocker && !instance.emailCheckDone) {
2671 - instance.emailCheckDone = true;
2672 - resolveEmailState(botId);
2673 - } else if (!emailBlocker) {
2674 - // No email collection — still route through showChatContainerForBot
2675 - // so the loader is shown while chat history loads
2676 - showChatContainerForBot(botId);
2677 - }
2678 2414 } else {
2679 2415 $chatbot.removeClass('visible').addClass('hidden');
2680 2416 $(this).removeClass('hidden');
2681 2417 enableScroll();
@@ -2693,9 +2429,11 @@
2693 2429
2694 2430 $(document).on('click', '.close-pre-chat-message', function(e) {
2695 2431 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2696 2432 var botId = getBotIdFromElement(this);
2697 - handlePreChatDismissal(botId);
2433 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2434 + $(this).remove();
2435 + });
2698 2436 });
2699 2437
2700 2438
2701 2439 // PDF upload button handlers - use class selector
@@ -2896,59 +2634,8 @@
2896 2634 });
2897 2635
2898 2636
2899 2637 // ====================================
2900 -// INIT LOADER & CHAT CONTAINER HELPERS
2901 -// ====================================
2902 -// These must be outside the email collection block so they're always available
2903 -// (used by persistence loading even when email collection is off)
2904 -
2905 -function showInitLoader(botId) {
2906 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2907 - if (loader) loader.style.display = 'flex';
2908 -}
2909 -
2910 -function hideInitLoader(botId) {
2911 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2912 - if (loader) loader.style.display = 'none';
2913 -}
2914 -
2915 -function showEmailFormForBot(botId) {
2916 - hideInitLoader(botId);
2917 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2918 - var chatContainer = getElementDOM(botId, 'chat-container');
2919 - if (emailBlocker) emailBlocker.style.display = 'flex';
2920 - if (chatContainer) chatContainer.style.display = 'none';
2921 -}
2922 -
2923 -function showChatContainerForBot(botId) {
2924 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2925 - var chatContainer = getElementDOM(botId, 'chat-container');
2926 - if (emailBlocker) emailBlocker.style.display = 'none';
2927 -
2928 - var instance = MxChatInstances.get(botId);
2929 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2930 -
2931 - // If persistence is on and history hasn't loaded yet, show loader
2932 - // while history loads to prevent flash of empty chat
2933 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
2934 - if (chatContainer) chatContainer.style.display = 'none';
2935 - showInitLoader(botId);
2936 - loadChatHistory(botId, function() {
2937 - hideInitLoader(botId);
2938 - if (chatContainer) chatContainer.style.display = 'flex';
2939 - scrollToBottom(botId, true);
2940 - });
2941 - } else {
2942 - hideInitLoader(botId);
2943 - if (chatContainer) chatContainer.style.display = 'flex';
2944 - if (typeof loadChatHistory === 'function') {
2945 - loadChatHistory(botId);
2946 - }
2947 - }
2948 -}
2949 -
2950 -// ====================================
2951 2638 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2952 2639 // ====================================
2953 2640 // Only run email collection setup if it's enabled
2954 2641 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -2984,8 +2671,28 @@
2984 2671 `;
2985 2672 document.head.appendChild(style);
2986 2673 }
2987 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 +
2988 2695 function isValidEmailAddress(email) {
2989 2696 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2990 2697 return emailRegex.test(email.trim()) && email.length <= 254;
2991 2698 }
@@ -2993,41 +2700,8 @@
2993 2700 function isValidNameInput(name) {
2994 2701 return name && name.trim().length >= 2 && name.trim().length <= 100;
2995 2702 }
2996 2703
2997 - /**
2998 - * Replace {visitor_name} placeholder in intro message with actual visitor name
2999 - * @param {string} botId - The bot instance ID
3000 - * @param {string} visitorName - The visitor's name to insert
3001 - */
3002 - function replaceVisitorNamePlaceholder(botId, visitorName) {
3003 - var chatBox = getElementDOM(botId, 'chat-box');
3004 - if (!chatBox) return;
3005 -
3006 - // Find the first bot message (intro message)
3007 - var introMessage = chatBox.querySelector('.bot-message');
3008 - if (!introMessage) return;
3009 -
3010 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3011 - if (!messageContent) return;
3012 -
3013 - var html = messageContent.innerHTML;
3014 -
3015 - // Replace {visitor_name} placeholder (case-insensitive)
3016 - if (visitorName && visitorName.trim()) {
3017 - // Escape HTML to prevent XSS
3018 - var safeName = $('<div>').text(visitorName.trim()).html();
3019 - html = html.replace(/\{visitor_name\}/gi, safeName);
3020 - } else {
3021 - // Remove placeholder and clean up spacing if no name provided
3022 - html = html.replace(/\{visitor_name\}/gi, '');
3023 - // Clean up any double spaces that might result
3024 - html = html.replace(/\s{2,}/g, ' ').trim();
3025 - }
3026 -
3027 - messageContent.innerHTML = html;
3028 - }
3029 -
3030 2704 function setEmailSubmissionState(botId, loading) {
3031 2705 var submitButton = getElementDOM(botId, 'email-submit-button');
3032 2706 var emailInput = getElementDOM(botId, 'user-email');
3033 2707 var nameInput = getElementDOM(botId, 'user-name');
@@ -3107,31 +2781,11 @@
3107 2781 existingErrors.forEach(error => error.remove());
3108 2782 }
3109 2783 }
3110 2784
3111 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3112 - function resolveEmailState(botId) {
3113 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3114 - if (mxchatChat.initial_email_state.show_email_form) {
3115 - showEmailFormForBot(botId);
3116 - } else {
3117 - showChatContainerForBot(botId);
3118 - }
3119 - } else {
3120 - checkSessionAndEmailForBot(botId);
3121 - }
3122 - }
3123 -
3124 2785 function checkSessionAndEmailForBot(botId) {
3125 - const sessionId = MxChatInstances.ensureSession(botId);
2786 + const sessionId = getChatSession(botId);
3126 2787
3127 - // Hide both panels while we check — show loader instead
3128 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3129 - var chatContainer = getElementDOM(botId, 'chat-container');
3130 - if (emailBlocker) emailBlocker.style.display = 'none';
3131 - if (chatContainer) chatContainer.style.display = 'none';
3132 - showInitLoader(botId);
3133 -
3134 2788 fetch(mxchatChat.ajax_url, {
3135 2789 method: 'POST',
3136 2790 headers: {
3137 2791 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3179,9 +2833,9 @@
3179 2833 var emailInput = getElementDOM(botId, 'user-email');
3180 2834 var nameInput = getElementDOM(botId, 'user-name');
3181 2835 var userEmail = emailInput ? emailInput.value.trim() : '';
3182 2836 var userName = nameInput ? nameInput.value.trim() : '';
3183 - var sessionId = MxChatInstances.ensureSession(botId);
2837 + var sessionId = getChatSession(botId);
3184 2838
3185 2839 // Validate email
3186 2840 if (!userEmail) {
3187 2841 showEmailError(botId, 'Please enter your email address.');
@@ -3232,16 +2886,8 @@
3232 2886
3233 2887 if (data.success) {
3234 2888 showChatContainerForBot(botId);
3235 2889
3236 - // Replace {visitor_name} placeholder in intro message with actual name
3237 - if (userName) {
3238 - replaceVisitorNamePlaceholder(botId, userName);
3239 - } else {
3240 - // Remove placeholder if no name provided
3241 - replaceVisitorNamePlaceholder(botId, '');
3242 - }
3243 -
3244 2890 if (data.message && typeof appendMessage === 'function') {
3245 2891 setTimeout(() => {
3246 2892 appendMessage('bot', data.message, '', [], false, botId);
3247 2893 if (typeof scrollToBottom === 'function') {
@@ -3304,27 +2950,25 @@
3304 2950 }
3305 2951 });
3306 2952
3307 2953 // Initialize email check for all bot instances
3308 - // For floating bots: defer until widget is opened (zero passive AJAX)
3309 - // For embedded bots: check immediately since the form is visible
3310 2954 $('.mxchat-chatbot-wrapper').each(function() {
3311 2955 var botId = $(this).data('bot-id') || 'default';
3312 2956 var emailBlocker = getElementDOM(botId, 'email-blocker');
3313 2957
2958 + // Only check if email blocker exists for this bot
3314 2959 if (emailBlocker) {
3315 - if (isEmbeddedBot(botId)) {
3316 - // Embedded bots are always visible — check now
3317 - 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);
3318 2970 }
3319 - // Floating bots: handled in the widget open handler
3320 - } else if (isEmbeddedBot(botId)) {
3321 - // Embedded bot, no email collection — load history with loader
3322 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3323 - if (chatPersistenceEnabled) {
3324 - MxChatInstances.ensureSession(botId);
3325 - showChatContainerForBot(botId);
3326 - }
3327 2971 }
3328 2972 });
3329 2973 }
3330 2974
@@ -3334,32 +2978,39 @@
3334 2978 var $chatbot = getElement(botId, 'floating-chatbot');
3335 2979 if ($chatbot.hasClass('hidden')) {
3336 2980 $chatbot.removeClass('hidden').addClass('visible');
3337 2981 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3338 - handlePreChatDismissal(botId);
2982 + $(this).fadeOut(250); // Hide pre-chat message
3339 2983 disableScroll(); // Disable scroll when chatbot opens
2984 + }
2985 + });
3340 2986
3341 - // Load chat history for returning visitors (persistence)
3342 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3343 - if (chatPersistenceEnabled) {
3344 - MxChatInstances.ensureSession(botId);
3345 - }
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
3346 2993
3347 - // Deferred email check — only on first widget open
3348 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3349 - var instance = MxChatInstances.get(botId);
3350 - if (emailBlocker && !instance.emailCheckDone) {
3351 - instance.emailCheckDone = true;
3352 - resolveEmailState(botId);
3353 - } else if (!emailBlocker) {
3354 - 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
3355 3008 }
3356 - }
3009 + });
3357 3010 });
3358 3011
3359 - // Legacy duplicate close handler removed — handled by single event delegation above
3360 3012
3361 -
3362 3013 function hasQuickQuestions(botId) {
3363 3014 botId = botId || 'default';
3364 3015 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3365 3016 if (!questionsContainer) return false;
@@ -3495,11 +3146,18 @@
3495 3146 });
3496 3147
3497 3148 // Initialize when document is ready
3498 3149 setFullHeight();
3150 + trackOriginatingPage();
3499 3151
3500 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3501 - // 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 + }
3502 3160
3503 3161 // Initialize chat visibility for all instances
3504 3162 $('.mxchat-chatbot-wrapper').each(function() {
3505 3163 var botId = $(this).data('bot-id') || 'default';