PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.2
MxChat – AI Chatbot & Content Generation for WordPress v3.1.2
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 +189 -691 3.2.53.1.2 View file →
@@ -1,20 +1,12 @@
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;
3 + // Refresh nonce on load — fixes stale nonces from page-cache plugins
4 + if (typeof mxchatChat !== 'undefined' && mxchatChat.ajax_url) {
12 5 $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' }, function(res) {
13 6 if (res && res.success && res.data && res.data.nonce) {
14 7 mxchatChat.nonce = res.data.nonce;
15 8 }
16 - if (callback) callback();
17 9 });
18 10 }
19 11
20 12 // ====================================
@@ -33,9 +25,9 @@
33 25 var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
34 26
35 27 this.instances[botId] = {
36 28 botId: botId,
37 - sessionId: null,
29 + sessionId: this.getChatSession(botId),
38 30 lastSeenMessageId: '',
39 31 notificationCheckInterval: null,
40 32 pollingInterval: null,
41 33 processedMessageIds: new Set(),
@@ -60,77 +52,23 @@
60 52 return Object.keys(this.instances);
61 53 },
62 54
63 55 // 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 56 getChatSession: function(botId) {
67 57 var cookieName = 'mxchat_session_id_' + botId;
68 - var storageKey = 'mxchat_session_id_' + botId;
69 58 var sessionId = getCookie(cookieName);
70 59
71 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
72 60 if (!sessionId) {
73 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
61 + sessionId = generateSessionId();
62 + this.setChatSession(botId, sessionId);
74 63 }
75 64
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;
65 + return sessionId;
95 66 },
96 67
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 68 setChatSession: function(botId, sessionId) {
129 69 var cookieName = 'mxchat_session_id_' + botId;
130 - var storageKey = 'mxchat_session_id_' + botId;
131 70 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
132 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
133 71 if (this.instances[botId]) {
134 72 this.instances[botId].sessionId = sessionId;
135 73 }
136 74 },
@@ -135,10 +73,8 @@
135 73 }
136 74 },
137 75
138 76 resetChatSession: function(botId) {
139 - // Clear old session from localStorage before setting new one
140 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
141 77 var newSessionId = generateSessionId();
142 78 this.setChatSession(botId, newSessionId);
143 79 var $chatBox = getElement(botId, 'chat-box');
144 80 if ($chatBox.length) {
@@ -147,20 +83,8 @@
147 83 if (this.instances[botId]) {
148 84 this.instances[botId].chatHistoryLoaded = false;
149 85 this.instances[botId].processedMessageIds = new Set();
150 86 }
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 87 }
164 88 };
165 89
166 90 // ====================================
@@ -465,9 +389,8 @@
465 389
466 390 // Update your existing sendMessage function
467 391 function sendMessage(botId) {
468 392 botId = botId || 'default';
469 - MxChatInstances.ensureSession(botId);
470 393 var $chatInput = getElement(botId, 'chat-input');
471 394 var message = $chatInput.val();
472 395
473 396 // ADD PROMPT HOOK HERE
@@ -510,9 +433,8 @@
510 433
511 434 // Update your existing sendMessageToChatbot function
512 435 function sendMessageToChatbot(message, botId) {
513 436 botId = botId || 'default';
514 - MxChatInstances.ensureSession(botId);
515 437
516 438 // ADD PROMPT HOOK HERE
517 439 if (typeof customMxChatFilter === 'function') {
518 440 message = customMxChatFilter(message, "prompt");
@@ -612,23 +534,13 @@
612 534
613 535 // Get instance for session start timestamp (used when persistence is OFF)
614 536 var instance = MxChatInstances.get(botId);
615 537
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 538 // Prepare AJAX data
627 539 const ajaxData = {
628 540 action: 'mxchat_handle_chat_request',
629 541 message: message,
630 - session_id: sessionId,
542 + session_id: getChatSession(botId),
631 543 nonce: mxchatChat.nonce,
632 544 current_page_url: window.location.href,
633 545 current_page_title: document.title,
634 546 bot_id: botId,
@@ -690,16 +602,23 @@
690 602 errorMessage = "An error occurred. Please try again or contact support.";
691 603 }
692 604
693 605 // Handle session reset action (IP changed, session expired, etc.)
694 - // Silent reset — keep chat UI intact, just get a new session and retry
695 606 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)
607 + // Clear the old session and generate a new one
608 + resetChatSession(botId);
609 + // Remove the temporary loading message
610 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
611 + // Re-send the original message with the new session
698 612 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
699 613 if (originalMessage) {
700 614 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
701 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
615 + // Re-add the user message and thinking indicator
616 + appendMessage("user", originalMessage, '', [], false, botId);
617 + appendThinkingMessage(botId);
618 + scrollToBottom(botId);
619 + // Determine whether to use streaming
620 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
702 621 if (shouldUseStreaming(currentModel)) {
703 622 callMxChatStream(originalMessage, function(response) {
704 623 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
705 624 }, botId);
@@ -793,13 +712,9 @@
793 712 $badge.show();
794 713 }
795 714 }
796 715 } 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);
716 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
802 717 }
803 718
804 719 if (response.message_id) {
805 720 var instance = MxChatInstances.get(botId);
@@ -861,22 +776,12 @@
861 776
862 777 // Get instance for session start timestamp (used when persistence is OFF)
863 778 var instance = MxChatInstances.get(botId);
864 779
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 780 const formData = new FormData();
876 781 formData.append('action', 'mxchat_stream_chat');
877 782 formData.append('message', message);
878 - formData.append('session_id', streamSessionId);
783 + formData.append('session_id', getChatSession(botId));
879 784 formData.append('nonce', mxchatChat.nonce);
880 785 formData.append('current_page_url', window.location.href);
881 786 formData.append('current_page_title', document.title);
882 787 formData.append('bot_id', botId);
@@ -976,16 +881,8 @@
976 881
977 882 // Re-enable chat input when stream ends with content
978 883 enableChatInput(botId);
979 884
980 - // Scroll the user's last message to the top now that the
981 - // bot's full reply has rendered (gives max reading room).
982 - var $chatBoxDone = getElement(botId, 'chat-box');
983 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
984 - if ($lastUserMsgDone.length) {
985 - scrollElementToTop($lastUserMsgDone, botId);
986 - }
987 -
988 885 if (callback) {
989 886 callback(accumulatedContent);
990 887 }
991 888 return;
@@ -1008,16 +905,8 @@
1008 905
1009 906 // Re-enable chat input after streaming completes
1010 907 enableChatInput(botId);
1011 908
1012 - // Scroll the user's last message to the top now
1013 - // that the bot's full reply has rendered.
1014 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1015 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1016 - if ($lastUserMsgStreamDone.length) {
1017 - scrollElementToTop($lastUserMsgStreamDone, botId);
1018 - }
1019 -
1020 909 if (callback) {
1021 910 callback(accumulatedContent);
1022 911 }
1023 912 return;
@@ -1136,16 +1025,21 @@
1136 1025 errorMessage = "An error occurred. Please try again or contact support.";
1137 1026 }
1138 1027
1139 1028 // Handle session reset action (IP changed, session expired, etc.)
1140 - // Silent reset — keep chat UI intact, just get a new session and retry
1141 1029 if (data.data && data.data.action === 'reset_session') {
1142 - MxChatInstances.silentResetSession(botId);
1143 - // Re-send the original message with the new session (user message is already displayed)
1030 + // Clear the old session and generate a new one
1031 + resetChatSession(botId);
1032 + // Re-send the original message with the new session
1144 1033 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1145 1034 if (originalMessage) {
1146 1035 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1147 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1036 + // Re-add the user message and thinking indicator
1037 + appendMessage("user", originalMessage, '', [], false, botId);
1038 + appendThinkingMessage(botId);
1039 + scrollToBottom(botId);
1040 + // Determine whether to use streaming
1041 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1148 1042 if (shouldUseStreaming(currentModel)) {
1149 1043 callMxChatStream(originalMessage, callback, botId);
1150 1044 } else {
1151 1045 callMxChat(originalMessage, callback, botId);
@@ -1327,247 +1221,8 @@
1327 1221 }
1328 1222 });
1329 1223
1330 1224
1331 -// Tags the chat-box transcript so the print stylesheet can target it,
1332 -// and lazily initializes the header overflow menu for this bot if the
1333 -// markup is present but not yet wired (covers dynamically-rendered widgets).
1334 -// Idempotent; safe to call on every appended message.
1335 -function mxchatEnsurePrintRoot(botId) {
1336 - var $chatBox = getElement(botId, 'chat-box');
1337 - if (!$chatBox || !$chatBox.length) return;
1338 - $chatBox.addClass('mxchat-conversation-print-root');
1339 - if (!$chatBox.attr('data-print-title')) {
1340 - var nowStr = new Date().toLocaleString();
1341 - var headerTitle = ((typeof mxchatChat !== 'undefined' && mxchatChat.print_header_title) || 'Chat transcript') + ' — ' + nowStr;
1342 - $chatBox.attr('data-print-title', headerTitle);
1343 - }
1344 - if (typeof mxchatInitHeaderMenu === 'function') {
1345 - mxchatInitHeaderMenu(botId);
1346 - }
1347 -}
1348 -
1349 -// Builds the list of overflow-menu items for a given bot.
1350 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1351 -function mxchatGetHeaderMenuItems(botId) {
1352 - var items = [];
1353 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1354 -
1355 - // The `print_button_*` keys still gate this item for back-compat with
1356 - // existing user options. The action is now a transcript download, not print.
1357 - if (settings.print_button_enabled === 'on') {
1358 - items.push({
1359 - id: 'download-transcript',
1360 - label: settings.print_button_label || 'Download Transcript',
1361 - icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
1362 - action: function() {
1363 - mxchatDownloadTranscript(botId);
1364 - }
1365 - });
1366 - }
1367 -
1368 - return items;
1369 -}
1370 -
1371 -// Builds a clean markdown transcript of the current conversation and triggers
1372 -// a file download. Used by the "Download Transcript" menu item.
1373 -function mxchatDownloadTranscript(botId) {
1374 - var $chatBox = getElement(botId, 'chat-box');
1375 - if (!$chatBox || !$chatBox.length) return;
1376 -
1377 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1378 - var headerTitle = settings.print_header_title || 'Chat transcript';
1379 - var now = new Date();
1380 - var stamp = now.toLocaleString();
1381 -
1382 - var lines = [];
1383 - lines.push('# ' + headerTitle);
1384 - lines.push('');
1385 - lines.push('Exported: ' + stamp);
1386 - lines.push('');
1387 - lines.push('---');
1388 - lines.push('');
1389 -
1390 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1391 - var $msg = $(this);
1392 - // Skip thinking placeholders and any in-flight temporary messages.
1393 - if ($msg.find('.thinking-dots').length) return;
1394 - if ($msg.hasClass('temporary-message')) return;
1395 -
1396 - var sender;
1397 - if ($msg.hasClass('user-message')) sender = 'User';
1398 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1399 - else sender = 'AI Agent';
1400 -
1401 - // Strip interactive UI from the cloned message so we get the conversation text.
1402 - var $clone = $msg.clone();
1403 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1404 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1405 - if (!text) return;
1406 -
1407 - lines.push('**' + sender + '**');
1408 - lines.push('');
1409 - lines.push(text);
1410 - lines.push('');
1411 - });
1412 -
1413 - var content = lines.join('\n');
1414 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1415 - var fname = 'mxchat-transcript-' + iso + '.md';
1416 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1417 - var url = URL.createObjectURL(blob);
1418 - var a = document.createElement('a');
1419 - a.href = url;
1420 - a.download = fname;
1421 - a.style.display = 'none';
1422 - document.body.appendChild(a);
1423 - a.click();
1424 - setTimeout(function() {
1425 - if (a.parentNode) a.parentNode.removeChild(a);
1426 - URL.revokeObjectURL(url);
1427 - }, 100);
1428 -}
1429 -
1430 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1431 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1432 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1433 -function mxchatSyncMenuColors(botId, $wrap) {
1434 - if (!$wrap || !$wrap.length) return;
1435 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1436 - if (!$bot.length) return;
1437 - var cs = window.getComputedStyle($bot[0]);
1438 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1439 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1440 - }
1441 - // Bot text color usually lives on a child div, not .bot-message itself.
1442 - var $textChild = $bot.find('[style*="color"]').first();
1443 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1444 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1445 -}
1446 -
1447 -// One-time per-widget init: renders menu items, wires open/close,
1448 -// outside-click, Escape, and arrow-key navigation. If no items, hides the trigger.
1449 -function mxchatInitHeaderMenu(botId) {
1450 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1451 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1452 -
1453 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1454 - var $menu = $wrap.find('.mxchat-header-menu');
1455 - var items = mxchatGetHeaderMenuItems(botId);
1456 -
1457 - // Initial color sync — covers normal page load.
1458 - mxchatSyncMenuColors(botId, $wrap);
1459 -
1460 - if (!items.length) {
1461 - $trigger.hide();
1462 - $menu.hide();
1463 - $wrap.data('mxchatMenuReady', true);
1464 - return;
1465 - }
1466 -
1467 - // Build the menu items.
1468 - $menu.empty();
1469 - items.forEach(function(item, idx) {
1470 - var $btn = $('<button>', {
1471 - type: 'button',
1472 - 'class': 'mxchat-menu-item',
1473 - 'role': 'menuitem',
1474 - 'tabindex': '-1',
1475 - 'data-menu-id': item.id,
1476 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1477 - '<span class="mxchat-menu-item-label"></span>'
1478 - });
1479 - $btn.find('.mxchat-menu-item-label').text(item.label);
1480 - $btn.on('click', function(e) {
1481 - e.preventDefault();
1482 - e.stopPropagation();
1483 - closeMenu();
1484 - try { item.action(); } catch (err) { /* no-op */ }
1485 - });
1486 - $menu.append($btn);
1487 - });
1488 -
1489 - function openMenu() {
1490 - // Re-sync each open in case the active theme changed since init.
1491 - mxchatSyncMenuColors(botId, $wrap);
1492 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1493 - $trigger.attr('aria-expanded', 'true');
1494 - // Focus the first item for keyboard users
1495 - setTimeout(function() {
1496 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1497 - }, 0);
1498 - }
1499 - function closeMenu(returnFocus) {
1500 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1501 - $trigger.attr('aria-expanded', 'false');
1502 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1503 - if (returnFocus) $trigger.trigger('focus');
1504 - }
1505 -
1506 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1507 - // click-to-collapse handler does not fire.
1508 - $trigger.on('click', function(e) {
1509 - e.preventDefault();
1510 - e.stopPropagation();
1511 - if ($menu.hasClass('is-open')) closeMenu();
1512 - else openMenu();
1513 - });
1514 -
1515 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1516 - $menu.on('click', function(e) {
1517 - e.stopPropagation();
1518 - });
1519 -
1520 - // Outside click closes the menu.
1521 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1522 - if (!$menu.hasClass('is-open')) return;
1523 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1524 - closeMenu();
1525 - });
1526 -
1527 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1528 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1529 - var $items = $menu.find('.mxchat-menu-item');
1530 - var idx = $items.index(this);
1531 - if (e.key === 'Escape') {
1532 - e.preventDefault();
1533 - closeMenu(true);
1534 - } else if (e.key === 'ArrowDown') {
1535 - e.preventDefault();
1536 - var $next = $items.eq((idx + 1) % $items.length);
1537 - $items.attr('tabindex', '-1');
1538 - $next.attr('tabindex', '0').trigger('focus');
1539 - } else if (e.key === 'ArrowUp') {
1540 - e.preventDefault();
1541 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1542 - $items.attr('tabindex', '-1');
1543 - $prev.attr('tabindex', '0').trigger('focus');
1544 - } else if (e.key === 'Enter' || e.key === ' ') {
1545 - e.preventDefault();
1546 - $(this).trigger('click');
1547 - }
1548 - });
1549 - $trigger.on('keydown', function(e) {
1550 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1551 - e.preventDefault();
1552 - closeMenu(true);
1553 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1554 - e.preventDefault();
1555 - openMenu();
1556 - }
1557 - });
1558 -
1559 - $wrap.data('mxchatMenuReady', true);
1560 -}
1561 -
1562 -// Initialize header menus for every rendered widget on DOM ready.
1563 -$(function() {
1564 - $('.mxchat-header-menu-wrap').each(function() {
1565 - var botId = $(this).data('bot-id');
1566 - if (botId) mxchatInitHeaderMenu(botId);
1567 - });
1568 -});
1569 -
1570 1225 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1571 1226 try {
1572 1227 // Determine styles based on sender type
1573 1228 let messageClass, bgColor, fontColor;
@@ -1605,12 +1260,17 @@
1605 1260 'margin-bottom': '1em'
1606 1261 });
1607 1262 }
1608 1263
1609 - // Process the message content - always run linkify to convert markdown
1610 - // links and format text. linkify() handles existing HTML safely via
1611 - // negative lookaheads that skip URLs already inside <a> tags.
1612 - let fullMessage = linkify(messageText);
1264 + // Process the message content based on sender
1265 + let fullMessage;
1266 + if (sender === "user") {
1267 + // For user messages, apply linkify after sanitization
1268 + fullMessage = linkify(messageText);
1269 + } else {
1270 + // For bot/agent messages, preserve HTML
1271 + fullMessage = messageText;
1272 + }
1613 1273
1614 1274 // Add images if provided
1615 1275 if (images && images.length > 0) {
1616 1276 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1659,12 +1319,8 @@
1659 1319 if (lastUserMessage.length) {
1660 1320 scrollElementToTop(lastUserMessage, botId);
1661 1321 }
1662 1322 }
1663 -
1664 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1665 - mxchatEnsurePrintRoot(botId);
1666 - }
1667 1323 });
1668 1324
1669 1325 if (messageText.id) {
1670 1326 var instance = MxChatInstances.get(botId);
@@ -1749,12 +1405,26 @@
1749 1405 bgColor = botMessageBgColor;
1750 1406 fontColor = botMessageFontColor;
1751 1407 }
1752 1408
1753 - // Always run linkify to convert markdown links and format text.
1754 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1755 - // to avoid double-processing URLs that are already inside <a> tags).
1756 - var fullMessage = linkify(responseText);
1409 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1410 + // This prevents double-processing of URLs that are already formatted as HTML
1411 + var fullMessage;
1412 + if (sender === "user") {
1413 + // Always linkify user messages (they're plain text)
1414 + fullMessage = linkify(responseText);
1415 + } else {
1416 + // For bot/agent messages, check if HTML already exists
1417 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1418 + responseText.includes('<img') || responseText.includes('<div') ||
1419 + responseText.includes('<p>') || responseText.includes('<br>')) {
1420 + // Response already has HTML, don't process it
1421 + fullMessage = responseText;
1422 + } else {
1423 + // Plain text response, apply linkify
1424 + fullMessage = linkify(responseText);
1425 + }
1426 + }
1757 1427
1758 1428 if (responseHtml) {
1759 1429 // Only add line breaks if there's actual text content before the HTML
1760 1430 if (fullMessage && fullMessage.trim()) {
@@ -1811,12 +1481,8 @@
1811 1481 }
1812 1482
1813 1483 // Re-enable chat input after response is displayed
1814 1484 enableChatInput(botId);
1815 -
1816 - if (sender === "bot" || sender === "agent") {
1817 - mxchatEnsurePrintRoot(botId);
1818 - }
1819 1485 } else {
1820 1486 appendMessage(sender, responseText, responseHtml, images, false, botId);
1821 1487 // Re-enable chat input after response is displayed
1822 1488 enableChatInput(botId);
@@ -1901,12 +1567,9 @@
1901 1567 processedText = formatTextStyling(processedText);
1902 1568
1903 1569 // Process code blocks BEFORE processing links
1904 1570 processedText = formatCodeBlocks(processedText);
1905 -
1906 - // Process markdown tables BEFORE converting newlines to paragraphs
1907 - processedText = formatMarkdownTables(processedText);
1908 -
1571 +
1909 1572 // NOW convert to paragraphs
1910 1573 processedText = convertNewlinesToBreaks(processedText);
1911 1574
1912 1575 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1919,63 +1582,37 @@
1919 1582 // Return as a proper link without the brackets
1920 1583 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1921 1584 });
1922 1585
1923 - // Process markdown links: [text](url) and [](url)
1924 - // Uses balanced parenthesis matching to handle URLs containing parens
1925 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
1926 - processedText = (function(input) {
1927 - var result = '';
1928 - var i = 0;
1929 - while (i < input.length) {
1930 - // Look for [ at current position
1931 - if (input[i] === '[') {
1932 - // Find closing ]
1933 - var closeBracket = input.indexOf(']', i + 1);
1934 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
1935 - result += input[i];
1936 - i++;
1937 - continue;
1938 - }
1939 - var linkText = input.substring(i + 1, closeBracket);
1940 - // Check if URL starts with http
1941 - var urlStart = closeBracket + 2;
1942 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
1943 - result += input[i];
1944 - i++;
1945 - continue;
1946 - }
1947 - // Find balanced closing paren
1948 - var depth = 1;
1949 - var j = urlStart;
1950 - while (j < input.length && depth > 0) {
1951 - if (input[j] === '(') depth++;
1952 - else if (input[j] === ')') depth--;
1953 - if (depth > 0) j++;
1954 - }
1955 - if (depth !== 0) {
1956 - result += input[i];
1957 - i++;
1958 - continue;
1959 - }
1960 - var url = input.substring(urlStart, j);
1961 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
1962 - var encodedUrl = safeEncodeUrl(cleanUrl);
1963 - if (!linkText || !linkText.trim()) {
1964 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
1965 - } else {
1966 - var safeText = sanitizeUserInput(linkText);
1967 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
1968 - }
1969 - i = j + 1; // Skip past the closing )
1970 - } else {
1971 - result += input[i];
1972 - i++;
1973 - }
1586 + // Process proper markdown links with text: [text](url)
1587 + // This MUST have non-empty text in the first brackets
1588 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1589 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1590 + // Make sure we have actual text (not just whitespace)
1591 + if (!text || !text.trim()) {
1592 + // If no text, treat the URL as the text
1593 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1594 + const safeUrl = safeEncodeUrl(cleanUrl);
1595 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1974 1596 }
1975 - return result;
1976 - })(processedText);
1597 +
1598 + // Clean the URL
1599 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1600 + const safeUrl = safeEncodeUrl(cleanUrl);
1601 + const safeText = sanitizeUserInput(text);
1602 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1603 + });
1977 1604
1605 + // Handle empty markdown links: [](url)
1606 + // This is a specific case where there's no text
1607 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1608 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1609 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1610 + const safeUrl = safeEncodeUrl(cleanUrl);
1611 + // Use the URL itself as the link text
1612 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1613 + });
1614 +
1978 1615 // Process phone numbers: [text](tel:number)
1979 1616 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1980 1617 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1981 1618 const safePhone = safeEncodeUrl(phone);
@@ -2127,78 +1764,9 @@
2127 1764 });
2128 1765
2129 1766 return text;
2130 1767 }
2131 -
2132 - function formatMarkdownTables(text) {
2133 - var lines = text.split('\n');
2134 - var result = [];
2135 - var i = 0;
2136 -
2137 - while (i < lines.length) {
2138 - // Check for a table: current line has pipes AND next line is a separator row
2139 - if (i + 1 < lines.length &&
2140 - lines[i].indexOf('|') !== -1 &&
2141 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2142 -
2143 - var tableLines = [];
2144 - var headerLine = lines[i];
2145 - var separatorLine = lines[i + 1];
2146 - tableLines.push(headerLine);
2147 - tableLines.push(separatorLine);
2148 -
2149 - // Collect remaining table rows
2150 - var j = i + 2;
2151 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2152 - tableLines.push(lines[j]);
2153 - j++;
2154 - }
2155 -
2156 - // Parse alignment from separator row
2157 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2158 - var alignments = sepCells.map(function(cell) {
2159 - var trimmed = cell.trim();
2160 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2161 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2162 - return 'left';
2163 - });
2164 -
2165 - // Build HTML table
2166 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2167 -
2168 - // Header row
2169 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2170 - html += '<thead><tr>';
2171 - headerCells.forEach(function(cell, idx) {
2172 - var align = alignments[idx] || 'left';
2173 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2174 - });
2175 - html += '</tr></thead>';
2176 -
2177 - // Body rows
2178 - html += '<tbody>';
2179 - for (var r = 2; r < tableLines.length; r++) {
2180 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2181 - html += '<tr>';
2182 - rowCells.forEach(function(cell, idx) {
2183 - var align = alignments[idx] || 'left';
2184 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2185 - });
2186 - html += '</tr>';
2187 - }
2188 - html += '</tbody></table></div>';
2189 -
2190 - result.push(html);
2191 - i = j;
2192 - } else {
2193 - result.push(lines[i]);
2194 - i++;
2195 - }
2196 - }
2197 -
2198 - return result.join('\n');
2199 - }
2200 -
1768 +
2201 1769 function sanitizeUserInput(text) {
2202 1770 const div = document.createElement('div');
2203 1771 div.textContent = text;
2204 1772 return div.innerHTML;
@@ -2269,14 +1837,13 @@
2269 1837 requestAnimationFrame(smoothScroll);
2270 1838 }
2271 1839 }
2272 1840
2273 - function scrollElementToTop(element, botId, topOffset) {
1841 + function scrollElementToTop(element, botId) {
2274 1842 botId = botId || 'default';
2275 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2276 1843 var chatBox = getElement(botId, 'chat-box');
2277 1844 var elementTop = element.position().top + chatBox.scrollTop();
2278 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1845 + chatBox.animate({ scrollTop: elementTop }, 500);
2279 1846 }
2280 1847
2281 1848 function showChatWidget(botId) {
2282 1849 botId = botId || 'default';
@@ -2493,29 +2060,20 @@
2493 2060 // ====================================
2494 2061 // CHAT HISTORY & PERSISTENCE
2495 2062 // ====================================
2496 2063
2497 -function loadChatHistory(botId, onComplete) {
2064 +function loadChatHistory(botId) {
2498 2065 botId = botId || 'default';
2499 2066 var instance = MxChatInstances.get(botId);
2500 2067
2501 2068 // Prevent duplicate loading
2502 2069 if (instance.chatHistoryLoaded) {
2503 - if (onComplete) onComplete();
2504 2070 return;
2505 2071 }
2506 2072
2507 - // Use getChatSession which returns null if no session exists (does NOT create one)
2508 2073 var sessionId = getChatSession(botId);
2509 2074 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2510 2075
2511 - // No session yet — nothing to load. History will load after first message via ensureSession.
2512 - if (!sessionId) {
2513 - instance.chatHistoryLoaded = true;
2514 - if (onComplete) onComplete();
2515 - return;
2516 - }
2517 -
2518 2076 if (chatPersistenceEnabled && sessionId) {
2519 2077 $.ajax({
2520 2078 url: mxchatChat.ajax_url,
2521 2079 type: 'POST',
@@ -2526,12 +2084,11 @@
2526 2084 },
2527 2085 success: function(response) {
2528 2086 // Handle session reset (IP changed while user was away)
2529 2087 if (response.success === false && response.data && response.data.action === 'reset_session') {
2530 - // Silent reset — new session but don't clear UI
2531 - MxChatInstances.silentResetSession(botId);
2088 + // Silently reset session - user will start fresh
2089 + resetChatSession(botId);
2532 2090 instance.chatHistoryLoaded = true; // Prevent retry loop
2533 - if (onComplete) onComplete();
2534 2091 return;
2535 2092 }
2536 2093
2537 2094 // Check if the response indicates success
@@ -2587,19 +2144,9 @@
2587 2144 var content = message.content;
2588 2145 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2589 2146 content = decodeHTMLEntities(content);
2590 2147
2591 - // Skip linkify for messages containing structured HTML
2592 - // (forms, product cards, galleries, etc.) to avoid
2593 - // markdown formatting corrupting HTML attributes
2594 - // (e.g. underscores in name="field_name" becoming <em> tags)
2595 - if (content.includes("mxchat-product-card") ||
2596 - content.includes("mxchat-image-gallery") ||
2597 - content.includes("mxchat-featured-products") ||
2598 - content.includes("<form") ||
2599 - content.includes("<input") ||
2600 - content.includes("<select") ||
2601 - content.includes("<textarea")) {
2148 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2602 2149 messageElement.html(content);
2603 2150 } else {
2604 2151 var formattedContent = linkify(content);
2605 2152 messageElement.html(formattedContent);
@@ -2639,17 +2186,13 @@
2639 2186 instance.chatHistoryLoaded = true;
2640 2187 }
2641 2188 }
2642 2189 }
2643 - if (onComplete) onComplete();
2644 2190 },
2645 2191 error: function(xhr, status, error) {
2646 2192 // Error loading chat history - silently continue
2647 - if (onComplete) onComplete();
2648 2193 }
2649 2194 });
2650 - } else {
2651 - if (onComplete) onComplete();
2652 2195 }
2653 2196 }
2654 2197
2655 2198
@@ -2825,35 +2368,45 @@
2825 2368 // ====================================
2826 2369
2827 2370 function checkPreChatDismissal(botId) {
2828 2371 botId = botId || 'default';
2829 - try {
2830 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2831 - if (dismissedAt) {
2832 - // Re-show after 24 hours
2833 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2834 - if (elapsed < 86400000) {
2372 + $.ajax({
2373 + url: mxchatChat.ajax_url,
2374 + type: 'POST',
2375 + data: {
2376 + action: 'mxchat_check_pre_chat_message_status',
2377 + _ajax_nonce: mxchatChat.nonce
2378 + },
2379 + success: function(response) {
2380 + if (response.success && !response.data.dismissed) {
2381 + getElement(botId, 'pre-chat-message').fadeIn(250);
2382 + } else {
2835 2383 getElement(botId, 'pre-chat-message').hide();
2836 - return;
2837 2384 }
2838 - // Expired — clear and show again
2839 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2385 + },
2386 + error: function() {
2387 + // Error checking pre-chat dismissal - silently continue
2840 2388 }
2841 - getElement(botId, 'pre-chat-message').fadeIn(250);
2842 - } catch (e) {
2843 - // localStorage unavailable — show the message
2844 - getElement(botId, 'pre-chat-message').fadeIn(250);
2845 - }
2389 + });
2846 2390 }
2847 2391
2848 2392 function handlePreChatDismissal(botId) {
2849 2393 botId = botId || 'default';
2850 2394 getElement(botId, 'pre-chat-message').fadeOut(200);
2851 - try {
2852 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2853 - } catch (e) {
2854 - // localStorage unavailable — dismissal won't persist
2855 - }
2395 + $.ajax({
2396 + url: mxchatChat.ajax_url,
2397 + type: 'POST',
2398 + data: {
2399 + action: 'mxchat_dismiss_pre_chat_message',
2400 + _ajax_nonce: mxchatChat.nonce
2401 + },
2402 + success: function() {
2403 + $('#pre-chat-message').hide();
2404 + },
2405 + error: function() {
2406 + // Error dismissing pre-chat message - silently continue
2407 + }
2408 + });
2856 2409 }
2857 2410
2858 2411
2859 2412 // ====================================
@@ -2920,26 +2473,8 @@
2920 2473 $(this).addClass('hidden');
2921 2474 $badge.hide(); // Hide notification when opening chat
2922 2475 disableScroll();
2923 2476 $preChat.fadeOut(250);
2924 -
2925 - // Load chat history for returning visitors (persistence)
2926 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
2927 - if (chatPersistenceEnabled) {
2928 - MxChatInstances.ensureSession(botId);
2929 - }
2930 -
2931 - // Deferred email check — only on first widget open
2932 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2933 - var instance = MxChatInstances.get(botId);
2934 - if (emailBlocker && !instance.emailCheckDone) {
2935 - instance.emailCheckDone = true;
2936 - resolveEmailState(botId);
2937 - } else if (!emailBlocker) {
2938 - // No email collection — still route through showChatContainerForBot
2939 - // so the loader is shown while chat history loads
2940 - showChatContainerForBot(botId);
2941 - }
2942 2477 } else {
2943 2478 $chatbot.removeClass('visible').addClass('hidden');
2944 2479 $(this).removeClass('hidden');
2945 2480 enableScroll();
@@ -2957,9 +2492,11 @@
2957 2492
2958 2493 $(document).on('click', '.close-pre-chat-message', function(e) {
2959 2494 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2960 2495 var botId = getBotIdFromElement(this);
2961 - handlePreChatDismissal(botId);
2496 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2497 + $(this).remove();
2498 + });
2962 2499 });
2963 2500
2964 2501
2965 2502 // PDF upload button handlers - use class selector
@@ -3160,59 +2697,8 @@
3160 2697 });
3161 2698
3162 2699
3163 2700 // ====================================
3164 -// INIT LOADER & CHAT CONTAINER HELPERS
3165 -// ====================================
3166 -// These must be outside the email collection block so they're always available
3167 -// (used by persistence loading even when email collection is off)
3168 -
3169 -function showInitLoader(botId) {
3170 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3171 - if (loader) loader.style.display = 'flex';
3172 -}
3173 -
3174 -function hideInitLoader(botId) {
3175 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3176 - if (loader) loader.style.display = 'none';
3177 -}
3178 -
3179 -function showEmailFormForBot(botId) {
3180 - hideInitLoader(botId);
3181 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3182 - var chatContainer = getElementDOM(botId, 'chat-container');
3183 - if (emailBlocker) emailBlocker.style.display = 'flex';
3184 - if (chatContainer) chatContainer.style.display = 'none';
3185 -}
3186 -
3187 -function showChatContainerForBot(botId) {
3188 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3189 - var chatContainer = getElementDOM(botId, 'chat-container');
3190 - if (emailBlocker) emailBlocker.style.display = 'none';
3191 -
3192 - var instance = MxChatInstances.get(botId);
3193 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3194 -
3195 - // If persistence is on and history hasn't loaded yet, show loader
3196 - // while history loads to prevent flash of empty chat
3197 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3198 - if (chatContainer) chatContainer.style.display = 'none';
3199 - showInitLoader(botId);
3200 - loadChatHistory(botId, function() {
3201 - hideInitLoader(botId);
3202 - if (chatContainer) chatContainer.style.display = 'flex';
3203 - scrollToBottom(botId, true);
3204 - });
3205 - } else {
3206 - hideInitLoader(botId);
3207 - if (chatContainer) chatContainer.style.display = 'flex';
3208 - if (typeof loadChatHistory === 'function') {
3209 - loadChatHistory(botId);
3210 - }
3211 - }
3212 -}
3213 -
3214 -// ====================================
3215 2701 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3216 2702 // ====================================
3217 2703 // Only run email collection setup if it's enabled
3218 2704 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3248,8 +2734,28 @@
3248 2734 `;
3249 2735 document.head.appendChild(style);
3250 2736 }
3251 2737
2738 + // Helper functions for email collection (multi-instance aware)
2739 + function showEmailFormForBot(botId) {
2740 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2741 + var chatContainer = getElementDOM(botId, 'chat-container');
2742 + if (emailBlocker) emailBlocker.style.display = 'flex';
2743 + if (chatContainer) chatContainer.style.display = 'none';
2744 + }
2745 +
2746 + function showChatContainerForBot(botId) {
2747 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2748 + var chatContainer = getElementDOM(botId, 'chat-container');
2749 + if (emailBlocker) emailBlocker.style.display = 'none';
2750 + if (chatContainer) chatContainer.style.display = 'flex';
2751 +
2752 + // Load chat history for this bot
2753 + if (typeof loadChatHistory === 'function') {
2754 + loadChatHistory(botId);
2755 + }
2756 + }
2757 +
3252 2758 function isValidEmailAddress(email) {
3253 2759 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3254 2760 return emailRegex.test(email.trim()) && email.length <= 254;
3255 2761 }
@@ -3371,31 +2877,11 @@
3371 2877 existingErrors.forEach(error => error.remove());
3372 2878 }
3373 2879 }
3374 2880
3375 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3376 - function resolveEmailState(botId) {
3377 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3378 - if (mxchatChat.initial_email_state.show_email_form) {
3379 - showEmailFormForBot(botId);
3380 - } else {
3381 - showChatContainerForBot(botId);
3382 - }
3383 - } else {
3384 - checkSessionAndEmailForBot(botId);
3385 - }
3386 - }
3387 -
3388 2881 function checkSessionAndEmailForBot(botId) {
3389 - const sessionId = MxChatInstances.ensureSession(botId);
2882 + const sessionId = getChatSession(botId);
3390 2883
3391 - // Hide both panels while we check — show loader instead
3392 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3393 - var chatContainer = getElementDOM(botId, 'chat-container');
3394 - if (emailBlocker) emailBlocker.style.display = 'none';
3395 - if (chatContainer) chatContainer.style.display = 'none';
3396 - showInitLoader(botId);
3397 -
3398 2884 fetch(mxchatChat.ajax_url, {
3399 2885 method: 'POST',
3400 2886 headers: {
3401 2887 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3443,9 +2929,9 @@
3443 2929 var emailInput = getElementDOM(botId, 'user-email');
3444 2930 var nameInput = getElementDOM(botId, 'user-name');
3445 2931 var userEmail = emailInput ? emailInput.value.trim() : '';
3446 2932 var userName = nameInput ? nameInput.value.trim() : '';
3447 - var sessionId = MxChatInstances.ensureSession(botId);
2933 + var sessionId = getChatSession(botId);
3448 2934
3449 2935 // Validate email
3450 2936 if (!userEmail) {
3451 2937 showEmailError(botId, 'Please enter your email address.');
@@ -3568,27 +3054,25 @@
3568 3054 }
3569 3055 });
3570 3056
3571 3057 // Initialize email check for all bot instances
3572 - // For floating bots: defer until widget is opened (zero passive AJAX)
3573 - // For embedded bots: check immediately since the form is visible
3574 3058 $('.mxchat-chatbot-wrapper').each(function() {
3575 3059 var botId = $(this).data('bot-id') || 'default';
3576 3060 var emailBlocker = getElementDOM(botId, 'email-blocker');
3577 3061
3062 + // Only check if email blocker exists for this bot
3578 3063 if (emailBlocker) {
3579 - if (isEmbeddedBot(botId)) {
3580 - // Embedded bots are always visible — check now
3581 - resolveEmailState(botId);
3064 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3065 + if (mxchatChat.initial_email_state.show_email_form) {
3066 + showEmailFormForBot(botId);
3067 + } else {
3068 + showChatContainerForBot(botId);
3069 + }
3070 + } else {
3071 + setTimeout(function() {
3072 + checkSessionAndEmailForBot(botId);
3073 + }, 100);
3582 3074 }
3583 - // Floating bots: handled in the widget open handler
3584 - } else if (isEmbeddedBot(botId)) {
3585 - // Embedded bot, no email collection — load history with loader
3586 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3587 - if (chatPersistenceEnabled) {
3588 - MxChatInstances.ensureSession(botId);
3589 - showChatContainerForBot(botId);
3590 - }
3591 3075 }
3592 3076 });
3593 3077 }
3594 3078
@@ -3598,32 +3082,39 @@
3598 3082 var $chatbot = getElement(botId, 'floating-chatbot');
3599 3083 if ($chatbot.hasClass('hidden')) {
3600 3084 $chatbot.removeClass('hidden').addClass('visible');
3601 3085 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3602 - handlePreChatDismissal(botId);
3086 + $(this).fadeOut(250); // Hide pre-chat message
3603 3087 disableScroll(); // Disable scroll when chatbot opens
3088 + }
3089 + });
3604 3090
3605 - // Load chat history for returning visitors (persistence)
3606 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3607 - if (chatPersistenceEnabled) {
3608 - MxChatInstances.ensureSession(botId);
3609 - }
3091 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
3092 + // This is a fallback for legacy support
3093 + $(document).on('click', '.close-pre-chat-message', function() {
3094 + var botId = getBotIdFromElement(this);
3095 + var $preChat = getElement(botId, 'pre-chat-message');
3096 + $preChat.fadeOut(200); // Hide the message
3610 3097
3611 - // Deferred email check — only on first widget open
3612 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3613 - var instance = MxChatInstances.get(botId);
3614 - if (emailBlocker && !instance.emailCheckDone) {
3615 - instance.emailCheckDone = true;
3616 - resolveEmailState(botId);
3617 - } else if (!emailBlocker) {
3618 - showChatContainerForBot(botId);
3098 + // Send an AJAX request to set the transient flag for 24 hours
3099 + $.ajax({
3100 + url: mxchatChat.ajax_url,
3101 + type: 'POST',
3102 + data: {
3103 + action: 'mxchat_dismiss_pre_chat_message',
3104 + _ajax_nonce: mxchatChat.nonce
3105 + },
3106 + success: function() {
3107 + // Ensure the message is hidden after dismissal
3108 + $preChat.hide();
3109 + },
3110 + error: function() {
3111 + // Error dismissing pre-chat message - silently continue
3619 3112 }
3620 - }
3113 + });
3621 3114 });
3622 3115
3623 - // Legacy duplicate close handler removed — handled by single event delegation above
3624 3116
3625 -
3626 3117 function hasQuickQuestions(botId) {
3627 3118 botId = botId || 'default';
3628 3119 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3629 3120 if (!questionsContainer) return false;
@@ -3759,11 +3250,18 @@
3759 3250 });
3760 3251
3761 3252 // Initialize when document is ready
3762 3253 setFullHeight();
3254 + trackOriginatingPage();
3763 3255
3764 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3765 - // until the user's first interaction via MxChatInstances.ensureSession()
3256 + // Only load chat history if email collection is disabled
3257 + if (mxchatChat.email_collection_enabled !== 'on') {
3258 + // Load history for all instances
3259 + $('.mxchat-chatbot-wrapper').each(function() {
3260 + var botId = $(this).data('bot-id') || 'default';
3261 + loadChatHistory(botId);
3262 + });
3263 + }
3766 3264
3767 3265 // Initialize chat visibility for all instances
3768 3266 $('.mxchat-chatbot-wrapper').each(function() {
3769 3267 var botId = $(this).data('bot-id') || 'default';