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 +187 -425 3.2.33.1.2 View file →
@@ -1,20 +1,12 @@
1 1 jQuery(document).ready(function($) {
2 2
3 - // Nonce refresh is deferred until first user interaction (ensureSession)
4 - // to avoid admin-ajax calls on passive page loads.
5 - var nonceRefreshed = false;
6 - function refreshNonceIfNeeded(callback) {
7 - if (nonceRefreshed || typeof mxchatChat === 'undefined' || !mxchatChat.ajax_url) {
8 - if (callback) callback();
9 - return;
10 - }
11 - nonceRefreshed = true;
3 + // Refresh nonce on load — fixes stale nonces from page-cache plugins
4 + if (typeof mxchatChat !== 'undefined' && mxchatChat.ajax_url) {
12 5 $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' }, function(res) {
13 6 if (res && res.success && res.data && res.data.nonce) {
14 7 mxchatChat.nonce = res.data.nonce;
15 8 }
16 - if (callback) callback();
17 9 });
18 10 }
19 11
20 12 // ====================================
@@ -33,9 +25,9 @@
33 25 var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
34 26
35 27 this.instances[botId] = {
36 28 botId: botId,
37 - sessionId: null,
29 + sessionId: this.getChatSession(botId),
38 30 lastSeenMessageId: '',
39 31 notificationCheckInterval: null,
40 32 pollingInterval: null,
41 33 processedMessageIds: new Set(),
@@ -60,77 +52,23 @@
60 52 return Object.keys(this.instances);
61 53 },
62 54
63 55 // Session management per bot
64 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
65 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
66 56 getChatSession: function(botId) {
67 57 var cookieName = 'mxchat_session_id_' + botId;
68 - var storageKey = 'mxchat_session_id_' + botId;
69 58 var sessionId = getCookie(cookieName);
70 59
71 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
72 60 if (!sessionId) {
73 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
61 + sessionId = generateSessionId();
62 + this.setChatSession(botId, sessionId);
74 63 }
75 64
76 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
77 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
78 - // storage). Without this, ensureSession() can generate and store an ID that
79 - // getChatSession() then can't read back, causing null session_ids on send.
80 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
81 - sessionId = this.instances[botId].sessionId;
82 - }
83 -
84 - // Guard against stored sentinel values that indicate earlier broken writes.
85 - if (sessionId === 'null' || sessionId === 'undefined') {
86 - sessionId = null;
87 - }
88 -
89 - // Re-sync cookie from localStorage if cookie was lost
90 - if (sessionId && !getCookie(cookieName)) {
91 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
92 - }
93 -
94 - return sessionId || null;
65 + return sessionId;
95 66 },
96 67
97 - // Lazy session initializer — called on first user interaction
98 - ensureSession: function(botId) {
99 - botId = botId || 'default';
100 - var instance = this.instances[botId] || this.init(botId);
101 -
102 - if (instance.sessionId) {
103 - return instance.sessionId;
104 - }
105 -
106 - // Check for existing session from cookie or localStorage
107 - var existingSession = this.getChatSession(botId);
108 -
109 - if (existingSession) {
110 - instance.sessionId = existingSession;
111 - } else {
112 - // Brand new session
113 - var newId = generateSessionId();
114 - this.setChatSession(botId, newId);
115 - instance.sessionId = newId;
116 - }
117 -
118 - // Now that we have a session, do the deferred work
119 - refreshNonceIfNeeded();
120 - trackOriginatingPage();
121 -
122 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
123 - // so we do NOT call it here to avoid a race condition.
124 -
125 - return instance.sessionId;
126 - },
127 -
128 68 setChatSession: function(botId, sessionId) {
129 69 var cookieName = 'mxchat_session_id_' + botId;
130 - var storageKey = 'mxchat_session_id_' + botId;
131 70 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
132 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
133 71 if (this.instances[botId]) {
134 72 this.instances[botId].sessionId = sessionId;
135 73 }
136 74 },
@@ -135,10 +73,8 @@
135 73 }
136 74 },
137 75
138 76 resetChatSession: function(botId) {
139 - // Clear old session from localStorage before setting new one
140 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
141 77 var newSessionId = generateSessionId();
142 78 this.setChatSession(botId, newSessionId);
143 79 var $chatBox = getElement(botId, 'chat-box');
144 80 if ($chatBox.length) {
@@ -147,20 +83,8 @@
147 83 if (this.instances[botId]) {
148 84 this.instances[botId].chatHistoryLoaded = false;
149 85 this.instances[botId].processedMessageIds = new Set();
150 86 }
151 - },
152 -
153 - // Silent reset — new session ID without clearing the chat UI
154 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
155 - silentResetSession: function(botId) {
156 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
157 - var newSessionId = generateSessionId();
158 - this.setChatSession(botId, newSessionId);
159 - if (this.instances[botId]) {
160 - this.instances[botId].sessionId = newSessionId;
161 - }
162 - return newSessionId;
163 87 }
164 88 };
165 89
166 90 // ====================================
@@ -465,9 +389,8 @@
465 389
466 390 // Update your existing sendMessage function
467 391 function sendMessage(botId) {
468 392 botId = botId || 'default';
469 - MxChatInstances.ensureSession(botId);
470 393 var $chatInput = getElement(botId, 'chat-input');
471 394 var message = $chatInput.val();
472 395
473 396 // ADD PROMPT HOOK HERE
@@ -510,9 +433,8 @@
510 433
511 434 // Update your existing sendMessageToChatbot function
512 435 function sendMessageToChatbot(message, botId) {
513 436 botId = botId || 'default';
514 - MxChatInstances.ensureSession(botId);
515 437
516 438 // ADD PROMPT HOOK HERE
517 439 if (typeof customMxChatFilter === 'function') {
518 440 message = customMxChatFilter(message, "prompt");
@@ -612,23 +534,13 @@
612 534
613 535 // Get instance for session start timestamp (used when persistence is OFF)
614 536 var instance = MxChatInstances.get(botId);
615 537
616 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
617 - // and returns the guaranteed-present session id from the in-memory instance even when
618 - // cookie/localStorage writes are silently blocked by the browser.
619 - var sessionId = MxChatInstances.ensureSession(botId);
620 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
621 - // Last-resort generation to ensure we never POST a null marker.
622 - sessionId = generateSessionId();
623 - MxChatInstances.setChatSession(botId, sessionId);
624 - }
625 -
626 538 // Prepare AJAX data
627 539 const ajaxData = {
628 540 action: 'mxchat_handle_chat_request',
629 541 message: message,
630 - session_id: sessionId,
542 + session_id: getChatSession(botId),
631 543 nonce: mxchatChat.nonce,
632 544 current_page_url: window.location.href,
633 545 current_page_title: document.title,
634 546 bot_id: botId,
@@ -690,16 +602,23 @@
690 602 errorMessage = "An error occurred. Please try again or contact support.";
691 603 }
692 604
693 605 // Handle session reset action (IP changed, session expired, etc.)
694 - // Silent reset — keep chat UI intact, just get a new session and retry
695 606 if (response.data && response.data.action === 'reset_session') {
696 - MxChatInstances.silentResetSession(botId);
697 - // Re-send the original message with the new session (user message is already displayed)
607 + // Clear the old session and generate a new one
608 + resetChatSession(botId);
609 + // Remove the temporary loading message
610 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
611 + // Re-send the original message with the new session
698 612 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
699 613 if (originalMessage) {
700 614 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
701 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
615 + // Re-add the user message and thinking indicator
616 + appendMessage("user", originalMessage, '', [], false, botId);
617 + appendThinkingMessage(botId);
618 + scrollToBottom(botId);
619 + // Determine whether to use streaming
620 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
702 621 if (shouldUseStreaming(currentModel)) {
703 622 callMxChatStream(originalMessage, function(response) {
704 623 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
705 624 }, botId);
@@ -793,13 +712,9 @@
793 712 $badge.show();
794 713 }
795 714 }
796 715 } else {
797 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
798 - if (response.vectorstore_error) {
799 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
800 - }
801 - replaceLastMessage("bot", emptyMsg, '', [], botId);
716 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
802 717 }
803 718
804 719 if (response.message_id) {
805 720 var instance = MxChatInstances.get(botId);
@@ -861,22 +776,12 @@
861 776
862 777 // Get instance for session start timestamp (used when persistence is OFF)
863 778 var instance = MxChatInstances.get(botId);
864 779
865 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
866 - // non-string value via String(), so passing `null` would POST the literal string "null"
867 - // and land in the transcripts table as a ghost session. ensureSession() always returns
868 - // a real string even when cookies/localStorage are blocked.
869 - var streamSessionId = MxChatInstances.ensureSession(botId);
870 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
871 - streamSessionId = generateSessionId();
872 - MxChatInstances.setChatSession(botId, streamSessionId);
873 - }
874 -
875 780 const formData = new FormData();
876 781 formData.append('action', 'mxchat_stream_chat');
877 782 formData.append('message', message);
878 - formData.append('session_id', streamSessionId);
783 + formData.append('session_id', getChatSession(botId));
879 784 formData.append('nonce', mxchatChat.nonce);
880 785 formData.append('current_page_url', window.location.href);
881 786 formData.append('current_page_title', document.title);
882 787 formData.append('bot_id', botId);
@@ -1120,16 +1025,21 @@
1120 1025 errorMessage = "An error occurred. Please try again or contact support.";
1121 1026 }
1122 1027
1123 1028 // Handle session reset action (IP changed, session expired, etc.)
1124 - // Silent reset — keep chat UI intact, just get a new session and retry
1125 1029 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)
1030 + // Clear the old session and generate a new one
1031 + resetChatSession(botId);
1032 + // Re-send the original message with the new session
1128 1033 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1129 1034 if (originalMessage) {
1130 1035 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1131 - 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';
1132 1042 if (shouldUseStreaming(currentModel)) {
1133 1043 callMxChatStream(originalMessage, callback, botId);
1134 1044 } else {
1135 1045 callMxChat(originalMessage, callback, botId);
@@ -1350,12 +1260,17 @@
1350 1260 'margin-bottom': '1em'
1351 1261 });
1352 1262 }
1353 1263
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);
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 + }
1358 1273
1359 1274 // Add images if provided
1360 1275 if (images && images.length > 0) {
1361 1276 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1490,12 +1405,26 @@
1490 1405 bgColor = botMessageBgColor;
1491 1406 fontColor = botMessageFontColor;
1492 1407 }
1493 1408
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);
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 + }
1498 1427
1499 1428 if (responseHtml) {
1500 1429 // Only add line breaks if there's actual text content before the HTML
1501 1430 if (fullMessage && fullMessage.trim()) {
@@ -1638,12 +1567,9 @@
1638 1567 processedText = formatTextStyling(processedText);
1639 1568
1640 1569 // Process code blocks BEFORE processing links
1641 1570 processedText = formatCodeBlocks(processedText);
1642 -
1643 - // Process markdown tables BEFORE converting newlines to paragraphs
1644 - processedText = formatMarkdownTables(processedText);
1645 -
1571 +
1646 1572 // NOW convert to paragraphs
1647 1573 processedText = convertNewlinesToBreaks(processedText);
1648 1574
1649 1575 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1656,63 +1582,37 @@
1656 1582 // Return as a proper link without the brackets
1657 1583 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1658 1584 });
1659 1585
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 - }
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>`;
1711 1596 }
1712 - return result;
1713 - })(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 + });
1714 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 +
1715 1615 // Process phone numbers: [text](tel:number)
1716 1616 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1717 1617 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1718 1618 const safePhone = safeEncodeUrl(phone);
@@ -1864,78 +1764,9 @@
1864 1764 });
1865 1765
1866 1766 return text;
1867 1767 }
1868 -
1869 - function formatMarkdownTables(text) {
1870 - var lines = text.split('\n');
1871 - var result = [];
1872 - var i = 0;
1873 -
1874 - while (i < lines.length) {
1875 - // Check for a table: current line has pipes AND next line is a separator row
1876 - if (i + 1 < lines.length &&
1877 - lines[i].indexOf('|') !== -1 &&
1878 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
1879 -
1880 - var tableLines = [];
1881 - var headerLine = lines[i];
1882 - var separatorLine = lines[i + 1];
1883 - tableLines.push(headerLine);
1884 - tableLines.push(separatorLine);
1885 -
1886 - // Collect remaining table rows
1887 - var j = i + 2;
1888 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
1889 - tableLines.push(lines[j]);
1890 - j++;
1891 - }
1892 -
1893 - // Parse alignment from separator row
1894 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
1895 - var alignments = sepCells.map(function(cell) {
1896 - var trimmed = cell.trim();
1897 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
1898 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
1899 - return 'left';
1900 - });
1901 -
1902 - // Build HTML table
1903 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
1904 -
1905 - // Header row
1906 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
1907 - html += '<thead><tr>';
1908 - headerCells.forEach(function(cell, idx) {
1909 - var align = alignments[idx] || 'left';
1910 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
1911 - });
1912 - html += '</tr></thead>';
1913 -
1914 - // Body rows
1915 - html += '<tbody>';
1916 - for (var r = 2; r < tableLines.length; r++) {
1917 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
1918 - html += '<tr>';
1919 - rowCells.forEach(function(cell, idx) {
1920 - var align = alignments[idx] || 'left';
1921 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
1922 - });
1923 - html += '</tr>';
1924 - }
1925 - html += '</tbody></table></div>';
1926 -
1927 - result.push(html);
1928 - i = j;
1929 - } else {
1930 - result.push(lines[i]);
1931 - i++;
1932 - }
1933 - }
1934 -
1935 - return result.join('\n');
1936 - }
1937 -
1768 +
1938 1769 function sanitizeUserInput(text) {
1939 1770 const div = document.createElement('div');
1940 1771 div.textContent = text;
1941 1772 return div.innerHTML;
@@ -2229,29 +2060,20 @@
2229 2060 // ====================================
2230 2061 // CHAT HISTORY & PERSISTENCE
2231 2062 // ====================================
2232 2063
2233 -function loadChatHistory(botId, onComplete) {
2064 +function loadChatHistory(botId) {
2234 2065 botId = botId || 'default';
2235 2066 var instance = MxChatInstances.get(botId);
2236 2067
2237 2068 // Prevent duplicate loading
2238 2069 if (instance.chatHistoryLoaded) {
2239 - if (onComplete) onComplete();
2240 2070 return;
2241 2071 }
2242 2072
2243 - // Use getChatSession which returns null if no session exists (does NOT create one)
2244 2073 var sessionId = getChatSession(botId);
2245 2074 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2246 2075
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 2076 if (chatPersistenceEnabled && sessionId) {
2255 2077 $.ajax({
2256 2078 url: mxchatChat.ajax_url,
2257 2079 type: 'POST',
@@ -2262,12 +2084,11 @@
2262 2084 },
2263 2085 success: function(response) {
2264 2086 // Handle session reset (IP changed while user was away)
2265 2087 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);
2088 + // Silently reset session - user will start fresh
2089 + resetChatSession(botId);
2268 2090 instance.chatHistoryLoaded = true; // Prevent retry loop
2269 - if (onComplete) onComplete();
2270 2091 return;
2271 2092 }
2272 2093
2273 2094 // Check if the response indicates success
@@ -2323,19 +2144,9 @@
2323 2144 var content = message.content;
2324 2145 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2325 2146 content = decodeHTMLEntities(content);
2326 2147
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")) {
2148 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2338 2149 messageElement.html(content);
2339 2150 } else {
2340 2151 var formattedContent = linkify(content);
2341 2152 messageElement.html(formattedContent);
@@ -2375,17 +2186,13 @@
2375 2186 instance.chatHistoryLoaded = true;
2376 2187 }
2377 2188 }
2378 2189 }
2379 - if (onComplete) onComplete();
2380 2190 },
2381 2191 error: function(xhr, status, error) {
2382 2192 // Error loading chat history - silently continue
2383 - if (onComplete) onComplete();
2384 2193 }
2385 2194 });
2386 - } else {
2387 - if (onComplete) onComplete();
2388 2195 }
2389 2196 }
2390 2197
2391 2198
@@ -2561,35 +2368,45 @@
2561 2368 // ====================================
2562 2369
2563 2370 function checkPreChatDismissal(botId) {
2564 2371 botId = botId || 'default';
2565 - try {
2566 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2567 - if (dismissedAt) {
2568 - // Re-show after 24 hours
2569 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2570 - if (elapsed < 86400000) {
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 {
2571 2383 getElement(botId, 'pre-chat-message').hide();
2572 - return;
2573 2384 }
2574 - // Expired — clear and show again
2575 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2385 + },
2386 + error: function() {
2387 + // Error checking pre-chat dismissal - silently continue
2576 2388 }
2577 - getElement(botId, 'pre-chat-message').fadeIn(250);
2578 - } catch (e) {
2579 - // localStorage unavailable — show the message
2580 - getElement(botId, 'pre-chat-message').fadeIn(250);
2581 - }
2389 + });
2582 2390 }
2583 2391
2584 2392 function handlePreChatDismissal(botId) {
2585 2393 botId = botId || 'default';
2586 2394 getElement(botId, 'pre-chat-message').fadeOut(200);
2587 - try {
2588 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2589 - } catch (e) {
2590 - // localStorage unavailable — dismissal won't persist
2591 - }
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 + });
2592 2409 }
2593 2410
2594 2411
2595 2412 // ====================================
@@ -2656,26 +2473,8 @@
2656 2473 $(this).addClass('hidden');
2657 2474 $badge.hide(); // Hide notification when opening chat
2658 2475 disableScroll();
2659 2476 $preChat.fadeOut(250);
2660 -
2661 - // Load chat history for returning visitors (persistence)
2662 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
2663 - if (chatPersistenceEnabled) {
2664 - MxChatInstances.ensureSession(botId);
2665 - }
2666 -
2667 - // Deferred email check — only on first widget open
2668 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2669 - var instance = MxChatInstances.get(botId);
2670 - if (emailBlocker && !instance.emailCheckDone) {
2671 - instance.emailCheckDone = true;
2672 - resolveEmailState(botId);
2673 - } else if (!emailBlocker) {
2674 - // No email collection — still route through showChatContainerForBot
2675 - // so the loader is shown while chat history loads
2676 - showChatContainerForBot(botId);
2677 - }
2678 2477 } else {
2679 2478 $chatbot.removeClass('visible').addClass('hidden');
2680 2479 $(this).removeClass('hidden');
2681 2480 enableScroll();
@@ -2693,9 +2492,11 @@
2693 2492
2694 2493 $(document).on('click', '.close-pre-chat-message', function(e) {
2695 2494 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2696 2495 var botId = getBotIdFromElement(this);
2697 - handlePreChatDismissal(botId);
2496 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2497 + $(this).remove();
2498 + });
2698 2499 });
2699 2500
2700 2501
2701 2502 // PDF upload button handlers - use class selector
@@ -2896,59 +2697,8 @@
2896 2697 });
2897 2698
2898 2699
2899 2700 // ====================================
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 2701 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2952 2702 // ====================================
2953 2703 // Only run email collection setup if it's enabled
2954 2704 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -2984,8 +2734,28 @@
2984 2734 `;
2985 2735 document.head.appendChild(style);
2986 2736 }
2987 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 +
2988 2758 function isValidEmailAddress(email) {
2989 2759 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2990 2760 return emailRegex.test(email.trim()) && email.length <= 254;
2991 2761 }
@@ -3107,31 +2877,11 @@
3107 2877 existingErrors.forEach(error => error.remove());
3108 2878 }
3109 2879 }
3110 2880
3111 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3112 - function resolveEmailState(botId) {
3113 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3114 - if (mxchatChat.initial_email_state.show_email_form) {
3115 - showEmailFormForBot(botId);
3116 - } else {
3117 - showChatContainerForBot(botId);
3118 - }
3119 - } else {
3120 - checkSessionAndEmailForBot(botId);
3121 - }
3122 - }
3123 -
3124 2881 function checkSessionAndEmailForBot(botId) {
3125 - const sessionId = MxChatInstances.ensureSession(botId);
2882 + const sessionId = getChatSession(botId);
3126 2883
3127 - // Hide both panels while we check — show loader instead
3128 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3129 - var chatContainer = getElementDOM(botId, 'chat-container');
3130 - if (emailBlocker) emailBlocker.style.display = 'none';
3131 - if (chatContainer) chatContainer.style.display = 'none';
3132 - showInitLoader(botId);
3133 -
3134 2884 fetch(mxchatChat.ajax_url, {
3135 2885 method: 'POST',
3136 2886 headers: {
3137 2887 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3179,9 +2929,9 @@
3179 2929 var emailInput = getElementDOM(botId, 'user-email');
3180 2930 var nameInput = getElementDOM(botId, 'user-name');
3181 2931 var userEmail = emailInput ? emailInput.value.trim() : '';
3182 2932 var userName = nameInput ? nameInput.value.trim() : '';
3183 - var sessionId = MxChatInstances.ensureSession(botId);
2933 + var sessionId = getChatSession(botId);
3184 2934
3185 2935 // Validate email
3186 2936 if (!userEmail) {
3187 2937 showEmailError(botId, 'Please enter your email address.');
@@ -3304,27 +3054,25 @@
3304 3054 }
3305 3055 });
3306 3056
3307 3057 // Initialize email check for all bot instances
3308 - // For floating bots: defer until widget is opened (zero passive AJAX)
3309 - // For embedded bots: check immediately since the form is visible
3310 3058 $('.mxchat-chatbot-wrapper').each(function() {
3311 3059 var botId = $(this).data('bot-id') || 'default';
3312 3060 var emailBlocker = getElementDOM(botId, 'email-blocker');
3313 3061
3062 + // Only check if email blocker exists for this bot
3314 3063 if (emailBlocker) {
3315 - if (isEmbeddedBot(botId)) {
3316 - // Embedded bots are always visible — check now
3317 - 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);
3318 3074 }
3319 - // Floating bots: handled in the widget open handler
3320 - } else if (isEmbeddedBot(botId)) {
3321 - // Embedded bot, no email collection — load history with loader
3322 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3323 - if (chatPersistenceEnabled) {
3324 - MxChatInstances.ensureSession(botId);
3325 - showChatContainerForBot(botId);
3326 - }
3327 3075 }
3328 3076 });
3329 3077 }
3330 3078
@@ -3334,32 +3082,39 @@
3334 3082 var $chatbot = getElement(botId, 'floating-chatbot');
3335 3083 if ($chatbot.hasClass('hidden')) {
3336 3084 $chatbot.removeClass('hidden').addClass('visible');
3337 3085 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3338 - handlePreChatDismissal(botId);
3086 + $(this).fadeOut(250); // Hide pre-chat message
3339 3087 disableScroll(); // Disable scroll when chatbot opens
3088 + }
3089 + });
3340 3090
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 - }
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
3346 3097
3347 - // Deferred email check — only on first widget open
3348 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3349 - var instance = MxChatInstances.get(botId);
3350 - if (emailBlocker && !instance.emailCheckDone) {
3351 - instance.emailCheckDone = true;
3352 - resolveEmailState(botId);
3353 - } else if (!emailBlocker) {
3354 - showChatContainerForBot(botId);
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
3355 3112 }
3356 - }
3113 + });
3357 3114 });
3358 3115
3359 - // Legacy duplicate close handler removed — handled by single event delegation above
3360 3116
3361 -
3362 3117 function hasQuickQuestions(botId) {
3363 3118 botId = botId || 'default';
3364 3119 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3365 3120 if (!questionsContainer) return false;
@@ -3495,11 +3250,18 @@
3495 3250 });
3496 3251
3497 3252 // Initialize when document is ready
3498 3253 setFullHeight();
3254 + trackOriginatingPage();
3499 3255
3500 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3501 - // 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 + }
3502 3264
3503 3265 // Initialize chat visibility for all instances
3504 3266 $('.mxchat-chatbot-wrapper').each(function() {
3505 3267 var botId = $(this).data('bot-id') || 'default';