PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.5
MxChat – AI Chatbot & Content Generation for WordPress v3.1.5
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/chat-script.js +159 -251 3.2.33.1.5 View file →
@@ -60,39 +60,18 @@
60 60 return Object.keys(this.instances);
61 61 },
62 62
63 63 // 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 64 getChatSession: function(botId) {
67 65 var cookieName = 'mxchat_session_id_' + botId;
68 - var storageKey = 'mxchat_session_id_' + botId;
69 66 var sessionId = getCookie(cookieName);
70 67
71 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
72 68 if (!sessionId) {
73 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
69 + sessionId = generateSessionId();
70 + this.setChatSession(botId, sessionId);
74 71 }
75 72
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;
73 + return sessionId;
95 74 },
96 75
97 76 // Lazy session initializer — called on first user interaction
98 77 ensureSession: function(botId) {
@@ -102,10 +81,11 @@
102 81 if (instance.sessionId) {
103 82 return instance.sessionId;
104 83 }
105 84
106 - // Check for existing session from cookie or localStorage
107 - var existingSession = this.getChatSession(botId);
85 + // Check if a cookie already exists from a prior visit
86 + var cookieName = 'mxchat_session_id_' + botId;
87 + var existingSession = getCookie(cookieName);
108 88
109 89 if (existingSession) {
110 90 instance.sessionId = existingSession;
111 91 } else {
@@ -118,10 +98,12 @@
118 98 // Now that we have a session, do the deferred work
119 99 refreshNonceIfNeeded();
120 100 trackOriginatingPage();
121 101
122 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
123 - // so we do NOT call it here to avoid a race condition.
102 + var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
103 + if (chatPersistenceEnabled && mxchatChat.email_collection_enabled !== 'on') {
104 + loadChatHistory(botId);
105 + }
124 106
125 107 return instance.sessionId;
126 108 },
127 109
@@ -126,11 +108,9 @@
126 108 },
127 109
128 110 setChatSession: function(botId, sessionId) {
129 111 var cookieName = 'mxchat_session_id_' + botId;
130 - var storageKey = 'mxchat_session_id_' + botId;
131 112 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
132 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
133 113 if (this.instances[botId]) {
134 114 this.instances[botId].sessionId = sessionId;
135 115 }
136 116 },
@@ -135,10 +115,8 @@
135 115 }
136 116 },
137 117
138 118 resetChatSession: function(botId) {
139 - // Clear old session from localStorage before setting new one
140 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
141 119 var newSessionId = generateSessionId();
142 120 this.setChatSession(botId, newSessionId);
143 121 var $chatBox = getElement(botId, 'chat-box');
144 122 if ($chatBox.length) {
@@ -147,20 +125,8 @@
147 125 if (this.instances[botId]) {
148 126 this.instances[botId].chatHistoryLoaded = false;
149 127 this.instances[botId].processedMessageIds = new Set();
150 128 }
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 129 }
164 130 };
165 131
166 132 // ====================================
@@ -612,23 +578,13 @@
612 578
613 579 // Get instance for session start timestamp (used when persistence is OFF)
614 580 var instance = MxChatInstances.get(botId);
615 581
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 582 // Prepare AJAX data
627 583 const ajaxData = {
628 584 action: 'mxchat_handle_chat_request',
629 585 message: message,
630 - session_id: sessionId,
586 + session_id: getChatSession(botId),
631 587 nonce: mxchatChat.nonce,
632 588 current_page_url: window.location.href,
633 589 current_page_title: document.title,
634 590 bot_id: botId,
@@ -690,16 +646,23 @@
690 646 errorMessage = "An error occurred. Please try again or contact support.";
691 647 }
692 648
693 649 // Handle session reset action (IP changed, session expired, etc.)
694 - // Silent reset — keep chat UI intact, just get a new session and retry
695 650 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)
651 + // Clear the old session and generate a new one
652 + resetChatSession(botId);
653 + // Remove the temporary loading message
654 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
655 + // Re-send the original message with the new session
698 656 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
699 657 if (originalMessage) {
700 658 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
701 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
659 + // Re-add the user message and thinking indicator
660 + appendMessage("user", originalMessage, '', [], false, botId);
661 + appendThinkingMessage(botId);
662 + scrollToBottom(botId);
663 + // Determine whether to use streaming
664 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
702 665 if (shouldUseStreaming(currentModel)) {
703 666 callMxChatStream(originalMessage, function(response) {
704 667 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
705 668 }, botId);
@@ -861,22 +824,12 @@
861 824
862 825 // Get instance for session start timestamp (used when persistence is OFF)
863 826 var instance = MxChatInstances.get(botId);
864 827
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 828 const formData = new FormData();
876 829 formData.append('action', 'mxchat_stream_chat');
877 830 formData.append('message', message);
878 - formData.append('session_id', streamSessionId);
831 + formData.append('session_id', getChatSession(botId));
879 832 formData.append('nonce', mxchatChat.nonce);
880 833 formData.append('current_page_url', window.location.href);
881 834 formData.append('current_page_title', document.title);
882 835 formData.append('bot_id', botId);
@@ -1120,16 +1073,21 @@
1120 1073 errorMessage = "An error occurred. Please try again or contact support.";
1121 1074 }
1122 1075
1123 1076 // Handle session reset action (IP changed, session expired, etc.)
1124 - // Silent reset — keep chat UI intact, just get a new session and retry
1125 1077 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)
1078 + // Clear the old session and generate a new one
1079 + resetChatSession(botId);
1080 + // Re-send the original message with the new session
1128 1081 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1129 1082 if (originalMessage) {
1130 1083 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1131 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1084 + // Re-add the user message and thinking indicator
1085 + appendMessage("user", originalMessage, '', [], false, botId);
1086 + appendThinkingMessage(botId);
1087 + scrollToBottom(botId);
1088 + // Determine whether to use streaming
1089 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1132 1090 if (shouldUseStreaming(currentModel)) {
1133 1091 callMxChatStream(originalMessage, callback, botId);
1134 1092 } else {
1135 1093 callMxChat(originalMessage, callback, botId);
@@ -1350,12 +1308,17 @@
1350 1308 'margin-bottom': '1em'
1351 1309 });
1352 1310 }
1353 1311
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);
1312 + // Process the message content based on sender
1313 + let fullMessage;
1314 + if (sender === "user") {
1315 + // For user messages, apply linkify after sanitization
1316 + fullMessage = linkify(messageText);
1317 + } else {
1318 + // For bot/agent messages, preserve HTML
1319 + fullMessage = messageText;
1320 + }
1358 1321
1359 1322 // Add images if provided
1360 1323 if (images && images.length > 0) {
1361 1324 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1490,12 +1453,26 @@
1490 1453 bgColor = botMessageBgColor;
1491 1454 fontColor = botMessageFontColor;
1492 1455 }
1493 1456
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);
1457 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1458 + // This prevents double-processing of URLs that are already formatted as HTML
1459 + var fullMessage;
1460 + if (sender === "user") {
1461 + // Always linkify user messages (they're plain text)
1462 + fullMessage = linkify(responseText);
1463 + } else {
1464 + // For bot/agent messages, check if HTML already exists
1465 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1466 + responseText.includes('<img') || responseText.includes('<div') ||
1467 + responseText.includes('<p>') || responseText.includes('<br>')) {
1468 + // Response already has HTML, don't process it
1469 + fullMessage = responseText;
1470 + } else {
1471 + // Plain text response, apply linkify
1472 + fullMessage = linkify(responseText);
1473 + }
1474 + }
1498 1475
1499 1476 if (responseHtml) {
1500 1477 // Only add line breaks if there's actual text content before the HTML
1501 1478 if (fullMessage && fullMessage.trim()) {
@@ -1656,63 +1633,37 @@
1656 1633 // Return as a proper link without the brackets
1657 1634 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1658 1635 });
1659 1636
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 - }
1637 + // Process proper markdown links with text: [text](url)
1638 + // This MUST have non-empty text in the first brackets
1639 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1640 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1641 + // Make sure we have actual text (not just whitespace)
1642 + if (!text || !text.trim()) {
1643 + // If no text, treat the URL as the text
1644 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1645 + const safeUrl = safeEncodeUrl(cleanUrl);
1646 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1711 1647 }
1712 - return result;
1713 - })(processedText);
1648 +
1649 + // Clean the URL
1650 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1651 + const safeUrl = safeEncodeUrl(cleanUrl);
1652 + const safeText = sanitizeUserInput(text);
1653 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1654 + });
1714 1655
1656 + // Handle empty markdown links: [](url)
1657 + // This is a specific case where there's no text
1658 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1659 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1660 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1661 + const safeUrl = safeEncodeUrl(cleanUrl);
1662 + // Use the URL itself as the link text
1663 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1664 + });
1665 +
1715 1666 // Process phone numbers: [text](tel:number)
1716 1667 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1717 1668 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1718 1669 const safePhone = safeEncodeUrl(phone);
@@ -2239,19 +2190,11 @@
2239 2190 if (onComplete) onComplete();
2240 2191 return;
2241 2192 }
2242 2193
2243 - // Use getChatSession which returns null if no session exists (does NOT create one)
2244 2194 var sessionId = getChatSession(botId);
2245 2195 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2246 2196
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 2197 if (chatPersistenceEnabled && sessionId) {
2255 2198 $.ajax({
2256 2199 url: mxchatChat.ajax_url,
2257 2200 type: 'POST',
@@ -2262,10 +2205,10 @@
2262 2205 },
2263 2206 success: function(response) {
2264 2207 // Handle session reset (IP changed while user was away)
2265 2208 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);
2209 + // Silently reset session - user will start fresh
2210 + resetChatSession(botId);
2268 2211 instance.chatHistoryLoaded = true; // Prevent retry loop
2269 2212 if (onComplete) onComplete();
2270 2213 return;
2271 2214 }
@@ -2323,19 +2266,9 @@
2323 2266 var content = message.content;
2324 2267 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2325 2268 content = decodeHTMLEntities(content);
2326 2269
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")) {
2270 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2338 2271 messageElement.html(content);
2339 2272 } else {
2340 2273 var formattedContent = linkify(content);
2341 2274 messageElement.html(formattedContent);
@@ -2562,20 +2495,14 @@
2562 2495
2563 2496 function checkPreChatDismissal(botId) {
2564 2497 botId = botId || 'default';
2565 2498 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) {
2571 - getElement(botId, 'pre-chat-message').hide();
2572 - return;
2573 - }
2574 - // Expired — clear and show again
2575 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2499 + var dismissed = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2500 + if (!dismissed) {
2501 + getElement(botId, 'pre-chat-message').fadeIn(250);
2502 + } else {
2503 + getElement(botId, 'pre-chat-message').hide();
2576 2504 }
2577 - getElement(botId, 'pre-chat-message').fadeIn(250);
2578 2505 } catch (e) {
2579 2506 // localStorage unavailable — show the message
2580 2507 getElement(botId, 'pre-chat-message').fadeIn(250);
2581 2508 }
@@ -2584,9 +2511,9 @@
2584 2511 function handlePreChatDismissal(botId) {
2585 2512 botId = botId || 'default';
2586 2513 getElement(botId, 'pre-chat-message').fadeOut(200);
2587 2514 try {
2588 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2515 + localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, '1');
2589 2516 } catch (e) {
2590 2517 // localStorage unavailable — dismissal won't persist
2591 2518 }
2592 2519 }
@@ -2657,14 +2584,8 @@
2657 2584 $badge.hide(); // Hide notification when opening chat
2658 2585 disableScroll();
2659 2586 $preChat.fadeOut(250);
2660 2587
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 2588 // Deferred email check — only on first widget open
2668 2589 var emailBlocker = getElementDOM(botId, 'email-blocker');
2669 2590 var instance = MxChatInstances.get(botId);
2670 2591 if (emailBlocker && !instance.emailCheckDone) {
@@ -2669,12 +2590,8 @@
2669 2590 var instance = MxChatInstances.get(botId);
2670 2591 if (emailBlocker && !instance.emailCheckDone) {
2671 2592 instance.emailCheckDone = true;
2672 2593 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 2594 }
2678 2595 } else {
2679 2596 $chatbot.removeClass('visible').addClass('hidden');
2680 2597 $(this).removeClass('hidden');
@@ -2693,9 +2610,11 @@
2693 2610
2694 2611 $(document).on('click', '.close-pre-chat-message', function(e) {
2695 2612 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2696 2613 var botId = getBotIdFromElement(this);
2697 - handlePreChatDismissal(botId);
2614 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2615 + $(this).remove();
2616 + });
2698 2617 });
2699 2618
2700 2619
2701 2620 // PDF upload button handlers - use class selector
@@ -2896,59 +2815,8 @@
2896 2815 });
2897 2816
2898 2817
2899 2818 // ====================================
2900 -// INIT LOADER & CHAT CONTAINER HELPERS
2901 -// ====================================
2902 -// These must be outside the email collection block so they're always available
2903 -// (used by persistence loading even when email collection is off)
2904 -
2905 -function showInitLoader(botId) {
2906 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2907 - if (loader) loader.style.display = 'flex';
2908 -}
2909 -
2910 -function hideInitLoader(botId) {
2911 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2912 - if (loader) loader.style.display = 'none';
2913 -}
2914 -
2915 -function showEmailFormForBot(botId) {
2916 - hideInitLoader(botId);
2917 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2918 - var chatContainer = getElementDOM(botId, 'chat-container');
2919 - if (emailBlocker) emailBlocker.style.display = 'flex';
2920 - if (chatContainer) chatContainer.style.display = 'none';
2921 -}
2922 -
2923 -function showChatContainerForBot(botId) {
2924 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2925 - var chatContainer = getElementDOM(botId, 'chat-container');
2926 - if (emailBlocker) emailBlocker.style.display = 'none';
2927 -
2928 - var instance = MxChatInstances.get(botId);
2929 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2930 -
2931 - // If persistence is on and history hasn't loaded yet, show loader
2932 - // while history loads to prevent flash of empty chat
2933 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
2934 - if (chatContainer) chatContainer.style.display = 'none';
2935 - showInitLoader(botId);
2936 - loadChatHistory(botId, function() {
2937 - hideInitLoader(botId);
2938 - if (chatContainer) chatContainer.style.display = 'flex';
2939 - scrollToBottom(botId, true);
2940 - });
2941 - } else {
2942 - hideInitLoader(botId);
2943 - if (chatContainer) chatContainer.style.display = 'flex';
2944 - if (typeof loadChatHistory === 'function') {
2945 - loadChatHistory(botId);
2946 - }
2947 - }
2948 -}
2949 -
2950 -// ====================================
2951 2819 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2952 2820 // ====================================
2953 2821 // Only run email collection setup if it's enabled
2954 2822 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -2984,8 +2852,40 @@
2984 2852 `;
2985 2853 document.head.appendChild(style);
2986 2854 }
2987 2855
2856 + // Helper functions for email collection (multi-instance aware)
2857 + function showEmailFormForBot(botId) {
2858 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2859 + var chatContainer = getElementDOM(botId, 'chat-container');
2860 + if (emailBlocker) emailBlocker.style.display = 'flex';
2861 + if (chatContainer) chatContainer.style.display = 'none';
2862 + }
2863 +
2864 + function showChatContainerForBot(botId) {
2865 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2866 + var chatContainer = getElementDOM(botId, 'chat-container');
2867 + if (emailBlocker) emailBlocker.style.display = 'none';
2868 +
2869 + var instance = MxChatInstances.get(botId);
2870 + var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2871 +
2872 + // If persistence is on and history hasn't loaded yet, keep container
2873 + // hidden until history loads to prevent flash of empty chat
2874 + if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
2875 + if (chatContainer) chatContainer.style.display = 'none';
2876 + loadChatHistory(botId, function() {
2877 + if (chatContainer) chatContainer.style.display = 'flex';
2878 + scrollToBottom(botId, true);
2879 + });
2880 + } else {
2881 + if (chatContainer) chatContainer.style.display = 'flex';
2882 + if (typeof loadChatHistory === 'function') {
2883 + loadChatHistory(botId);
2884 + }
2885 + }
2886 + }
2887 +
2988 2888 function isValidEmailAddress(email) {
2989 2889 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2990 2890 return emailRegex.test(email.trim()) && email.length <= 254;
2991 2891 }
@@ -3121,16 +3021,15 @@
3121 3021 }
3122 3022 }
3123 3023
3124 3024 function checkSessionAndEmailForBot(botId) {
3125 - const sessionId = MxChatInstances.ensureSession(botId);
3025 + const sessionId = getChatSession(botId);
3126 3026
3127 - // Hide both panels while we check — show loader instead
3027 + // Hide both panels while we check — prevents flash of wrong state
3128 3028 var emailBlocker = getElementDOM(botId, 'email-blocker');
3129 3029 var chatContainer = getElementDOM(botId, 'chat-container');
3130 3030 if (emailBlocker) emailBlocker.style.display = 'none';
3131 3031 if (chatContainer) chatContainer.style.display = 'none';
3132 - showInitLoader(botId);
3133 3032
3134 3033 fetch(mxchatChat.ajax_url, {
3135 3034 method: 'POST',
3136 3035 headers: {
@@ -3179,9 +3078,9 @@
3179 3078 var emailInput = getElementDOM(botId, 'user-email');
3180 3079 var nameInput = getElementDOM(botId, 'user-name');
3181 3080 var userEmail = emailInput ? emailInput.value.trim() : '';
3182 3081 var userName = nameInput ? nameInput.value.trim() : '';
3183 - var sessionId = MxChatInstances.ensureSession(botId);
3082 + var sessionId = getChatSession(botId);
3184 3083
3185 3084 // Validate email
3186 3085 if (!userEmail) {
3187 3086 showEmailError(botId, 'Please enter your email address.');
@@ -3310,8 +3209,9 @@
3310 3209 $('.mxchat-chatbot-wrapper').each(function() {
3311 3210 var botId = $(this).data('bot-id') || 'default';
3312 3211 var emailBlocker = getElementDOM(botId, 'email-blocker');
3313 3212
3213 + // Only check if email blocker exists for this bot
3314 3214 if (emailBlocker) {
3315 3215 if (isEmbeddedBot(botId)) {
3316 3216 // Embedded bots are always visible — check now
3317 3217 resolveEmailState(botId);
@@ -3316,15 +3216,8 @@
3316 3216 // Embedded bots are always visible — check now
3317 3217 resolveEmailState(botId);
3318 3218 }
3319 3219 // 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 3220 }
3328 3221 });
3329 3222 }
3330 3223
@@ -3334,17 +3227,11 @@
3334 3227 var $chatbot = getElement(botId, 'floating-chatbot');
3335 3228 if ($chatbot.hasClass('hidden')) {
3336 3229 $chatbot.removeClass('hidden').addClass('visible');
3337 3230 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3338 - handlePreChatDismissal(botId);
3231 + $(this).fadeOut(250); // Hide pre-chat message
3339 3232 disableScroll(); // Disable scroll when chatbot opens
3340 3233
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 - }
3346 -
3347 3234 // Deferred email check — only on first widget open
3348 3235 var emailBlocker = getElementDOM(botId, 'email-blocker');
3349 3236 var instance = MxChatInstances.get(botId);
3350 3237 if (emailBlocker && !instance.emailCheckDone) {
@@ -3349,15 +3236,36 @@
3349 3236 var instance = MxChatInstances.get(botId);
3350 3237 if (emailBlocker && !instance.emailCheckDone) {
3351 3238 instance.emailCheckDone = true;
3352 3239 resolveEmailState(botId);
3353 - } else if (!emailBlocker) {
3354 - showChatContainerForBot(botId);
3355 3240 }
3356 3241 }
3357 3242 });
3358 3243
3359 - // Legacy duplicate close handler removed — handled by single event delegation above
3244 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
3245 + // This is a fallback for legacy support
3246 + $(document).on('click', '.close-pre-chat-message', function() {
3247 + var botId = getBotIdFromElement(this);
3248 + var $preChat = getElement(botId, 'pre-chat-message');
3249 + $preChat.fadeOut(200); // Hide the message
3250 +
3251 + // Send an AJAX request to set the transient flag for 24 hours
3252 + $.ajax({
3253 + url: mxchatChat.ajax_url,
3254 + type: 'POST',
3255 + data: {
3256 + action: 'mxchat_dismiss_pre_chat_message',
3257 + _ajax_nonce: mxchatChat.nonce
3258 + },
3259 + success: function() {
3260 + // Ensure the message is hidden after dismissal
3261 + $preChat.hide();
3262 + },
3263 + error: function() {
3264 + // Error dismissing pre-chat message - silently continue
3265 + }
3266 + });
3267 + });
3360 3268
3361 3269
3362 3270 function hasQuickQuestions(botId) {
3363 3271 botId = botId || 'default';