PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.2
MxChat – AI Chatbot & Content Generation for WordPress v3.1.2
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/chat-script.js +189 -444 3.2.43.1.2 View file →
@@ -1,20 +1,12 @@
1 1 jQuery(document).ready(function($) {
2 2
3 - // Nonce refresh is deferred until first user interaction (ensureSession)
4 - // to avoid admin-ajax calls on passive page loads.
5 - var nonceRefreshed = false;
6 - function refreshNonceIfNeeded(callback) {
7 - if (nonceRefreshed || typeof mxchatChat === 'undefined' || !mxchatChat.ajax_url) {
8 - if (callback) callback();
9 - return;
10 - }
11 - nonceRefreshed = true;
3 + // Refresh nonce on load — fixes stale nonces from page-cache plugins
4 + if (typeof mxchatChat !== 'undefined' && mxchatChat.ajax_url) {
12 5 $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' }, function(res) {
13 6 if (res && res.success && res.data && res.data.nonce) {
14 7 mxchatChat.nonce = res.data.nonce;
15 8 }
16 - if (callback) callback();
17 9 });
18 10 }
19 11
20 12 // ====================================
@@ -33,9 +25,9 @@
33 25 var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
34 26
35 27 this.instances[botId] = {
36 28 botId: botId,
37 - sessionId: null,
29 + sessionId: this.getChatSession(botId),
38 30 lastSeenMessageId: '',
39 31 notificationCheckInterval: null,
40 32 pollingInterval: null,
41 33 processedMessageIds: new Set(),
@@ -60,77 +52,23 @@
60 52 return Object.keys(this.instances);
61 53 },
62 54
63 55 // Session management per bot
64 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
65 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
66 56 getChatSession: function(botId) {
67 57 var cookieName = 'mxchat_session_id_' + botId;
68 - var storageKey = 'mxchat_session_id_' + botId;
69 58 var sessionId = getCookie(cookieName);
70 59
71 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
72 60 if (!sessionId) {
73 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
61 + sessionId = generateSessionId();
62 + this.setChatSession(botId, sessionId);
74 63 }
75 64
76 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
77 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
78 - // storage). Without this, ensureSession() can generate and store an ID that
79 - // getChatSession() then can't read back, causing null session_ids on send.
80 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
81 - sessionId = this.instances[botId].sessionId;
82 - }
83 -
84 - // Guard against stored sentinel values that indicate earlier broken writes.
85 - if (sessionId === 'null' || sessionId === 'undefined') {
86 - sessionId = null;
87 - }
88 -
89 - // Re-sync cookie from localStorage if cookie was lost
90 - if (sessionId && !getCookie(cookieName)) {
91 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
92 - }
93 -
94 - return sessionId || null;
65 + return sessionId;
95 66 },
96 67
97 - // Lazy session initializer — called on first user interaction
98 - ensureSession: function(botId) {
99 - botId = botId || 'default';
100 - var instance = this.instances[botId] || this.init(botId);
101 -
102 - if (instance.sessionId) {
103 - return instance.sessionId;
104 - }
105 -
106 - // Check for existing session from cookie or localStorage
107 - var existingSession = this.getChatSession(botId);
108 -
109 - if (existingSession) {
110 - instance.sessionId = existingSession;
111 - } else {
112 - // Brand new session
113 - var newId = generateSessionId();
114 - this.setChatSession(botId, newId);
115 - instance.sessionId = newId;
116 - }
117 -
118 - // Now that we have a session, do the deferred work
119 - refreshNonceIfNeeded();
120 - trackOriginatingPage();
121 -
122 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
123 - // so we do NOT call it here to avoid a race condition.
124 -
125 - return instance.sessionId;
126 - },
127 -
128 68 setChatSession: function(botId, sessionId) {
129 69 var cookieName = 'mxchat_session_id_' + botId;
130 - var storageKey = 'mxchat_session_id_' + botId;
131 70 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
132 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
133 71 if (this.instances[botId]) {
134 72 this.instances[botId].sessionId = sessionId;
135 73 }
136 74 },
@@ -135,10 +73,8 @@
135 73 }
136 74 },
137 75
138 76 resetChatSession: function(botId) {
139 - // Clear old session from localStorage before setting new one
140 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
141 77 var newSessionId = generateSessionId();
142 78 this.setChatSession(botId, newSessionId);
143 79 var $chatBox = getElement(botId, 'chat-box');
144 80 if ($chatBox.length) {
@@ -147,20 +83,8 @@
147 83 if (this.instances[botId]) {
148 84 this.instances[botId].chatHistoryLoaded = false;
149 85 this.instances[botId].processedMessageIds = new Set();
150 86 }
151 - },
152 -
153 - // Silent reset — new session ID without clearing the chat UI
154 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
155 - silentResetSession: function(botId) {
156 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
157 - var newSessionId = generateSessionId();
158 - this.setChatSession(botId, newSessionId);
159 - if (this.instances[botId]) {
160 - this.instances[botId].sessionId = newSessionId;
161 - }
162 - return newSessionId;
163 87 }
164 88 };
165 89
166 90 // ====================================
@@ -465,9 +389,8 @@
465 389
466 390 // Update your existing sendMessage function
467 391 function sendMessage(botId) {
468 392 botId = botId || 'default';
469 - MxChatInstances.ensureSession(botId);
470 393 var $chatInput = getElement(botId, 'chat-input');
471 394 var message = $chatInput.val();
472 395
473 396 // ADD PROMPT HOOK HERE
@@ -510,9 +433,8 @@
510 433
511 434 // Update your existing sendMessageToChatbot function
512 435 function sendMessageToChatbot(message, botId) {
513 436 botId = botId || 'default';
514 - MxChatInstances.ensureSession(botId);
515 437
516 438 // ADD PROMPT HOOK HERE
517 439 if (typeof customMxChatFilter === 'function') {
518 440 message = customMxChatFilter(message, "prompt");
@@ -612,23 +534,13 @@
612 534
613 535 // Get instance for session start timestamp (used when persistence is OFF)
614 536 var instance = MxChatInstances.get(botId);
615 537
616 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
617 - // and returns the guaranteed-present session id from the in-memory instance even when
618 - // cookie/localStorage writes are silently blocked by the browser.
619 - var sessionId = MxChatInstances.ensureSession(botId);
620 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
621 - // Last-resort generation to ensure we never POST a null marker.
622 - sessionId = generateSessionId();
623 - MxChatInstances.setChatSession(botId, sessionId);
624 - }
625 -
626 538 // Prepare AJAX data
627 539 const ajaxData = {
628 540 action: 'mxchat_handle_chat_request',
629 541 message: message,
630 - session_id: sessionId,
542 + session_id: getChatSession(botId),
631 543 nonce: mxchatChat.nonce,
632 544 current_page_url: window.location.href,
633 545 current_page_title: document.title,
634 546 bot_id: botId,
@@ -690,16 +602,23 @@
690 602 errorMessage = "An error occurred. Please try again or contact support.";
691 603 }
692 604
693 605 // Handle session reset action (IP changed, session expired, etc.)
694 - // Silent reset — keep chat UI intact, just get a new session and retry
695 606 if (response.data && response.data.action === 'reset_session') {
696 - MxChatInstances.silentResetSession(botId);
697 - // Re-send the original message with the new session (user message is already displayed)
607 + // Clear the old session and generate a new one
608 + resetChatSession(botId);
609 + // Remove the temporary loading message
610 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
611 + // Re-send the original message with the new session
698 612 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
699 613 if (originalMessage) {
700 614 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
701 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
615 + // Re-add the user message and thinking indicator
616 + appendMessage("user", originalMessage, '', [], false, botId);
617 + appendThinkingMessage(botId);
618 + scrollToBottom(botId);
619 + // Determine whether to use streaming
620 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
702 621 if (shouldUseStreaming(currentModel)) {
703 622 callMxChatStream(originalMessage, function(response) {
704 623 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
705 624 }, botId);
@@ -793,13 +712,9 @@
793 712 $badge.show();
794 713 }
795 714 }
796 715 } else {
797 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
798 - if (response.vectorstore_error) {
799 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
800 - }
801 - replaceLastMessage("bot", emptyMsg, '', [], botId);
716 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
802 717 }
803 718
804 719 if (response.message_id) {
805 720 var instance = MxChatInstances.get(botId);
@@ -861,22 +776,12 @@
861 776
862 777 // Get instance for session start timestamp (used when persistence is OFF)
863 778 var instance = MxChatInstances.get(botId);
864 779
865 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
866 - // non-string value via String(), so passing `null` would POST the literal string "null"
867 - // and land in the transcripts table as a ghost session. ensureSession() always returns
868 - // a real string even when cookies/localStorage are blocked.
869 - var streamSessionId = MxChatInstances.ensureSession(botId);
870 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
871 - streamSessionId = generateSessionId();
872 - MxChatInstances.setChatSession(botId, streamSessionId);
873 - }
874 -
875 780 const formData = new FormData();
876 781 formData.append('action', 'mxchat_stream_chat');
877 782 formData.append('message', message);
878 - formData.append('session_id', streamSessionId);
783 + formData.append('session_id', getChatSession(botId));
879 784 formData.append('nonce', mxchatChat.nonce);
880 785 formData.append('current_page_url', window.location.href);
881 786 formData.append('current_page_title', document.title);
882 787 formData.append('bot_id', botId);
@@ -976,16 +881,8 @@
976 881
977 882 // Re-enable chat input when stream ends with content
978 883 enableChatInput(botId);
979 884
980 - // Scroll the user's last message to the top now that the
981 - // bot's full reply has rendered (gives max reading room).
982 - var $chatBoxDone = getElement(botId, 'chat-box');
983 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
984 - if ($lastUserMsgDone.length) {
985 - scrollElementToTop($lastUserMsgDone, botId);
986 - }
987 -
988 885 if (callback) {
989 886 callback(accumulatedContent);
990 887 }
991 888 return;
@@ -1008,16 +905,8 @@
1008 905
1009 906 // Re-enable chat input after streaming completes
1010 907 enableChatInput(botId);
1011 908
1012 - // Scroll the user's last message to the top now
1013 - // that the bot's full reply has rendered.
1014 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1015 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1016 - if ($lastUserMsgStreamDone.length) {
1017 - scrollElementToTop($lastUserMsgStreamDone, botId);
1018 - }
1019 -
1020 909 if (callback) {
1021 910 callback(accumulatedContent);
1022 911 }
1023 912 return;
@@ -1136,16 +1025,21 @@
1136 1025 errorMessage = "An error occurred. Please try again or contact support.";
1137 1026 }
1138 1027
1139 1028 // Handle session reset action (IP changed, session expired, etc.)
1140 - // Silent reset — keep chat UI intact, just get a new session and retry
1141 1029 if (data.data && data.data.action === 'reset_session') {
1142 - MxChatInstances.silentResetSession(botId);
1143 - // Re-send the original message with the new session (user message is already displayed)
1030 + // Clear the old session and generate a new one
1031 + resetChatSession(botId);
1032 + // Re-send the original message with the new session
1144 1033 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1145 1034 if (originalMessage) {
1146 1035 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1147 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1036 + // Re-add the user message and thinking indicator
1037 + appendMessage("user", originalMessage, '', [], false, botId);
1038 + appendThinkingMessage(botId);
1039 + scrollToBottom(botId);
1040 + // Determine whether to use streaming
1041 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1148 1042 if (shouldUseStreaming(currentModel)) {
1149 1043 callMxChatStream(originalMessage, callback, botId);
1150 1044 } else {
1151 1045 callMxChat(originalMessage, callback, botId);
@@ -1366,12 +1260,17 @@
1366 1260 'margin-bottom': '1em'
1367 1261 });
1368 1262 }
1369 1263
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);
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 + }
1374 1273
1375 1274 // Add images if provided
1376 1275 if (images && images.length > 0) {
1377 1276 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1506,12 +1405,26 @@
1506 1405 bgColor = botMessageBgColor;
1507 1406 fontColor = botMessageFontColor;
1508 1407 }
1509 1408
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);
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 + }
1514 1427
1515 1428 if (responseHtml) {
1516 1429 // Only add line breaks if there's actual text content before the HTML
1517 1430 if (fullMessage && fullMessage.trim()) {
@@ -1654,12 +1567,9 @@
1654 1567 processedText = formatTextStyling(processedText);
1655 1568
1656 1569 // Process code blocks BEFORE processing links
1657 1570 processedText = formatCodeBlocks(processedText);
1658 -
1659 - // Process markdown tables BEFORE converting newlines to paragraphs
1660 - processedText = formatMarkdownTables(processedText);
1661 -
1571 +
1662 1572 // NOW convert to paragraphs
1663 1573 processedText = convertNewlinesToBreaks(processedText);
1664 1574
1665 1575 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1672,63 +1582,37 @@
1672 1582 // Return as a proper link without the brackets
1673 1583 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1674 1584 });
1675 1585
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 - }
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>`;
1727 1596 }
1728 - return result;
1729 - })(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 + });
1730 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 +
1731 1615 // Process phone numbers: [text](tel:number)
1732 1616 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1733 1617 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1734 1618 const safePhone = safeEncodeUrl(phone);
@@ -1880,78 +1764,9 @@
1880 1764 });
1881 1765
1882 1766 return text;
1883 1767 }
1884 -
1885 - function formatMarkdownTables(text) {
1886 - var lines = text.split('\n');
1887 - var result = [];
1888 - var i = 0;
1889 -
1890 - while (i < lines.length) {
1891 - // Check for a table: current line has pipes AND next line is a separator row
1892 - if (i + 1 < lines.length &&
1893 - lines[i].indexOf('|') !== -1 &&
1894 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
1895 -
1896 - var tableLines = [];
1897 - var headerLine = lines[i];
1898 - var separatorLine = lines[i + 1];
1899 - tableLines.push(headerLine);
1900 - tableLines.push(separatorLine);
1901 -
1902 - // Collect remaining table rows
1903 - var j = i + 2;
1904 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
1905 - tableLines.push(lines[j]);
1906 - j++;
1907 - }
1908 -
1909 - // Parse alignment from separator row
1910 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
1911 - var alignments = sepCells.map(function(cell) {
1912 - var trimmed = cell.trim();
1913 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
1914 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
1915 - return 'left';
1916 - });
1917 -
1918 - // Build HTML table
1919 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
1920 -
1921 - // Header row
1922 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
1923 - html += '<thead><tr>';
1924 - headerCells.forEach(function(cell, idx) {
1925 - var align = alignments[idx] || 'left';
1926 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
1927 - });
1928 - html += '</tr></thead>';
1929 -
1930 - // Body rows
1931 - html += '<tbody>';
1932 - for (var r = 2; r < tableLines.length; r++) {
1933 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
1934 - html += '<tr>';
1935 - rowCells.forEach(function(cell, idx) {
1936 - var align = alignments[idx] || 'left';
1937 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
1938 - });
1939 - html += '</tr>';
1940 - }
1941 - html += '</tbody></table></div>';
1942 -
1943 - result.push(html);
1944 - i = j;
1945 - } else {
1946 - result.push(lines[i]);
1947 - i++;
1948 - }
1949 - }
1950 -
1951 - return result.join('\n');
1952 - }
1953 -
1768 +
1954 1769 function sanitizeUserInput(text) {
1955 1770 const div = document.createElement('div');
1956 1771 div.textContent = text;
1957 1772 return div.innerHTML;
@@ -2022,14 +1837,13 @@
2022 1837 requestAnimationFrame(smoothScroll);
2023 1838 }
2024 1839 }
2025 1840
2026 - function scrollElementToTop(element, botId, topOffset) {
1841 + function scrollElementToTop(element, botId) {
2027 1842 botId = botId || 'default';
2028 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2029 1843 var chatBox = getElement(botId, 'chat-box');
2030 1844 var elementTop = element.position().top + chatBox.scrollTop();
2031 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1845 + chatBox.animate({ scrollTop: elementTop }, 500);
2032 1846 }
2033 1847
2034 1848 function showChatWidget(botId) {
2035 1849 botId = botId || 'default';
@@ -2246,29 +2060,20 @@
2246 2060 // ====================================
2247 2061 // CHAT HISTORY & PERSISTENCE
2248 2062 // ====================================
2249 2063
2250 -function loadChatHistory(botId, onComplete) {
2064 +function loadChatHistory(botId) {
2251 2065 botId = botId || 'default';
2252 2066 var instance = MxChatInstances.get(botId);
2253 2067
2254 2068 // Prevent duplicate loading
2255 2069 if (instance.chatHistoryLoaded) {
2256 - if (onComplete) onComplete();
2257 2070 return;
2258 2071 }
2259 2072
2260 - // Use getChatSession which returns null if no session exists (does NOT create one)
2261 2073 var sessionId = getChatSession(botId);
2262 2074 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2263 2075
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 2076 if (chatPersistenceEnabled && sessionId) {
2272 2077 $.ajax({
2273 2078 url: mxchatChat.ajax_url,
2274 2079 type: 'POST',
@@ -2279,12 +2084,11 @@
2279 2084 },
2280 2085 success: function(response) {
2281 2086 // Handle session reset (IP changed while user was away)
2282 2087 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);
2088 + // Silently reset session - user will start fresh
2089 + resetChatSession(botId);
2285 2090 instance.chatHistoryLoaded = true; // Prevent retry loop
2286 - if (onComplete) onComplete();
2287 2091 return;
2288 2092 }
2289 2093
2290 2094 // Check if the response indicates success
@@ -2340,19 +2144,9 @@
2340 2144 var content = message.content;
2341 2145 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2342 2146 content = decodeHTMLEntities(content);
2343 2147
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")) {
2148 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2355 2149 messageElement.html(content);
2356 2150 } else {
2357 2151 var formattedContent = linkify(content);
2358 2152 messageElement.html(formattedContent);
@@ -2392,17 +2186,13 @@
2392 2186 instance.chatHistoryLoaded = true;
2393 2187 }
2394 2188 }
2395 2189 }
2396 - if (onComplete) onComplete();
2397 2190 },
2398 2191 error: function(xhr, status, error) {
2399 2192 // Error loading chat history - silently continue
2400 - if (onComplete) onComplete();
2401 2193 }
2402 2194 });
2403 - } else {
2404 - if (onComplete) onComplete();
2405 2195 }
2406 2196 }
2407 2197
2408 2198
@@ -2578,35 +2368,45 @@
2578 2368 // ====================================
2579 2369
2580 2370 function checkPreChatDismissal(botId) {
2581 2371 botId = botId || 'default';
2582 - 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) {
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 {
2588 2383 getElement(botId, 'pre-chat-message').hide();
2589 - return;
2590 2384 }
2591 - // Expired — clear and show again
2592 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2385 + },
2386 + error: function() {
2387 + // Error checking pre-chat dismissal - silently continue
2593 2388 }
2594 - getElement(botId, 'pre-chat-message').fadeIn(250);
2595 - } catch (e) {
2596 - // localStorage unavailable — show the message
2597 - getElement(botId, 'pre-chat-message').fadeIn(250);
2598 - }
2389 + });
2599 2390 }
2600 2391
2601 2392 function handlePreChatDismissal(botId) {
2602 2393 botId = botId || 'default';
2603 2394 getElement(botId, 'pre-chat-message').fadeOut(200);
2604 - try {
2605 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2606 - } catch (e) {
2607 - // localStorage unavailable — dismissal won't persist
2608 - }
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 + });
2609 2409 }
2610 2410
2611 2411
2612 2412 // ====================================
@@ -2673,26 +2473,8 @@
2673 2473 $(this).addClass('hidden');
2674 2474 $badge.hide(); // Hide notification when opening chat
2675 2475 disableScroll();
2676 2476 $preChat.fadeOut(250);
2677 -
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 - // Deferred email check — only on first widget open
2685 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2686 - var instance = MxChatInstances.get(botId);
2687 - if (emailBlocker && !instance.emailCheckDone) {
2688 - instance.emailCheckDone = true;
2689 - 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 - }
2695 2477 } else {
2696 2478 $chatbot.removeClass('visible').addClass('hidden');
2697 2479 $(this).removeClass('hidden');
2698 2480 enableScroll();
@@ -2710,9 +2492,11 @@
2710 2492
2711 2493 $(document).on('click', '.close-pre-chat-message', function(e) {
2712 2494 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2713 2495 var botId = getBotIdFromElement(this);
2714 - handlePreChatDismissal(botId);
2496 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2497 + $(this).remove();
2498 + });
2715 2499 });
2716 2500
2717 2501
2718 2502 // PDF upload button handlers - use class selector
@@ -2913,59 +2697,8 @@
2913 2697 });
2914 2698
2915 2699
2916 2700 // ====================================
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 2701 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2969 2702 // ====================================
2970 2703 // Only run email collection setup if it's enabled
2971 2704 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3001,8 +2734,28 @@
3001 2734 `;
3002 2735 document.head.appendChild(style);
3003 2736 }
3004 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 +
3005 2758 function isValidEmailAddress(email) {
3006 2759 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3007 2760 return emailRegex.test(email.trim()) && email.length <= 254;
3008 2761 }
@@ -3124,31 +2877,11 @@
3124 2877 existingErrors.forEach(error => error.remove());
3125 2878 }
3126 2879 }
3127 2880
3128 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3129 - function resolveEmailState(botId) {
3130 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3131 - if (mxchatChat.initial_email_state.show_email_form) {
3132 - showEmailFormForBot(botId);
3133 - } else {
3134 - showChatContainerForBot(botId);
3135 - }
3136 - } else {
3137 - checkSessionAndEmailForBot(botId);
3138 - }
3139 - }
3140 -
3141 2881 function checkSessionAndEmailForBot(botId) {
3142 - const sessionId = MxChatInstances.ensureSession(botId);
2882 + const sessionId = getChatSession(botId);
3143 2883
3144 - // Hide both panels while we check — show loader instead
3145 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3146 - var chatContainer = getElementDOM(botId, 'chat-container');
3147 - if (emailBlocker) emailBlocker.style.display = 'none';
3148 - if (chatContainer) chatContainer.style.display = 'none';
3149 - showInitLoader(botId);
3150 -
3151 2884 fetch(mxchatChat.ajax_url, {
3152 2885 method: 'POST',
3153 2886 headers: {
3154 2887 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3196,9 +2929,9 @@
3196 2929 var emailInput = getElementDOM(botId, 'user-email');
3197 2930 var nameInput = getElementDOM(botId, 'user-name');
3198 2931 var userEmail = emailInput ? emailInput.value.trim() : '';
3199 2932 var userName = nameInput ? nameInput.value.trim() : '';
3200 - var sessionId = MxChatInstances.ensureSession(botId);
2933 + var sessionId = getChatSession(botId);
3201 2934
3202 2935 // Validate email
3203 2936 if (!userEmail) {
3204 2937 showEmailError(botId, 'Please enter your email address.');
@@ -3321,27 +3054,25 @@
3321 3054 }
3322 3055 });
3323 3056
3324 3057 // Initialize email check for all bot instances
3325 - // For floating bots: defer until widget is opened (zero passive AJAX)
3326 - // For embedded bots: check immediately since the form is visible
3327 3058 $('.mxchat-chatbot-wrapper').each(function() {
3328 3059 var botId = $(this).data('bot-id') || 'default';
3329 3060 var emailBlocker = getElementDOM(botId, 'email-blocker');
3330 3061
3062 + // Only check if email blocker exists for this bot
3331 3063 if (emailBlocker) {
3332 - if (isEmbeddedBot(botId)) {
3333 - // Embedded bots are always visible — check now
3334 - 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);
3335 3074 }
3336 - // 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 3075 }
3345 3076 });
3346 3077 }
3347 3078
@@ -3351,32 +3082,39 @@
3351 3082 var $chatbot = getElement(botId, 'floating-chatbot');
3352 3083 if ($chatbot.hasClass('hidden')) {
3353 3084 $chatbot.removeClass('hidden').addClass('visible');
3354 3085 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3355 - handlePreChatDismissal(botId);
3086 + $(this).fadeOut(250); // Hide pre-chat message
3356 3087 disableScroll(); // Disable scroll when chatbot opens
3088 + }
3089 + });
3357 3090
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 - }
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
3363 3097
3364 - // Deferred email check — only on first widget open
3365 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3366 - var instance = MxChatInstances.get(botId);
3367 - if (emailBlocker && !instance.emailCheckDone) {
3368 - instance.emailCheckDone = true;
3369 - resolveEmailState(botId);
3370 - } else if (!emailBlocker) {
3371 - 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
3372 3112 }
3373 - }
3113 + });
3374 3114 });
3375 3115
3376 - // Legacy duplicate close handler removed — handled by single event delegation above
3377 3116
3378 -
3379 3117 function hasQuickQuestions(botId) {
3380 3118 botId = botId || 'default';
3381 3119 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3382 3120 if (!questionsContainer) return false;
@@ -3512,11 +3250,18 @@
3512 3250 });
3513 3251
3514 3252 // Initialize when document is ready
3515 3253 setFullHeight();
3254 + trackOriginatingPage();
3516 3255
3517 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3518 - // 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 + }
3519 3264
3520 3265 // Initialize chat visibility for all instances
3521 3266 $('.mxchat-chatbot-wrapper').each(function() {
3522 3267 var botId = $(this).data('bot-id') || 'default';