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 +185 -390 3.2.23.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,64 +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, or null if none exists.
65 - // 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 - // Re-sync cookie from localStorage if cookie was lost
77 - if (sessionId && !getCookie(cookieName)) {
78 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
79 - }
80 -
81 - return sessionId || null;
65 + return sessionId;
82 66 },
83 67
84 - // Lazy session initializer — called on first user interaction
85 - ensureSession: function(botId) {
86 - botId = botId || 'default';
87 - var instance = this.instances[botId] || this.init(botId);
88 -
89 - if (instance.sessionId) {
90 - return instance.sessionId;
91 - }
92 -
93 - // Check for existing session from cookie or localStorage
94 - var existingSession = this.getChatSession(botId);
95 -
96 - if (existingSession) {
97 - instance.sessionId = existingSession;
98 - } else {
99 - // Brand new session
100 - var newId = generateSessionId();
101 - this.setChatSession(botId, newId);
102 - instance.sessionId = newId;
103 - }
104 -
105 - // Now that we have a session, do the deferred work
106 - refreshNonceIfNeeded();
107 - trackOriginatingPage();
108 -
109 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
110 - // so we do NOT call it here to avoid a race condition.
111 -
112 - return instance.sessionId;
113 - },
114 -
115 68 setChatSession: function(botId, sessionId) {
116 69 var cookieName = 'mxchat_session_id_' + botId;
117 - var storageKey = 'mxchat_session_id_' + botId;
118 70 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
119 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
120 71 if (this.instances[botId]) {
121 72 this.instances[botId].sessionId = sessionId;
122 73 }
123 74 },
@@ -122,10 +73,8 @@
122 73 }
123 74 },
124 75
125 76 resetChatSession: function(botId) {
126 - // Clear old session from localStorage before setting new one
127 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
128 77 var newSessionId = generateSessionId();
129 78 this.setChatSession(botId, newSessionId);
130 79 var $chatBox = getElement(botId, 'chat-box');
131 80 if ($chatBox.length) {
@@ -134,20 +83,8 @@
134 83 if (this.instances[botId]) {
135 84 this.instances[botId].chatHistoryLoaded = false;
136 85 this.instances[botId].processedMessageIds = new Set();
137 86 }
138 - },
139 -
140 - // Silent reset — new session ID without clearing the chat UI
141 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
142 - silentResetSession: function(botId) {
143 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
144 - var newSessionId = generateSessionId();
145 - this.setChatSession(botId, newSessionId);
146 - if (this.instances[botId]) {
147 - this.instances[botId].sessionId = newSessionId;
148 - }
149 - return newSessionId;
150 87 }
151 88 };
152 89
153 90 // ====================================
@@ -452,9 +389,8 @@
452 389
453 390 // Update your existing sendMessage function
454 391 function sendMessage(botId) {
455 392 botId = botId || 'default';
456 - MxChatInstances.ensureSession(botId);
457 393 var $chatInput = getElement(botId, 'chat-input');
458 394 var message = $chatInput.val();
459 395
460 396 // ADD PROMPT HOOK HERE
@@ -497,9 +433,8 @@
497 433
498 434 // Update your existing sendMessageToChatbot function
499 435 function sendMessageToChatbot(message, botId) {
500 436 botId = botId || 'default';
501 - MxChatInstances.ensureSession(botId);
502 437
503 438 // ADD PROMPT HOOK HERE
504 439 if (typeof customMxChatFilter === 'function') {
505 440 message = customMxChatFilter(message, "prompt");
@@ -667,16 +602,23 @@
667 602 errorMessage = "An error occurred. Please try again or contact support.";
668 603 }
669 604
670 605 // Handle session reset action (IP changed, session expired, etc.)
671 - // Silent reset — keep chat UI intact, just get a new session and retry
672 606 if (response.data && response.data.action === 'reset_session') {
673 - MxChatInstances.silentResetSession(botId);
674 - // 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
675 612 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
676 613 if (originalMessage) {
677 614 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
678 - 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';
679 621 if (shouldUseStreaming(currentModel)) {
680 622 callMxChatStream(originalMessage, function(response) {
681 623 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
682 624 }, botId);
@@ -770,13 +712,9 @@
770 712 $badge.show();
771 713 }
772 714 }
773 715 } else {
774 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
775 - if (response.vectorstore_error) {
776 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
777 - }
778 - replaceLastMessage("bot", emptyMsg, '', [], botId);
716 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
779 717 }
780 718
781 719 if (response.message_id) {
782 720 var instance = MxChatInstances.get(botId);
@@ -1087,16 +1025,21 @@
1087 1025 errorMessage = "An error occurred. Please try again or contact support.";
1088 1026 }
1089 1027
1090 1028 // Handle session reset action (IP changed, session expired, etc.)
1091 - // Silent reset — keep chat UI intact, just get a new session and retry
1092 1029 if (data.data && data.data.action === 'reset_session') {
1093 - MxChatInstances.silentResetSession(botId);
1094 - // 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
1095 1033 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1096 1034 if (originalMessage) {
1097 1035 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1098 - 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';
1099 1042 if (shouldUseStreaming(currentModel)) {
1100 1043 callMxChatStream(originalMessage, callback, botId);
1101 1044 } else {
1102 1045 callMxChat(originalMessage, callback, botId);
@@ -1317,12 +1260,17 @@
1317 1260 'margin-bottom': '1em'
1318 1261 });
1319 1262 }
1320 1263
1321 - // Process the message content - always run linkify to convert markdown
1322 - // links and format text. linkify() handles existing HTML safely via
1323 - // negative lookaheads that skip URLs already inside <a> tags.
1324 - 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 + }
1325 1273
1326 1274 // Add images if provided
1327 1275 if (images && images.length > 0) {
1328 1276 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1457,12 +1405,26 @@
1457 1405 bgColor = botMessageBgColor;
1458 1406 fontColor = botMessageFontColor;
1459 1407 }
1460 1408
1461 - // Always run linkify to convert markdown links and format text.
1462 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1463 - // to avoid double-processing URLs that are already inside <a> tags).
1464 - 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 + }
1465 1427
1466 1428 if (responseHtml) {
1467 1429 // Only add line breaks if there's actual text content before the HTML
1468 1430 if (fullMessage && fullMessage.trim()) {
@@ -1605,12 +1567,9 @@
1605 1567 processedText = formatTextStyling(processedText);
1606 1568
1607 1569 // Process code blocks BEFORE processing links
1608 1570 processedText = formatCodeBlocks(processedText);
1609 -
1610 - // Process markdown tables BEFORE converting newlines to paragraphs
1611 - processedText = formatMarkdownTables(processedText);
1612 -
1571 +
1613 1572 // NOW convert to paragraphs
1614 1573 processedText = convertNewlinesToBreaks(processedText);
1615 1574
1616 1575 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1623,63 +1582,37 @@
1623 1582 // Return as a proper link without the brackets
1624 1583 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1625 1584 });
1626 1585
1627 - // Process markdown links: [text](url) and [](url)
1628 - // Uses balanced parenthesis matching to handle URLs containing parens
1629 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
1630 - processedText = (function(input) {
1631 - var result = '';
1632 - var i = 0;
1633 - while (i < input.length) {
1634 - // Look for [ at current position
1635 - if (input[i] === '[') {
1636 - // Find closing ]
1637 - var closeBracket = input.indexOf(']', i + 1);
1638 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
1639 - result += input[i];
1640 - i++;
1641 - continue;
1642 - }
1643 - var linkText = input.substring(i + 1, closeBracket);
1644 - // Check if URL starts with http
1645 - var urlStart = closeBracket + 2;
1646 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
1647 - result += input[i];
1648 - i++;
1649 - continue;
1650 - }
1651 - // Find balanced closing paren
1652 - var depth = 1;
1653 - var j = urlStart;
1654 - while (j < input.length && depth > 0) {
1655 - if (input[j] === '(') depth++;
1656 - else if (input[j] === ')') depth--;
1657 - if (depth > 0) j++;
1658 - }
1659 - if (depth !== 0) {
1660 - result += input[i];
1661 - i++;
1662 - continue;
1663 - }
1664 - var url = input.substring(urlStart, j);
1665 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
1666 - var encodedUrl = safeEncodeUrl(cleanUrl);
1667 - if (!linkText || !linkText.trim()) {
1668 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
1669 - } else {
1670 - var safeText = sanitizeUserInput(linkText);
1671 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
1672 - }
1673 - i = j + 1; // Skip past the closing )
1674 - } else {
1675 - result += input[i];
1676 - i++;
1677 - }
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>`;
1678 1596 }
1679 - return result;
1680 - })(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 + });
1681 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 +
1682 1615 // Process phone numbers: [text](tel:number)
1683 1616 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1684 1617 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1685 1618 const safePhone = safeEncodeUrl(phone);
@@ -1831,78 +1764,9 @@
1831 1764 });
1832 1765
1833 1766 return text;
1834 1767 }
1835 -
1836 - function formatMarkdownTables(text) {
1837 - var lines = text.split('\n');
1838 - var result = [];
1839 - var i = 0;
1840 -
1841 - while (i < lines.length) {
1842 - // Check for a table: current line has pipes AND next line is a separator row
1843 - if (i + 1 < lines.length &&
1844 - lines[i].indexOf('|') !== -1 &&
1845 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
1846 -
1847 - var tableLines = [];
1848 - var headerLine = lines[i];
1849 - var separatorLine = lines[i + 1];
1850 - tableLines.push(headerLine);
1851 - tableLines.push(separatorLine);
1852 -
1853 - // Collect remaining table rows
1854 - var j = i + 2;
1855 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
1856 - tableLines.push(lines[j]);
1857 - j++;
1858 - }
1859 -
1860 - // Parse alignment from separator row
1861 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
1862 - var alignments = sepCells.map(function(cell) {
1863 - var trimmed = cell.trim();
1864 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
1865 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
1866 - return 'left';
1867 - });
1868 -
1869 - // Build HTML table
1870 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
1871 -
1872 - // Header row
1873 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
1874 - html += '<thead><tr>';
1875 - headerCells.forEach(function(cell, idx) {
1876 - var align = alignments[idx] || 'left';
1877 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
1878 - });
1879 - html += '</tr></thead>';
1880 -
1881 - // Body rows
1882 - html += '<tbody>';
1883 - for (var r = 2; r < tableLines.length; r++) {
1884 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
1885 - html += '<tr>';
1886 - rowCells.forEach(function(cell, idx) {
1887 - var align = alignments[idx] || 'left';
1888 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
1889 - });
1890 - html += '</tr>';
1891 - }
1892 - html += '</tbody></table></div>';
1893 -
1894 - result.push(html);
1895 - i = j;
1896 - } else {
1897 - result.push(lines[i]);
1898 - i++;
1899 - }
1900 - }
1901 -
1902 - return result.join('\n');
1903 - }
1904 -
1768 +
1905 1769 function sanitizeUserInput(text) {
1906 1770 const div = document.createElement('div');
1907 1771 div.textContent = text;
1908 1772 return div.innerHTML;
@@ -2196,29 +2060,20 @@
2196 2060 // ====================================
2197 2061 // CHAT HISTORY & PERSISTENCE
2198 2062 // ====================================
2199 2063
2200 -function loadChatHistory(botId, onComplete) {
2064 +function loadChatHistory(botId) {
2201 2065 botId = botId || 'default';
2202 2066 var instance = MxChatInstances.get(botId);
2203 2067
2204 2068 // Prevent duplicate loading
2205 2069 if (instance.chatHistoryLoaded) {
2206 - if (onComplete) onComplete();
2207 2070 return;
2208 2071 }
2209 2072
2210 - // Use getChatSession which returns null if no session exists (does NOT create one)
2211 2073 var sessionId = getChatSession(botId);
2212 2074 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2213 2075
2214 - // No session yet — nothing to load. History will load after first message via ensureSession.
2215 - if (!sessionId) {
2216 - instance.chatHistoryLoaded = true;
2217 - if (onComplete) onComplete();
2218 - return;
2219 - }
2220 -
2221 2076 if (chatPersistenceEnabled && sessionId) {
2222 2077 $.ajax({
2223 2078 url: mxchatChat.ajax_url,
2224 2079 type: 'POST',
@@ -2229,12 +2084,11 @@
2229 2084 },
2230 2085 success: function(response) {
2231 2086 // Handle session reset (IP changed while user was away)
2232 2087 if (response.success === false && response.data && response.data.action === 'reset_session') {
2233 - // Silent reset — new session but don't clear UI
2234 - MxChatInstances.silentResetSession(botId);
2088 + // Silently reset session - user will start fresh
2089 + resetChatSession(botId);
2235 2090 instance.chatHistoryLoaded = true; // Prevent retry loop
2236 - if (onComplete) onComplete();
2237 2091 return;
2238 2092 }
2239 2093
2240 2094 // Check if the response indicates success
@@ -2290,19 +2144,9 @@
2290 2144 var content = message.content;
2291 2145 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2292 2146 content = decodeHTMLEntities(content);
2293 2147
2294 - // Skip linkify for messages containing structured HTML
2295 - // (forms, product cards, galleries, etc.) to avoid
2296 - // markdown formatting corrupting HTML attributes
2297 - // (e.g. underscores in name="field_name" becoming <em> tags)
2298 - if (content.includes("mxchat-product-card") ||
2299 - content.includes("mxchat-image-gallery") ||
2300 - content.includes("mxchat-featured-products") ||
2301 - content.includes("<form") ||
2302 - content.includes("<input") ||
2303 - content.includes("<select") ||
2304 - content.includes("<textarea")) {
2148 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2305 2149 messageElement.html(content);
2306 2150 } else {
2307 2151 var formattedContent = linkify(content);
2308 2152 messageElement.html(formattedContent);
@@ -2342,17 +2186,13 @@
2342 2186 instance.chatHistoryLoaded = true;
2343 2187 }
2344 2188 }
2345 2189 }
2346 - if (onComplete) onComplete();
2347 2190 },
2348 2191 error: function(xhr, status, error) {
2349 2192 // Error loading chat history - silently continue
2350 - if (onComplete) onComplete();
2351 2193 }
2352 2194 });
2353 - } else {
2354 - if (onComplete) onComplete();
2355 2195 }
2356 2196 }
2357 2197
2358 2198
@@ -2528,35 +2368,45 @@
2528 2368 // ====================================
2529 2369
2530 2370 function checkPreChatDismissal(botId) {
2531 2371 botId = botId || 'default';
2532 - try {
2533 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2534 - if (dismissedAt) {
2535 - // Re-show after 24 hours
2536 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2537 - 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 {
2538 2383 getElement(botId, 'pre-chat-message').hide();
2539 - return;
2540 2384 }
2541 - // Expired — clear and show again
2542 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2385 + },
2386 + error: function() {
2387 + // Error checking pre-chat dismissal - silently continue
2543 2388 }
2544 - getElement(botId, 'pre-chat-message').fadeIn(250);
2545 - } catch (e) {
2546 - // localStorage unavailable — show the message
2547 - getElement(botId, 'pre-chat-message').fadeIn(250);
2548 - }
2389 + });
2549 2390 }
2550 2391
2551 2392 function handlePreChatDismissal(botId) {
2552 2393 botId = botId || 'default';
2553 2394 getElement(botId, 'pre-chat-message').fadeOut(200);
2554 - try {
2555 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2556 - } catch (e) {
2557 - // localStorage unavailable — dismissal won't persist
2558 - }
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 + });
2559 2409 }
2560 2410
2561 2411
2562 2412 // ====================================
@@ -2623,26 +2473,8 @@
2623 2473 $(this).addClass('hidden');
2624 2474 $badge.hide(); // Hide notification when opening chat
2625 2475 disableScroll();
2626 2476 $preChat.fadeOut(250);
2627 -
2628 - // Load chat history for returning visitors (persistence)
2629 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
2630 - if (chatPersistenceEnabled) {
2631 - MxChatInstances.ensureSession(botId);
2632 - }
2633 -
2634 - // Deferred email check — only on first widget open
2635 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2636 - var instance = MxChatInstances.get(botId);
2637 - if (emailBlocker && !instance.emailCheckDone) {
2638 - instance.emailCheckDone = true;
2639 - resolveEmailState(botId);
2640 - } else if (!emailBlocker) {
2641 - // No email collection — still route through showChatContainerForBot
2642 - // so the loader is shown while chat history loads
2643 - showChatContainerForBot(botId);
2644 - }
2645 2477 } else {
2646 2478 $chatbot.removeClass('visible').addClass('hidden');
2647 2479 $(this).removeClass('hidden');
2648 2480 enableScroll();
@@ -2660,9 +2492,11 @@
2660 2492
2661 2493 $(document).on('click', '.close-pre-chat-message', function(e) {
2662 2494 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2663 2495 var botId = getBotIdFromElement(this);
2664 - handlePreChatDismissal(botId);
2496 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2497 + $(this).remove();
2498 + });
2665 2499 });
2666 2500
2667 2501
2668 2502 // PDF upload button handlers - use class selector
@@ -2863,59 +2697,8 @@
2863 2697 });
2864 2698
2865 2699
2866 2700 // ====================================
2867 -// INIT LOADER & CHAT CONTAINER HELPERS
2868 -// ====================================
2869 -// These must be outside the email collection block so they're always available
2870 -// (used by persistence loading even when email collection is off)
2871 -
2872 -function showInitLoader(botId) {
2873 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2874 - if (loader) loader.style.display = 'flex';
2875 -}
2876 -
2877 -function hideInitLoader(botId) {
2878 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2879 - if (loader) loader.style.display = 'none';
2880 -}
2881 -
2882 -function showEmailFormForBot(botId) {
2883 - hideInitLoader(botId);
2884 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2885 - var chatContainer = getElementDOM(botId, 'chat-container');
2886 - if (emailBlocker) emailBlocker.style.display = 'flex';
2887 - if (chatContainer) chatContainer.style.display = 'none';
2888 -}
2889 -
2890 -function showChatContainerForBot(botId) {
2891 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2892 - var chatContainer = getElementDOM(botId, 'chat-container');
2893 - if (emailBlocker) emailBlocker.style.display = 'none';
2894 -
2895 - var instance = MxChatInstances.get(botId);
2896 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2897 -
2898 - // If persistence is on and history hasn't loaded yet, show loader
2899 - // while history loads to prevent flash of empty chat
2900 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
2901 - if (chatContainer) chatContainer.style.display = 'none';
2902 - showInitLoader(botId);
2903 - loadChatHistory(botId, function() {
2904 - hideInitLoader(botId);
2905 - if (chatContainer) chatContainer.style.display = 'flex';
2906 - scrollToBottom(botId, true);
2907 - });
2908 - } else {
2909 - hideInitLoader(botId);
2910 - if (chatContainer) chatContainer.style.display = 'flex';
2911 - if (typeof loadChatHistory === 'function') {
2912 - loadChatHistory(botId);
2913 - }
2914 - }
2915 -}
2916 -
2917 -// ====================================
2918 2701 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2919 2702 // ====================================
2920 2703 // Only run email collection setup if it's enabled
2921 2704 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -2951,8 +2734,28 @@
2951 2734 `;
2952 2735 document.head.appendChild(style);
2953 2736 }
2954 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 +
2955 2758 function isValidEmailAddress(email) {
2956 2759 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2957 2760 return emailRegex.test(email.trim()) && email.length <= 254;
2958 2761 }
@@ -3074,31 +2877,11 @@
3074 2877 existingErrors.forEach(error => error.remove());
3075 2878 }
3076 2879 }
3077 2880
3078 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3079 - function resolveEmailState(botId) {
3080 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3081 - if (mxchatChat.initial_email_state.show_email_form) {
3082 - showEmailFormForBot(botId);
3083 - } else {
3084 - showChatContainerForBot(botId);
3085 - }
3086 - } else {
3087 - checkSessionAndEmailForBot(botId);
3088 - }
3089 - }
3090 -
3091 2881 function checkSessionAndEmailForBot(botId) {
3092 - const sessionId = MxChatInstances.ensureSession(botId);
2882 + const sessionId = getChatSession(botId);
3093 2883
3094 - // Hide both panels while we check — show loader instead
3095 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3096 - var chatContainer = getElementDOM(botId, 'chat-container');
3097 - if (emailBlocker) emailBlocker.style.display = 'none';
3098 - if (chatContainer) chatContainer.style.display = 'none';
3099 - showInitLoader(botId);
3100 -
3101 2884 fetch(mxchatChat.ajax_url, {
3102 2885 method: 'POST',
3103 2886 headers: {
3104 2887 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3146,9 +2929,9 @@
3146 2929 var emailInput = getElementDOM(botId, 'user-email');
3147 2930 var nameInput = getElementDOM(botId, 'user-name');
3148 2931 var userEmail = emailInput ? emailInput.value.trim() : '';
3149 2932 var userName = nameInput ? nameInput.value.trim() : '';
3150 - var sessionId = MxChatInstances.ensureSession(botId);
2933 + var sessionId = getChatSession(botId);
3151 2934
3152 2935 // Validate email
3153 2936 if (!userEmail) {
3154 2937 showEmailError(botId, 'Please enter your email address.');
@@ -3271,27 +3054,25 @@
3271 3054 }
3272 3055 });
3273 3056
3274 3057 // Initialize email check for all bot instances
3275 - // For floating bots: defer until widget is opened (zero passive AJAX)
3276 - // For embedded bots: check immediately since the form is visible
3277 3058 $('.mxchat-chatbot-wrapper').each(function() {
3278 3059 var botId = $(this).data('bot-id') || 'default';
3279 3060 var emailBlocker = getElementDOM(botId, 'email-blocker');
3280 3061
3062 + // Only check if email blocker exists for this bot
3281 3063 if (emailBlocker) {
3282 - if (isEmbeddedBot(botId)) {
3283 - // Embedded bots are always visible — check now
3284 - 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);
3285 3074 }
3286 - // Floating bots: handled in the widget open handler
3287 - } else if (isEmbeddedBot(botId)) {
3288 - // Embedded bot, no email collection — load history with loader
3289 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3290 - if (chatPersistenceEnabled) {
3291 - MxChatInstances.ensureSession(botId);
3292 - showChatContainerForBot(botId);
3293 - }
3294 3075 }
3295 3076 });
3296 3077 }
3297 3078
@@ -3301,32 +3082,39 @@
3301 3082 var $chatbot = getElement(botId, 'floating-chatbot');
3302 3083 if ($chatbot.hasClass('hidden')) {
3303 3084 $chatbot.removeClass('hidden').addClass('visible');
3304 3085 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3305 - handlePreChatDismissal(botId);
3086 + $(this).fadeOut(250); // Hide pre-chat message
3306 3087 disableScroll(); // Disable scroll when chatbot opens
3088 + }
3089 + });
3307 3090
3308 - // Load chat history for returning visitors (persistence)
3309 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3310 - if (chatPersistenceEnabled) {
3311 - MxChatInstances.ensureSession(botId);
3312 - }
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
3313 3097
3314 - // Deferred email check — only on first widget open
3315 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3316 - var instance = MxChatInstances.get(botId);
3317 - if (emailBlocker && !instance.emailCheckDone) {
3318 - instance.emailCheckDone = true;
3319 - resolveEmailState(botId);
3320 - } else if (!emailBlocker) {
3321 - 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
3322 3112 }
3323 - }
3113 + });
3324 3114 });
3325 3115
3326 - // Legacy duplicate close handler removed — handled by single event delegation above
3327 3116
3328 -
3329 3117 function hasQuickQuestions(botId) {
3330 3118 botId = botId || 'default';
3331 3119 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3332 3120 if (!questionsContainer) return false;
@@ -3462,11 +3250,18 @@
3462 3250 });
3463 3251
3464 3252 // Initialize when document is ready
3465 3253 setFullHeight();
3254 + trackOriginatingPage();
3466 3255
3467 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3468 - // 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 + }
3469 3264
3470 3265 // Initialize chat visibility for all instances
3471 3266 $('.mxchat-chatbot-wrapper').each(function() {
3472 3267 var botId = $(this).data('bot-id') || 'default';