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 +161 -270 3.2.43.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);
@@ -976,16 +929,8 @@
976 929
977 930 // Re-enable chat input when stream ends with content
978 931 enableChatInput(botId);
979 932
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 933 if (callback) {
989 934 callback(accumulatedContent);
990 935 }
991 936 return;
@@ -1008,16 +953,8 @@
1008 953
1009 954 // Re-enable chat input after streaming completes
1010 955 enableChatInput(botId);
1011 956
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 957 if (callback) {
1021 958 callback(accumulatedContent);
1022 959 }
1023 960 return;
@@ -1136,16 +1073,21 @@
1136 1073 errorMessage = "An error occurred. Please try again or contact support.";
1137 1074 }
1138 1075
1139 1076 // Handle session reset action (IP changed, session expired, etc.)
1140 - // Silent reset — keep chat UI intact, just get a new session and retry
1141 1077 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)
1078 + // Clear the old session and generate a new one
1079 + resetChatSession(botId);
1080 + // Re-send the original message with the new session
1144 1081 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1145 1082 if (originalMessage) {
1146 1083 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1147 - 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';
1148 1090 if (shouldUseStreaming(currentModel)) {
1149 1091 callMxChatStream(originalMessage, callback, botId);
1150 1092 } else {
1151 1093 callMxChat(originalMessage, callback, botId);
@@ -1366,12 +1308,17 @@
1366 1308 'margin-bottom': '1em'
1367 1309 });
1368 1310 }
1369 1311
1370 - // Process the message content - always run linkify to convert markdown
1371 - // links and format text. linkify() handles existing HTML safely via
1372 - // negative lookaheads that skip URLs already inside <a> tags.
1373 - let fullMessage = linkify(messageText);
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 + }
1374 1321
1375 1322 // Add images if provided
1376 1323 if (images && images.length > 0) {
1377 1324 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1506,12 +1453,26 @@
1506 1453 bgColor = botMessageBgColor;
1507 1454 fontColor = botMessageFontColor;
1508 1455 }
1509 1456
1510 - // Always run linkify to convert markdown links and format text.
1511 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1512 - // to avoid double-processing URLs that are already inside <a> tags).
1513 - var fullMessage = linkify(responseText);
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 + }
1514 1475
1515 1476 if (responseHtml) {
1516 1477 // Only add line breaks if there's actual text content before the HTML
1517 1478 if (fullMessage && fullMessage.trim()) {
@@ -1672,63 +1633,37 @@
1672 1633 // Return as a proper link without the brackets
1673 1634 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1674 1635 });
1675 1636
1676 - // Process markdown links: [text](url) and [](url)
1677 - // Uses balanced parenthesis matching to handle URLs containing parens
1678 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
1679 - processedText = (function(input) {
1680 - var result = '';
1681 - var i = 0;
1682 - while (i < input.length) {
1683 - // Look for [ at current position
1684 - if (input[i] === '[') {
1685 - // Find closing ]
1686 - var closeBracket = input.indexOf(']', i + 1);
1687 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
1688 - result += input[i];
1689 - i++;
1690 - continue;
1691 - }
1692 - var linkText = input.substring(i + 1, closeBracket);
1693 - // Check if URL starts with http
1694 - var urlStart = closeBracket + 2;
1695 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
1696 - result += input[i];
1697 - i++;
1698 - continue;
1699 - }
1700 - // Find balanced closing paren
1701 - var depth = 1;
1702 - var j = urlStart;
1703 - while (j < input.length && depth > 0) {
1704 - if (input[j] === '(') depth++;
1705 - else if (input[j] === ')') depth--;
1706 - if (depth > 0) j++;
1707 - }
1708 - if (depth !== 0) {
1709 - result += input[i];
1710 - i++;
1711 - continue;
1712 - }
1713 - var url = input.substring(urlStart, j);
1714 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
1715 - var encodedUrl = safeEncodeUrl(cleanUrl);
1716 - if (!linkText || !linkText.trim()) {
1717 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
1718 - } else {
1719 - var safeText = sanitizeUserInput(linkText);
1720 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
1721 - }
1722 - i = j + 1; // Skip past the closing )
1723 - } else {
1724 - result += input[i];
1725 - i++;
1726 - }
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>`;
1727 1647 }
1728 - return result;
1729 - })(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 + });
1730 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 +
1731 1666 // Process phone numbers: [text](tel:number)
1732 1667 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1733 1668 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1734 1669 const safePhone = safeEncodeUrl(phone);
@@ -2022,14 +1957,13 @@
2022 1957 requestAnimationFrame(smoothScroll);
2023 1958 }
2024 1959 }
2025 1960
2026 - function scrollElementToTop(element, botId, topOffset) {
1961 + function scrollElementToTop(element, botId) {
2027 1962 botId = botId || 'default';
2028 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2029 1963 var chatBox = getElement(botId, 'chat-box');
2030 1964 var elementTop = element.position().top + chatBox.scrollTop();
2031 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1965 + chatBox.animate({ scrollTop: elementTop }, 500);
2032 1966 }
2033 1967
2034 1968 function showChatWidget(botId) {
2035 1969 botId = botId || 'default';
@@ -2256,19 +2190,11 @@
2256 2190 if (onComplete) onComplete();
2257 2191 return;
2258 2192 }
2259 2193
2260 - // Use getChatSession which returns null if no session exists (does NOT create one)
2261 2194 var sessionId = getChatSession(botId);
2262 2195 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2263 2196
2264 - // No session yet — nothing to load. History will load after first message via ensureSession.
2265 - if (!sessionId) {
2266 - instance.chatHistoryLoaded = true;
2267 - if (onComplete) onComplete();
2268 - return;
2269 - }
2270 -
2271 2197 if (chatPersistenceEnabled && sessionId) {
2272 2198 $.ajax({
2273 2199 url: mxchatChat.ajax_url,
2274 2200 type: 'POST',
@@ -2279,10 +2205,10 @@
2279 2205 },
2280 2206 success: function(response) {
2281 2207 // Handle session reset (IP changed while user was away)
2282 2208 if (response.success === false && response.data && response.data.action === 'reset_session') {
2283 - // Silent reset — new session but don't clear UI
2284 - MxChatInstances.silentResetSession(botId);
2209 + // Silently reset session - user will start fresh
2210 + resetChatSession(botId);
2285 2211 instance.chatHistoryLoaded = true; // Prevent retry loop
2286 2212 if (onComplete) onComplete();
2287 2213 return;
2288 2214 }
@@ -2340,19 +2266,9 @@
2340 2266 var content = message.content;
2341 2267 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2342 2268 content = decodeHTMLEntities(content);
2343 2269
2344 - // Skip linkify for messages containing structured HTML
2345 - // (forms, product cards, galleries, etc.) to avoid
2346 - // markdown formatting corrupting HTML attributes
2347 - // (e.g. underscores in name="field_name" becoming <em> tags)
2348 - if (content.includes("mxchat-product-card") ||
2349 - content.includes("mxchat-image-gallery") ||
2350 - content.includes("mxchat-featured-products") ||
2351 - content.includes("<form") ||
2352 - content.includes("<input") ||
2353 - content.includes("<select") ||
2354 - content.includes("<textarea")) {
2270 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2355 2271 messageElement.html(content);
2356 2272 } else {
2357 2273 var formattedContent = linkify(content);
2358 2274 messageElement.html(formattedContent);
@@ -2579,20 +2495,14 @@
2579 2495
2580 2496 function checkPreChatDismissal(botId) {
2581 2497 botId = botId || 'default';
2582 2498 try {
2583 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2584 - if (dismissedAt) {
2585 - // Re-show after 24 hours
2586 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2587 - if (elapsed < 86400000) {
2588 - getElement(botId, 'pre-chat-message').hide();
2589 - return;
2590 - }
2591 - // Expired — clear and show again
2592 - 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();
2593 2504 }
2594 - getElement(botId, 'pre-chat-message').fadeIn(250);
2595 2505 } catch (e) {
2596 2506 // localStorage unavailable — show the message
2597 2507 getElement(botId, 'pre-chat-message').fadeIn(250);
2598 2508 }
@@ -2601,9 +2511,9 @@
2601 2511 function handlePreChatDismissal(botId) {
2602 2512 botId = botId || 'default';
2603 2513 getElement(botId, 'pre-chat-message').fadeOut(200);
2604 2514 try {
2605 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2515 + localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, '1');
2606 2516 } catch (e) {
2607 2517 // localStorage unavailable — dismissal won't persist
2608 2518 }
2609 2519 }
@@ -2674,14 +2584,8 @@
2674 2584 $badge.hide(); // Hide notification when opening chat
2675 2585 disableScroll();
2676 2586 $preChat.fadeOut(250);
2677 2587
2678 - // Load chat history for returning visitors (persistence)
2679 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
2680 - if (chatPersistenceEnabled) {
2681 - MxChatInstances.ensureSession(botId);
2682 - }
2683 -
2684 2588 // Deferred email check — only on first widget open
2685 2589 var emailBlocker = getElementDOM(botId, 'email-blocker');
2686 2590 var instance = MxChatInstances.get(botId);
2687 2591 if (emailBlocker && !instance.emailCheckDone) {
@@ -2686,12 +2590,8 @@
2686 2590 var instance = MxChatInstances.get(botId);
2687 2591 if (emailBlocker && !instance.emailCheckDone) {
2688 2592 instance.emailCheckDone = true;
2689 2593 resolveEmailState(botId);
2690 - } else if (!emailBlocker) {
2691 - // No email collection — still route through showChatContainerForBot
2692 - // so the loader is shown while chat history loads
2693 - showChatContainerForBot(botId);
2694 2594 }
2695 2595 } else {
2696 2596 $chatbot.removeClass('visible').addClass('hidden');
2697 2597 $(this).removeClass('hidden');
@@ -2710,9 +2610,11 @@
2710 2610
2711 2611 $(document).on('click', '.close-pre-chat-message', function(e) {
2712 2612 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2713 2613 var botId = getBotIdFromElement(this);
2714 - handlePreChatDismissal(botId);
2614 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2615 + $(this).remove();
2616 + });
2715 2617 });
2716 2618
2717 2619
2718 2620 // PDF upload button handlers - use class selector
@@ -2913,59 +2815,8 @@
2913 2815 });
2914 2816
2915 2817
2916 2818 // ====================================
2917 -// INIT LOADER & CHAT CONTAINER HELPERS
2918 -// ====================================
2919 -// These must be outside the email collection block so they're always available
2920 -// (used by persistence loading even when email collection is off)
2921 -
2922 -function showInitLoader(botId) {
2923 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2924 - if (loader) loader.style.display = 'flex';
2925 -}
2926 -
2927 -function hideInitLoader(botId) {
2928 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2929 - if (loader) loader.style.display = 'none';
2930 -}
2931 -
2932 -function showEmailFormForBot(botId) {
2933 - hideInitLoader(botId);
2934 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2935 - var chatContainer = getElementDOM(botId, 'chat-container');
2936 - if (emailBlocker) emailBlocker.style.display = 'flex';
2937 - if (chatContainer) chatContainer.style.display = 'none';
2938 -}
2939 -
2940 -function showChatContainerForBot(botId) {
2941 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2942 - var chatContainer = getElementDOM(botId, 'chat-container');
2943 - if (emailBlocker) emailBlocker.style.display = 'none';
2944 -
2945 - var instance = MxChatInstances.get(botId);
2946 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2947 -
2948 - // If persistence is on and history hasn't loaded yet, show loader
2949 - // while history loads to prevent flash of empty chat
2950 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
2951 - if (chatContainer) chatContainer.style.display = 'none';
2952 - showInitLoader(botId);
2953 - loadChatHistory(botId, function() {
2954 - hideInitLoader(botId);
2955 - if (chatContainer) chatContainer.style.display = 'flex';
2956 - scrollToBottom(botId, true);
2957 - });
2958 - } else {
2959 - hideInitLoader(botId);
2960 - if (chatContainer) chatContainer.style.display = 'flex';
2961 - if (typeof loadChatHistory === 'function') {
2962 - loadChatHistory(botId);
2963 - }
2964 - }
2965 -}
2966 -
2967 -// ====================================
2968 2819 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2969 2820 // ====================================
2970 2821 // Only run email collection setup if it's enabled
2971 2822 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3001,8 +2852,40 @@
3001 2852 `;
3002 2853 document.head.appendChild(style);
3003 2854 }
3004 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 +
3005 2888 function isValidEmailAddress(email) {
3006 2889 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3007 2890 return emailRegex.test(email.trim()) && email.length <= 254;
3008 2891 }
@@ -3138,16 +3021,15 @@
3138 3021 }
3139 3022 }
3140 3023
3141 3024 function checkSessionAndEmailForBot(botId) {
3142 - const sessionId = MxChatInstances.ensureSession(botId);
3025 + const sessionId = getChatSession(botId);
3143 3026
3144 - // Hide both panels while we check — show loader instead
3027 + // Hide both panels while we check — prevents flash of wrong state
3145 3028 var emailBlocker = getElementDOM(botId, 'email-blocker');
3146 3029 var chatContainer = getElementDOM(botId, 'chat-container');
3147 3030 if (emailBlocker) emailBlocker.style.display = 'none';
3148 3031 if (chatContainer) chatContainer.style.display = 'none';
3149 - showInitLoader(botId);
3150 3032
3151 3033 fetch(mxchatChat.ajax_url, {
3152 3034 method: 'POST',
3153 3035 headers: {
@@ -3196,9 +3078,9 @@
3196 3078 var emailInput = getElementDOM(botId, 'user-email');
3197 3079 var nameInput = getElementDOM(botId, 'user-name');
3198 3080 var userEmail = emailInput ? emailInput.value.trim() : '';
3199 3081 var userName = nameInput ? nameInput.value.trim() : '';
3200 - var sessionId = MxChatInstances.ensureSession(botId);
3082 + var sessionId = getChatSession(botId);
3201 3083
3202 3084 // Validate email
3203 3085 if (!userEmail) {
3204 3086 showEmailError(botId, 'Please enter your email address.');
@@ -3327,8 +3209,9 @@
3327 3209 $('.mxchat-chatbot-wrapper').each(function() {
3328 3210 var botId = $(this).data('bot-id') || 'default';
3329 3211 var emailBlocker = getElementDOM(botId, 'email-blocker');
3330 3212
3213 + // Only check if email blocker exists for this bot
3331 3214 if (emailBlocker) {
3332 3215 if (isEmbeddedBot(botId)) {
3333 3216 // Embedded bots are always visible — check now
3334 3217 resolveEmailState(botId);
@@ -3333,15 +3216,8 @@
3333 3216 // Embedded bots are always visible — check now
3334 3217 resolveEmailState(botId);
3335 3218 }
3336 3219 // Floating bots: handled in the widget open handler
3337 - } else if (isEmbeddedBot(botId)) {
3338 - // Embedded bot, no email collection — load history with loader
3339 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3340 - if (chatPersistenceEnabled) {
3341 - MxChatInstances.ensureSession(botId);
3342 - showChatContainerForBot(botId);
3343 - }
3344 3220 }
3345 3221 });
3346 3222 }
3347 3223
@@ -3351,17 +3227,11 @@
3351 3227 var $chatbot = getElement(botId, 'floating-chatbot');
3352 3228 if ($chatbot.hasClass('hidden')) {
3353 3229 $chatbot.removeClass('hidden').addClass('visible');
3354 3230 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3355 - handlePreChatDismissal(botId);
3231 + $(this).fadeOut(250); // Hide pre-chat message
3356 3232 disableScroll(); // Disable scroll when chatbot opens
3357 3233
3358 - // Load chat history for returning visitors (persistence)
3359 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3360 - if (chatPersistenceEnabled) {
3361 - MxChatInstances.ensureSession(botId);
3362 - }
3363 -
3364 3234 // Deferred email check — only on first widget open
3365 3235 var emailBlocker = getElementDOM(botId, 'email-blocker');
3366 3236 var instance = MxChatInstances.get(botId);
3367 3237 if (emailBlocker && !instance.emailCheckDone) {
@@ -3366,15 +3236,36 @@
3366 3236 var instance = MxChatInstances.get(botId);
3367 3237 if (emailBlocker && !instance.emailCheckDone) {
3368 3238 instance.emailCheckDone = true;
3369 3239 resolveEmailState(botId);
3370 - } else if (!emailBlocker) {
3371 - showChatContainerForBot(botId);
3372 3240 }
3373 3241 }
3374 3242 });
3375 3243
3376 - // 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 + });
3377 3268
3378 3269
3379 3270 function hasQuickQuestions(botId) {
3380 3271 botId = botId || 'default';