PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.3
MxChat – AI Chatbot & Content Generation for WordPress v3.0.3
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 +500 -805 3.2.33.0.3 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,437 +2634,380 @@
2896 2634 });
2897 2635
2898 2636
2899 2637 // ====================================
2900 -// INIT LOADER & CHAT CONTAINER HELPERS
2638 +// EMAIL COLLECTION SETUP - FIXED VERSION
2901 2639 // ====================================
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 -// EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2952 -// ====================================
2953 2640 // Only run email collection setup if it's enabled
2954 2641 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2642 + // Email collection form setup and handlers
2643 + const emailForm = document.getElementById('email-collection-form');
2644 + const emailBlocker = document.getElementById('email-blocker');
2645 + const chatbotWrapper = document.getElementById('chat-container');
2955 2646
2956 - // Track submitting state per bot
2957 - const emailSubmittingState = {};
2647 + if (emailForm && emailBlocker && chatbotWrapper) {
2648 +
2649 + // Add loading state management
2650 + let isSubmitting = false;
2651 +
2652 + // Optimized UI transition functions
2653 + function showEmailForm() {
2654 + emailBlocker.style.display = 'flex';
2655 + chatbotWrapper.style.display = 'none';
2656 + }
2958 2657
2959 - // Add CSS animations for email form (once globally)
2960 - if (!document.getElementById('email-error-styles')) {
2961 - const style = document.createElement('style');
2962 - style.id = 'email-error-styles';
2963 - style.textContent = `
2964 - @keyframes fadeInError {
2965 - from { opacity: 0; transform: translateY(-5px); }
2966 - to { opacity: 1; transform: translateY(0); }
2658 + function showChatContainer() {
2659 + // Show chat immediately without delay
2660 + emailBlocker.style.display = 'none';
2661 + chatbotWrapper.style.display = 'flex';
2662 +
2663 + // Load chat history only after showing chat container
2664 + if (typeof loadChatHistory === 'function') {
2665 + loadChatHistory();
2967 2666 }
2968 - .email-input-shake {
2969 - animation: shake 0.5s ease-in-out;
2970 - }
2971 - @keyframes shake {
2972 - 0%, 100% { transform: translateX(0); }
2973 - 25% { transform: translateX(-5px); }
2974 - 75% { transform: translateX(5px); }
2975 - }
2976 - @keyframes spin {
2977 - from { transform: rotate(0deg); }
2978 - to { transform: rotate(360deg); }
2979 - }
2980 - .email-spinner {
2981 - display: inline-block;
2982 - vertical-align: middle;
2983 - }
2984 - `;
2985 - document.head.appendChild(style);
2986 - }
2667 + }
2987 2668
2988 - function isValidEmailAddress(email) {
2989 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2990 - return emailRegex.test(email.trim()) && email.length <= 254;
2991 - }
2669 + // Enhanced email validation
2670 + function isValidEmail(email) {
2671 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2672 + return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2673 + }
2992 2674
2993 - function isValidNameInput(name) {
2994 - return name && name.trim().length >= 2 && name.trim().length <= 100;
2995 - }
2675 + // Enhanced name validation
2676 + function isValidName(name) {
2677 + return name && name.trim().length >= 2 && name.trim().length <= 100;
2678 + }
2996 2679
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();
2680 + // Show loading state with spinner
2681 + function setSubmissionState(loading) {
2682 + const submitButton = document.getElementById('email-submit-button');
2683 + const emailInput = document.getElementById('user-email');
2684 + const nameInput = document.getElementById('user-name');
2685 +
2686 + if (loading) {
2687 + isSubmitting = true;
2688 + if (submitButton) submitButton.disabled = true;
2689 + if (emailInput) emailInput.disabled = true;
2690 + if (nameInput) nameInput.disabled = true;
2691 +
2692 + // Store original content and add spinner
2693 + if (submitButton && !submitButton.getAttribute('data-original-html')) {
2694 + submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2695 +
2696 + // Add loading spinner while keeping original text
2697 + const originalText = submitButton.textContent;
2698 + submitButton.innerHTML = `
2699 + <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2700 + <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2701 + <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2702 + <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2703 + </circle>
2704 + </svg>
2705 + ${originalText}
2706 + `;
2707 +
2708 + submitButton.style.opacity = '0.8';
2709 + }
2710 + } else {
2711 + isSubmitting = false;
2712 + if (submitButton) submitButton.disabled = false;
2713 + if (emailInput) emailInput.disabled = false;
2714 + if (nameInput) nameInput.disabled = false;
2715 +
2716 + // Restore original content
2717 + if (submitButton) {
2718 + const originalHtml = submitButton.getAttribute('data-original-html');
2719 + if (originalHtml) {
2720 + submitButton.innerHTML = originalHtml;
2721 + }
2722 + submitButton.style.opacity = '1';
2723 + }
2724 + }
3025 2725 }
3026 2726
3027 - messageContent.innerHTML = html;
3028 - }
3029 -
3030 - function setEmailSubmissionState(botId, loading) {
3031 - var submitButton = getElementDOM(botId, 'email-submit-button');
3032 - var emailInput = getElementDOM(botId, 'user-email');
3033 - var nameInput = getElementDOM(botId, 'user-name');
3034 -
3035 - if (loading) {
3036 - emailSubmittingState[botId] = true;
3037 - if (submitButton) submitButton.disabled = true;
3038 - if (emailInput) emailInput.disabled = true;
3039 - if (nameInput) nameInput.disabled = true;
3040 -
3041 - if (submitButton && !submitButton.getAttribute('data-original-html')) {
3042 - submitButton.setAttribute('data-original-html', submitButton.innerHTML);
3043 - const originalText = submitButton.textContent;
3044 - submitButton.innerHTML = `
3045 - <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
3046 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
3047 - <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
3048 - <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
3049 - </circle>
3050 - </svg>
3051 - ${originalText}
2727 + // Error display functions
2728 + function showEmailError(message) {
2729 + clearEmailError();
2730 +
2731 + const errorDiv = document.createElement('div');
2732 + errorDiv.className = 'email-error';
2733 + errorDiv.style.cssText = `
2734 + color: #e74c3c;
2735 + font-size: 12px;
2736 + margin-top: 8px;
2737 + padding: 4px 0;
2738 + animation: fadeInError 0.3s ease;
2739 + `;
2740 + errorDiv.textContent = message;
2741 +
2742 + // Add CSS animation if not already present
2743 + if (!document.getElementById('email-error-styles')) {
2744 + const style = document.createElement('style');
2745 + style.id = 'email-error-styles';
2746 + style.textContent = `
2747 + @keyframes fadeInError {
2748 + from { opacity: 0; transform: translateY(-5px); }
2749 + to { opacity: 1; transform: translateY(0); }
2750 + }
2751 + .email-input-shake {
2752 + animation: shake 0.5s ease-in-out;
2753 + }
2754 + @keyframes shake {
2755 + 0%, 100% { transform: translateX(0); }
2756 + 25% { transform: translateX(-5px); }
2757 + 75% { transform: translateX(5px); }
2758 + }
2759 + @keyframes spin {
2760 + from { transform: rotate(0deg); }
2761 + to { transform: rotate(360deg); }
2762 + }
2763 + .email-spinner {
2764 + display: inline-block;
2765 + vertical-align: middle;
2766 + }
3052 2767 `;
3053 - submitButton.style.opacity = '0.8';
2768 + document.head.appendChild(style);
3054 2769 }
3055 - } else {
3056 - emailSubmittingState[botId] = false;
3057 - if (submitButton) submitButton.disabled = false;
3058 - if (emailInput) emailInput.disabled = false;
3059 - if (nameInput) nameInput.disabled = false;
3060 -
3061 - if (submitButton) {
3062 - const originalHtml = submitButton.getAttribute('data-original-html');
3063 - if (originalHtml) {
3064 - submitButton.innerHTML = originalHtml;
3065 - }
3066 - submitButton.style.opacity = '1';
2770 +
2771 + emailForm.appendChild(errorDiv);
2772 +
2773 + // Add shake animation to inputs
2774 + const emailInput = document.getElementById('user-email');
2775 + const nameInput = document.getElementById('user-name');
2776 +
2777 + if (emailInput) {
2778 + emailInput.classList.add('email-input-shake');
2779 + setTimeout(() => {
2780 + emailInput.classList.remove('email-input-shake');
2781 + }, 500);
3067 2782 }
2783 +
2784 + if (nameInput) {
2785 + nameInput.classList.add('email-input-shake');
2786 + setTimeout(() => {
2787 + nameInput.classList.remove('email-input-shake');
2788 + }, 500);
2789 + }
3068 2790 }
3069 - }
3070 2791
3071 - function showEmailError(botId, message) {
3072 - clearEmailError(botId);
3073 -
3074 - var emailForm = getElementDOM(botId, 'email-collection-form');
3075 - if (!emailForm) return;
3076 -
3077 - const errorDiv = document.createElement('div');
3078 - errorDiv.className = 'email-error';
3079 - errorDiv.style.cssText = `
3080 - color: #e74c3c;
3081 - font-size: 12px;
3082 - margin-top: 8px;
3083 - padding: 4px 0;
3084 - animation: fadeInError 0.3s ease;
3085 - `;
3086 - errorDiv.textContent = message;
3087 - emailForm.appendChild(errorDiv);
3088 -
3089 - // Add shake animation to inputs
3090 - var emailInput = getElementDOM(botId, 'user-email');
3091 - var nameInput = getElementDOM(botId, 'user-name');
3092 -
3093 - if (emailInput) {
3094 - emailInput.classList.add('email-input-shake');
3095 - setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
3096 - }
3097 - if (nameInput) {
3098 - nameInput.classList.add('email-input-shake');
3099 - setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
3100 - }
3101 - }
3102 -
3103 - function clearEmailError(botId) {
3104 - var emailForm = getElementDOM(botId, 'email-collection-form');
3105 - if (emailForm) {
2792 + function clearEmailError() {
3106 2793 const existingErrors = emailForm.querySelectorAll('.email-error');
3107 2794 existingErrors.forEach(error => error.remove());
3108 2795 }
3109 - }
3110 2796
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 - }
2797 + // MAIN FORM SUBMIT HANDLER
2798 + // Remove any existing event listeners first
2799 + emailForm.removeEventListener('submit', handleFormSubmit);
3123 2800
3124 - function checkSessionAndEmailForBot(botId) {
3125 - const sessionId = MxChatInstances.ensureSession(botId);
2801 + // Add the form submit handler
2802 + emailForm.addEventListener('submit', handleFormSubmit);
3126 2803
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);
2804 + function handleFormSubmit(event) {
2805 + event.preventDefault();
2806 + event.stopPropagation();
3133 2807
3134 - fetch(mxchatChat.ajax_url, {
3135 - method: 'POST',
3136 - headers: {
3137 - 'Content-Type': 'application/x-www-form-urlencoded',
3138 - },
3139 - body: new URLSearchParams({
3140 - action: 'mxchat_check_email_provided',
3141 - session_id: sessionId,
3142 - nonce: mxchatChat.nonce,
3143 - })
3144 - })
3145 - .then((response) => {
3146 - if (!response.ok) {
3147 - throw new Error(`HTTP error! status: ${response.status}`);
2808 + // Prevent double submission
2809 + if (isSubmitting) {
2810 + return false;
3148 2811 }
3149 - return response.json();
3150 - })
3151 - .then((data) => {
3152 - if (data.success) {
3153 - if (data.data.logged_in || data.data.email) {
3154 - showChatContainerForBot(botId);
3155 - } else {
3156 - showEmailFormForBot(botId);
3157 - }
3158 - } else {
3159 - showEmailFormForBot(botId);
3160 - }
3161 - })
3162 - .catch((error) => {
3163 - showEmailFormForBot(botId);
3164 - });
3165 - }
3166 2812
3167 - // Event delegation for email form submission
3168 - $(document).on('submit', '.email-collection-form', function(e) {
3169 - e.preventDefault();
3170 - e.stopPropagation();
2813 + const userEmail = document.getElementById('user-email').value.trim();
2814 + const nameInput = document.getElementById('user-name');
2815 + const userName = nameInput ? nameInput.value.trim() : '';
2816 + const sessionId = getChatSession();
3171 2817
3172 - var botId = getBotIdFromElement(this);
2818 + // Validate email before submission
2819 + if (!userEmail) {
2820 + showEmailError('Please enter your email address.');
2821 + return false;
2822 + }
3173 2823
3174 - // Prevent double submission
3175 - if (emailSubmittingState[botId]) {
3176 - return false;
3177 - }
2824 + if (!isValidEmail(userEmail)) {
2825 + showEmailError('Please enter a valid email address.');
2826 + return false;
2827 + }
3178 2828
3179 - var emailInput = getElementDOM(botId, 'user-email');
3180 - var nameInput = getElementDOM(botId, 'user-name');
3181 - var userEmail = emailInput ? emailInput.value.trim() : '';
3182 - var userName = nameInput ? nameInput.value.trim() : '';
3183 - var sessionId = MxChatInstances.ensureSession(botId);
2829 + // Validate name if field exists
2830 + if (nameInput && !isValidName(userName)) {
2831 + showEmailError('Please enter a valid name (2-100 characters).');
2832 + return false;
2833 + }
3184 2834
3185 - // Validate email
3186 - if (!userEmail) {
3187 - showEmailError(botId, 'Please enter your email address.');
3188 - return false;
3189 - }
2835 + // Clear any existing errors
2836 + clearEmailError();
2837 + setSubmissionState(true);
3190 2838
3191 - if (!isValidEmailAddress(userEmail)) {
3192 - showEmailError(botId, 'Please enter a valid email address.');
3193 - return false;
3194 - }
2839 + // Prepare form data with optional name
2840 + const formData = new URLSearchParams({
2841 + action: 'mxchat_handle_save_email_and_response',
2842 + email: userEmail,
2843 + session_id: sessionId,
2844 + nonce: mxchatChat.nonce,
2845 + });
3195 2846
3196 - // Validate name if field exists and has content
3197 - if (nameInput && userName && !isValidNameInput(userName)) {
3198 - showEmailError(botId, 'Please enter a valid name (2-100 characters).');
3199 - return false;
3200 - }
2847 + // Add name to form data if provided
2848 + if (userName) {
2849 + formData.append('name', userName);
2850 + }
3201 2851
3202 - clearEmailError(botId);
3203 - setEmailSubmissionState(botId, true);
2852 + fetch(mxchatChat.ajax_url, {
2853 + method: 'POST',
2854 + headers: {
2855 + 'Content-Type': 'application/x-www-form-urlencoded',
2856 + },
2857 + body: formData
2858 + })
2859 + .then((response) => {
2860 + if (!response.ok) {
2861 + throw new Error(`HTTP error! status: ${response.status}`);
2862 + }
2863 + return response.json();
2864 + })
2865 + .then((data) => {
2866 + setSubmissionState(false);
3204 2867
3205 - // Prepare form data
3206 - const formData = new URLSearchParams({
3207 - action: 'mxchat_handle_save_email_and_response',
3208 - email: userEmail,
3209 - session_id: sessionId,
3210 - nonce: mxchatChat.nonce,
3211 - });
2868 + if (data.success) {
2869 + // Show chat immediately
2870 + showChatContainer();
3212 2871
3213 - if (userName) {
3214 - formData.append('name', userName);
2872 + // Handle bot response if provided
2873 + if (data.message && typeof appendMessage === 'function') {
2874 + setTimeout(() => {
2875 + appendMessage('bot', data.message);
2876 + if (typeof scrollToBottom === 'function') {
2877 + scrollToBottom();
2878 + }
2879 + }, 100);
2880 + }
2881 + } else {
2882 + showEmailError(data.message || 'Failed to save email. Please try again.');
2883 + }
2884 + })
2885 + .catch((error) => {
2886 + setSubmissionState(false);
2887 + showEmailError('An error occurred. Please try again.');
2888 + });
2889 +
2890 + return false; // Extra prevention
3215 2891 }
3216 2892
3217 - fetch(mxchatChat.ajax_url, {
3218 - method: 'POST',
3219 - headers: {
3220 - 'Content-Type': 'application/x-www-form-urlencoded',
3221 - },
3222 - body: formData
3223 - })
3224 - .then((response) => {
3225 - if (!response.ok) {
3226 - throw new Error(`HTTP error! status: ${response.status}`);
3227 - }
3228 - return response.json();
3229 - })
3230 - .then((data) => {
3231 - setEmailSubmissionState(botId, false);
2893 + // Real-time email validation
2894 + const emailInput = document.getElementById('user-email');
2895 + if (emailInput) {
2896 + let validationTimeout;
2897 +
2898 + emailInput.addEventListener('input', function() {
2899 + // Clear previous validation timeout
2900 + if (validationTimeout) {
2901 + clearTimeout(validationTimeout);
2902 + }
2903 +
2904 + // Debounce validation
2905 + validationTimeout = setTimeout(() => {
2906 + const email = this.value.trim();
2907 + clearEmailError();
2908 +
2909 + if (email && !isValidEmail(email)) {
2910 + showEmailError('Please enter a valid email address.');
2911 + }
2912 + }, 500);
2913 + });
3232 2914
3233 - if (data.success) {
3234 - showChatContainerForBot(botId);
2915 + // Handle Enter key
2916 + emailInput.addEventListener('keypress', function(e) {
2917 + if (e.key === 'Enter' && !isSubmitting) {
2918 + e.preventDefault();
2919 + emailForm.dispatchEvent(new Event('submit'));
2920 + }
2921 + });
2922 + }
3235 2923
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, '');
2924 + // Real-time name validation
2925 + const nameInput = document.getElementById('user-name');
2926 + if (nameInput) {
2927 + let nameValidationTimeout;
2928 +
2929 + nameInput.addEventListener('input', function() {
2930 + // Clear previous validation timeout
2931 + if (nameValidationTimeout) {
2932 + clearTimeout(nameValidationTimeout);
3242 2933 }
2934 +
2935 + // Debounce validation
2936 + nameValidationTimeout = setTimeout(() => {
2937 + const name = this.value.trim();
2938 + clearEmailError();
2939 +
2940 + if (name && !isValidName(name)) {
2941 + showEmailError('Name must be between 2 and 100 characters.');
2942 + }
2943 + }, 500);
2944 + });
3243 2945
3244 - if (data.message && typeof appendMessage === 'function') {
3245 - setTimeout(() => {
3246 - appendMessage('bot', data.message, '', [], false, botId);
3247 - if (typeof scrollToBottom === 'function') {
3248 - scrollToBottom(botId);
3249 - }
3250 - }, 100);
2946 + // Handle Enter key
2947 + nameInput.addEventListener('keypress', function(e) {
2948 + if (e.key === 'Enter' && !isSubmitting) {
2949 + e.preventDefault();
2950 + emailForm.dispatchEvent(new Event('submit'));
3251 2951 }
2952 + });
2953 + }
2954 +
2955 + // Initial state check
2956 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2957 + const emailState = mxchatChat.initial_email_state;
2958 + if (emailState.show_email_form) {
2959 + showEmailForm();
3252 2960 } else {
3253 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
2961 + showChatContainer();
3254 2962 }
3255 - })
3256 - .catch((error) => {
3257 - setEmailSubmissionState(botId, false);
3258 - showEmailError(botId, 'An error occurred. Please try again.');
3259 - });
3260 -
3261 - return false;
3262 - });
3263 -
3264 - // Real-time email validation using event delegation
3265 - $(document).on('input', '.mxchat-email-input', function() {
3266 - var botId = getBotIdFromElement(this);
3267 - var $input = $(this);
3268 -
3269 - // Clear previous timeout
3270 - clearTimeout($input.data('validationTimeout'));
3271 -
3272 - // Debounce validation
3273 - var timeout = setTimeout(() => {
3274 - var email = this.value.trim();
3275 - clearEmailError(botId);
3276 -
3277 - if (email && !isValidEmailAddress(email)) {
3278 - showEmailError(botId, 'Please enter a valid email address.');
3279 - }
3280 - }, 500);
3281 -
3282 - $input.data('validationTimeout', timeout);
3283 - });
3284 -
3285 - // Handle Enter key in email input
3286 - $(document).on('keypress', '.mxchat-email-input', function(e) {
3287 - if (e.key === 'Enter') {
3288 - e.preventDefault();
3289 - var botId = getBotIdFromElement(this);
3290 - if (!emailSubmittingState[botId]) {
3291 - $(this).closest('.email-collection-form').submit();
3292 - }
2963 + } else {
2964 + // Check email status via AJAX
2965 + setTimeout(checkSessionAndEmail, 100);
3293 2966 }
3294 - });
3295 2967
3296 - // Handle Enter key in name input
3297 - $(document).on('keypress', '.mxchat-name-input', function(e) {
3298 - if (e.key === 'Enter') {
3299 - e.preventDefault();
3300 - var botId = getBotIdFromElement(this);
3301 - if (!emailSubmittingState[botId]) {
3302 - $(this).closest('.email-collection-form').submit();
3303 - }
2968 + // Check if email exists for the current session
2969 + function checkSessionAndEmail() {
2970 + const sessionId = getChatSession();
2971 +
2972 + fetch(mxchatChat.ajax_url, {
2973 + method: 'POST',
2974 + headers: {
2975 + 'Content-Type': 'application/x-www-form-urlencoded',
2976 + },
2977 + body: new URLSearchParams({
2978 + action: 'mxchat_check_email_provided',
2979 + session_id: sessionId,
2980 + nonce: mxchatChat.nonce,
2981 + })
2982 + })
2983 + .then((response) => {
2984 + if (!response.ok) {
2985 + throw new Error(`HTTP error! status: ${response.status}`);
2986 + }
2987 + return response.json();
2988 + })
2989 + .then((data) => {
2990 + if (data.success) {
2991 + if (data.data.logged_in || data.data.email) {
2992 + showChatContainer();
2993 + } else {
2994 + showEmailForm();
2995 + }
2996 + } else {
2997 + // On error, default to showing email form
2998 + showEmailForm();
2999 + }
3000 + })
3001 + .catch((error) => {
3002 + // Email check failed - default to email form
3003 + showEmailForm();
3004 + });
3304 3005 }
3305 - });
3306 3006
3307 - // 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 - $('.mxchat-chatbot-wrapper').each(function() {
3311 - var botId = $(this).data('bot-id') || 'default';
3312 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3313 -
3314 - if (emailBlocker) {
3315 - if (isEmbeddedBot(botId)) {
3316 - // Embedded bots are always visible — check now
3317 - resolveEmailState(botId);
3318 - }
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 - }
3328 - });
3007 + } else {
3008 + // Email collection is enabled but essential elements are missing - silently continue
3009 + }
3329 3010 }
3330 3011
3331 3012 // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
3332 3013 $(document).on('click', '.pre-chat-message', function() {
@@ -3334,32 +3015,39 @@
3334 3015 var $chatbot = getElement(botId, 'floating-chatbot');
3335 3016 if ($chatbot.hasClass('hidden')) {
3336 3017 $chatbot.removeClass('hidden').addClass('visible');
3337 3018 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3338 - handlePreChatDismissal(botId);
3019 + $(this).fadeOut(250); // Hide pre-chat message
3339 3020 disableScroll(); // Disable scroll when chatbot opens
3021 + }
3022 + });
3340 3023
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 - }
3024 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
3025 + // This is a fallback for legacy support
3026 + $(document).on('click', '.close-pre-chat-message', function() {
3027 + var botId = getBotIdFromElement(this);
3028 + var $preChat = getElement(botId, 'pre-chat-message');
3029 + $preChat.fadeOut(200); // Hide the message
3346 3030
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);
3031 + // Send an AJAX request to set the transient flag for 24 hours
3032 + $.ajax({
3033 + url: mxchatChat.ajax_url,
3034 + type: 'POST',
3035 + data: {
3036 + action: 'mxchat_dismiss_pre_chat_message',
3037 + _ajax_nonce: mxchatChat.nonce
3038 + },
3039 + success: function() {
3040 + // Ensure the message is hidden after dismissal
3041 + $preChat.hide();
3042 + },
3043 + error: function() {
3044 + // Error dismissing pre-chat message - silently continue
3355 3045 }
3356 - }
3046 + });
3357 3047 });
3358 3048
3359 - // Legacy duplicate close handler removed — handled by single event delegation above
3360 3049
3361 -
3362 3050 function hasQuickQuestions(botId) {
3363 3051 botId = botId || 'default';
3364 3052 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3365 3053 if (!questionsContainer) return false;
@@ -3495,11 +3183,18 @@
3495 3183 });
3496 3184
3497 3185 // Initialize when document is ready
3498 3186 setFullHeight();
3187 + trackOriginatingPage();
3499 3188
3500 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3501 - // until the user's first interaction via MxChatInstances.ensureSession()
3189 + // Only load chat history if email collection is disabled
3190 + if (mxchatChat.email_collection_enabled !== 'on') {
3191 + // Load history for all instances
3192 + $('.mxchat-chatbot-wrapper').each(function() {
3193 + var botId = $(this).data('bot-id') || 'default';
3194 + loadChatHistory(botId);
3195 + });
3196 + }
3502 3197
3503 3198 // Initialize chat visibility for all instances
3504 3199 $('.mxchat-chatbot-wrapper').each(function() {
3505 3200 var botId = $(this).data('bot-id') || 'default';