PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.7
MxChat – AI Chatbot & Content Generation for WordPress v3.0.7
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 +200 -452 3.2.23.0.7 View file →
@@ -1,23 +1,6 @@
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;
12 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' }, function(res) {
13 - if (res && res.success && res.data && res.data.nonce) {
14 - mxchatChat.nonce = res.data.nonce;
15 - }
16 - if (callback) callback();
17 - });
18 - }
19 -
20 3 // ====================================
21 4 // MULTI-INSTANCE MANAGEMENT SYSTEM
22 5 // ====================================
23 6
@@ -33,9 +16,9 @@
33 16 var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
34 17
35 18 this.instances[botId] = {
36 19 botId: botId,
37 - sessionId: null,
20 + sessionId: this.getChatSession(botId),
38 21 lastSeenMessageId: '',
39 22 notificationCheckInterval: null,
40 23 pollingInterval: null,
41 24 processedMessageIds: new Set(),
@@ -60,64 +43,23 @@
60 43 return Object.keys(this.instances);
61 44 },
62 45
63 46 // 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 47 getChatSession: function(botId) {
67 48 var cookieName = 'mxchat_session_id_' + botId;
68 - var storageKey = 'mxchat_session_id_' + botId;
69 49 var sessionId = getCookie(cookieName);
70 50
71 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
72 51 if (!sessionId) {
73 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
52 + sessionId = generateSessionId();
53 + this.setChatSession(botId, sessionId);
74 54 }
75 55
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;
56 + return sessionId;
82 57 },
83 58
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 59 setChatSession: function(botId, sessionId) {
116 60 var cookieName = 'mxchat_session_id_' + botId;
117 - var storageKey = 'mxchat_session_id_' + botId;
118 61 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
119 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
120 62 if (this.instances[botId]) {
121 63 this.instances[botId].sessionId = sessionId;
122 64 }
123 65 },
@@ -122,10 +64,8 @@
122 64 }
123 65 },
124 66
125 67 resetChatSession: function(botId) {
126 - // Clear old session from localStorage before setting new one
127 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
128 68 var newSessionId = generateSessionId();
129 69 this.setChatSession(botId, newSessionId);
130 70 var $chatBox = getElement(botId, 'chat-box');
131 71 if ($chatBox.length) {
@@ -134,20 +74,8 @@
134 74 if (this.instances[botId]) {
135 75 this.instances[botId].chatHistoryLoaded = false;
136 76 this.instances[botId].processedMessageIds = new Set();
137 77 }
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 78 }
151 79 };
152 80
153 81 // ====================================
@@ -452,9 +380,8 @@
452 380
453 381 // Update your existing sendMessage function
454 382 function sendMessage(botId) {
455 383 botId = botId || 'default';
456 - MxChatInstances.ensureSession(botId);
457 384 var $chatInput = getElement(botId, 'chat-input');
458 385 var message = $chatInput.val();
459 386
460 387 // ADD PROMPT HOOK HERE
@@ -462,14 +389,10 @@
462 389 message = customMxChatFilter(message, "prompt");
463 390 }
464 391
465 392 if (message) {
466 - // Don't disable input in live agent mode - let users chat freely
467 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
468 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
469 - if (!isAgentMode) {
470 - disableChatInput(botId);
471 - }
393 + // Disable input while waiting for response
394 + disableChatInput(botId);
472 395
473 396 appendMessage("user", message, '', [], false, botId);
474 397 $chatInput.val('');
475 398 $chatInput.css('height', 'auto');
@@ -497,9 +420,8 @@
497 420
498 421 // Update your existing sendMessageToChatbot function
499 422 function sendMessageToChatbot(message, botId) {
500 423 botId = botId || 'default';
501 - MxChatInstances.ensureSession(botId);
502 424
503 425 // ADD PROMPT HOOK HERE
504 426 if (typeof customMxChatFilter === 'function') {
505 427 message = customMxChatFilter(message, "prompt");
@@ -504,14 +426,10 @@
504 426 if (typeof customMxChatFilter === 'function') {
505 427 message = customMxChatFilter(message, "prompt");
506 428 }
507 429
508 - // Don't disable input in live agent mode - let users chat freely
509 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
510 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
511 - if (!isAgentMode) {
512 - disableChatInput(botId);
513 - }
430 + // Disable input while waiting for response
431 + disableChatInput(botId);
514 432
515 433 var sessionId = getChatSession(botId);
516 434
517 435 if (hasQuickQuestions(botId)) {
@@ -667,16 +585,23 @@
667 585 errorMessage = "An error occurred. Please try again or contact support.";
668 586 }
669 587
670 588 // Handle session reset action (IP changed, session expired, etc.)
671 - // Silent reset — keep chat UI intact, just get a new session and retry
672 589 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)
590 + // Clear the old session and generate a new one
591 + resetChatSession(botId);
592 + // Remove the temporary loading message
593 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
594 + // Re-send the original message with the new session
675 595 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
676 596 if (originalMessage) {
677 597 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
678 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
598 + // Re-add the user message and thinking indicator
599 + appendMessage("user", originalMessage, '', [], false, botId);
600 + appendThinkingMessage(botId);
601 + scrollToBottom(botId);
602 + // Determine whether to use streaming
603 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
679 604 if (shouldUseStreaming(currentModel)) {
680 605 callMxChatStream(originalMessage, function(response) {
681 606 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
682 607 }, botId);
@@ -733,11 +658,9 @@
733 658 }
734 659
735 660 // Check for live agent response
736 661 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
737 - removeThinkingDots(botId);
738 662 updateChatModeIndicator('agent', botId);
739 - enableChatInput(botId);
740 663 return;
741 664 }
742 665
743 666 // Handle the message and show notification if chat is hidden
@@ -770,13 +693,9 @@
770 693 $badge.show();
771 694 }
772 695 }
773 696 } 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);
697 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
779 698 }
780 699
781 700 if (response.message_id) {
782 701 var instance = MxChatInstances.get(botId);
@@ -1087,16 +1006,21 @@
1087 1006 errorMessage = "An error occurred. Please try again or contact support.";
1088 1007 }
1089 1008
1090 1009 // Handle session reset action (IP changed, session expired, etc.)
1091 - // Silent reset — keep chat UI intact, just get a new session and retry
1092 1010 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)
1011 + // Clear the old session and generate a new one
1012 + resetChatSession(botId);
1013 + // Re-send the original message with the new session
1095 1014 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1096 1015 if (originalMessage) {
1097 1016 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1098 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1017 + // Re-add the user message and thinking indicator
1018 + appendMessage("user", originalMessage, '', [], false, botId);
1019 + appendThinkingMessage(botId);
1020 + scrollToBottom(botId);
1021 + // Determine whether to use streaming
1022 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1099 1023 if (shouldUseStreaming(currentModel)) {
1100 1024 callMxChatStream(originalMessage, callback, botId);
1101 1025 } else {
1102 1026 callMxChat(originalMessage, callback, botId);
@@ -1118,22 +1042,8 @@
1118 1042 }
1119 1043 return; // Exit early for errors
1120 1044 }
1121 1045
1122 - // Check for live agent response
1123 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1124 - removeThinkingDots(botId);
1125 - // Also remove any leftover bot-message that lost its temporary-message class
1126 - var $chatBox = getElement(botId, 'chat-box');
1127 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1128 - updateChatModeIndicator('agent', botId);
1129 - enableChatInput(botId);
1130 - if (callback) {
1131 - callback('');
1132 - }
1133 - return;
1134 - }
1135 -
1136 1046 // Handle different response formats
1137 1047 if (data.text || data.html || data.message) {
1138 1048
1139 1049 // Apply response hooks
@@ -1178,15 +1088,19 @@
1178 1088 }
1179 1089
1180 1090 // Enhanced updateChatModeIndicator function for immediate DOM updates
1181 1091 function updateChatModeIndicator(mode, botId) {
1092 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1182 1093 botId = botId || 'default';
1183 1094 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1095 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1184 1096 if (indicator) {
1185 1097 const oldText = indicator.textContent;
1098 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1186 1099
1187 1100 if (mode === 'agent') {
1188 1101 indicator.textContent = 'Live Agent';
1102 + console.log('[MxChat] Mode is agent, calling startPolling...');
1189 1103 startPolling(botId);
1190 1104 } else {
1191 1105 // Everything else is AI mode
1192 1106 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1257,12 +1171,9 @@
1257 1171 // Update the event handlers to use the correct function names (using event delegation)
1258 1172 // Use class-based selectors for multi-instance support
1259 1173 $(document).on('click', '.send-button', function() {
1260 1174 var botId = getBotIdFromElement(this);
1261 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1262 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1263 - disableChatInput(botId);
1264 - }
1175 + disableChatInput(botId);
1265 1176 sendMessage(botId);
1266 1177 });
1267 1178
1268 1179 // Override enter key handler (using event delegation)
@@ -1269,12 +1180,9 @@
1269 1180 $(document).on('keypress', '.chat-input', function(e) {
1270 1181 if (e.which == 13 && !e.shiftKey) {
1271 1182 e.preventDefault();
1272 1183 var botId = getBotIdFromElement(this);
1273 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1274 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1275 - disableChatInput(botId);
1276 - }
1184 + disableChatInput(botId);
1277 1185 sendMessage(botId);
1278 1186 }
1279 1187 });
1280 1188
@@ -1317,12 +1225,17 @@
1317 1225 'margin-bottom': '1em'
1318 1226 });
1319 1227 }
1320 1228
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);
1229 + // Process the message content based on sender
1230 + let fullMessage;
1231 + if (sender === "user") {
1232 + // For user messages, apply linkify after sanitization
1233 + fullMessage = linkify(messageText);
1234 + } else {
1235 + // For bot/agent messages, preserve HTML
1236 + fullMessage = messageText;
1237 + }
1325 1238
1326 1239 // Add images if provided
1327 1240 if (images && images.length > 0) {
1328 1241 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1457,12 +1370,26 @@
1457 1370 bgColor = botMessageBgColor;
1458 1371 fontColor = botMessageFontColor;
1459 1372 }
1460 1373
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);
1374 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1375 + // This prevents double-processing of URLs that are already formatted as HTML
1376 + var fullMessage;
1377 + if (sender === "user") {
1378 + // Always linkify user messages (they're plain text)
1379 + fullMessage = linkify(responseText);
1380 + } else {
1381 + // For bot/agent messages, check if HTML already exists
1382 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1383 + responseText.includes('<img') || responseText.includes('<div') ||
1384 + responseText.includes('<p>') || responseText.includes('<br>')) {
1385 + // Response already has HTML, don't process it
1386 + fullMessage = responseText;
1387 + } else {
1388 + // Plain text response, apply linkify
1389 + fullMessage = linkify(responseText);
1390 + }
1391 + }
1465 1392
1466 1393 if (responseHtml) {
1467 1394 // Only add line breaks if there's actual text content before the HTML
1468 1395 if (fullMessage && fullMessage.trim()) {
@@ -1529,15 +1456,8 @@
1529 1456
1530 1457
1531 1458 function appendThinkingMessage(botId) {
1532 1459 botId = botId || 'default';
1533 -
1534 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
1535 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
1536 - if (indicator && indicator.textContent === 'Live Agent') {
1537 - return;
1538 - }
1539 -
1540 1460 var $chatBox = getElement(botId, 'chat-box');
1541 1461
1542 1462 // Remove any existing thinking dots in this bot's chat first
1543 1463 $chatBox.find('.thinking-dots').remove();
@@ -1559,9 +1479,9 @@
1559 1479 '</div>' +
1560 1480 '</div>';
1561 1481
1562 1482 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1563 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1483 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
1564 1484 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1565 1485 scrollToBottom(botId);
1566 1486 }
1567 1487
@@ -1567,11 +1487,9 @@
1567 1487
1568 1488 function removeThinkingDots(botId) {
1569 1489 botId = botId || 'default';
1570 1490 var $chatBox = getElement(botId, 'chat-box');
1571 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1572 1491 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1573 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1574 1492 }
1575 1493
1576 1494 // ====================================
1577 1495 // TEXT FORMATTING & PROCESSING
@@ -1605,12 +1523,9 @@
1605 1523 processedText = formatTextStyling(processedText);
1606 1524
1607 1525 // Process code blocks BEFORE processing links
1608 1526 processedText = formatCodeBlocks(processedText);
1609 -
1610 - // Process markdown tables BEFORE converting newlines to paragraphs
1611 - processedText = formatMarkdownTables(processedText);
1612 -
1527 +
1613 1528 // NOW convert to paragraphs
1614 1529 processedText = convertNewlinesToBreaks(processedText);
1615 1530
1616 1531 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1623,63 +1538,37 @@
1623 1538 // Return as a proper link without the brackets
1624 1539 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1625 1540 });
1626 1541
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 - }
1542 + // Process proper markdown links with text: [text](url)
1543 + // This MUST have non-empty text in the first brackets
1544 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1545 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1546 + // Make sure we have actual text (not just whitespace)
1547 + if (!text || !text.trim()) {
1548 + // If no text, treat the URL as the text
1549 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1550 + const safeUrl = safeEncodeUrl(cleanUrl);
1551 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1678 1552 }
1679 - return result;
1680 - })(processedText);
1553 +
1554 + // Clean the URL
1555 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1556 + const safeUrl = safeEncodeUrl(cleanUrl);
1557 + const safeText = sanitizeUserInput(text);
1558 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1559 + });
1681 1560
1561 + // Handle empty markdown links: [](url)
1562 + // This is a specific case where there's no text
1563 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1564 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1565 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1566 + const safeUrl = safeEncodeUrl(cleanUrl);
1567 + // Use the URL itself as the link text
1568 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1569 + });
1570 +
1682 1571 // Process phone numbers: [text](tel:number)
1683 1572 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1684 1573 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1685 1574 const safePhone = safeEncodeUrl(phone);
@@ -1831,78 +1720,9 @@
1831 1720 });
1832 1721
1833 1722 return text;
1834 1723 }
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 -
1724 +
1905 1725 function sanitizeUserInput(text) {
1906 1726 const div = document.createElement('div');
1907 1727 div.textContent = text;
1908 1728 return div.innerHTML;
@@ -2123,12 +1943,15 @@
2123 1943 // LIVE AGENT FUNCTIONALITY
2124 1944 // ====================================
2125 1945
2126 1946 function startPolling(botId) {
1947 + console.log('[MxChat] startPolling called for botId:', botId);
2127 1948 botId = botId || 'default';
2128 1949 var instance = MxChatInstances.get(botId);
2129 1950 // Clear any existing interval first
2130 1951 stopPolling(botId);
1952 + // Start new polling interval
1953 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2131 1954 instance.pollingInterval = setInterval(function() {
2132 1955 checkForAgentMessages(botId);
2133 1956 }, 5000);
2134 1957 }
@@ -2133,17 +1956,20 @@
2133 1956 }, 5000);
2134 1957 }
2135 1958
2136 1959 function stopPolling(botId) {
1960 + console.log('[MxChat] stopPolling called for botId:', botId);
2137 1961 botId = botId || 'default';
2138 1962 var instance = MxChatInstances.get(botId);
2139 1963 if (instance.pollingInterval) {
2140 1964 clearInterval(instance.pollingInterval);
2141 1965 instance.pollingInterval = null;
1966 + console.log('[MxChat] Polling stopped for botId:', botId);
2142 1967 }
2143 1968 }
2144 1969
2145 1970 function checkForAgentMessages(botId) {
1971 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2146 1972 botId = botId || 'default';
2147 1973 var instance = MxChatInstances.get(botId);
2148 1974 const sessionId = getChatSession(botId);
2149 1975 $.ajax({
@@ -2169,12 +1995,8 @@
2169 1995 instance.processedMessageIds.add(message.id);
2170 1996 }
2171 1997 });
2172 1998
2173 - if (hasNewMessage) {
2174 - enableChatInput(botId);
2175 - }
2176 -
2177 1999 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2178 2000 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2179 2001 showNotification(botId);
2180 2002 }
@@ -2180,13 +2002,8 @@
2180 2002 }
2181 2003
2182 2004 scrollToBottom(botId, true);
2183 2005 }
2184 -
2185 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2186 - if (response.success && response.data?.chat_mode) {
2187 - updateChatModeIndicator(response.data.chat_mode, botId);
2188 - }
2189 2006 },
2190 2007 error: function (xhr, status, error) {
2191 2008 // Polling error - silently continue
2192 2009 }
@@ -2196,29 +2013,20 @@
2196 2013 // ====================================
2197 2014 // CHAT HISTORY & PERSISTENCE
2198 2015 // ====================================
2199 2016
2200 -function loadChatHistory(botId, onComplete) {
2017 +function loadChatHistory(botId) {
2201 2018 botId = botId || 'default';
2202 2019 var instance = MxChatInstances.get(botId);
2203 2020
2204 2021 // Prevent duplicate loading
2205 2022 if (instance.chatHistoryLoaded) {
2206 - if (onComplete) onComplete();
2207 2023 return;
2208 2024 }
2209 2025
2210 - // Use getChatSession which returns null if no session exists (does NOT create one)
2211 2026 var sessionId = getChatSession(botId);
2212 2027 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2213 2028
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 2029 if (chatPersistenceEnabled && sessionId) {
2222 2030 $.ajax({
2223 2031 url: mxchatChat.ajax_url,
2224 2032 type: 'POST',
@@ -2229,12 +2037,11 @@
2229 2037 },
2230 2038 success: function(response) {
2231 2039 // Handle session reset (IP changed while user was away)
2232 2040 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);
2041 + // Silently reset session - user will start fresh
2042 + resetChatSession(botId);
2235 2043 instance.chatHistoryLoaded = true; // Prevent retry loop
2236 - if (onComplete) onComplete();
2237 2044 return;
2238 2045 }
2239 2046
2240 2047 // Check if the response indicates success
@@ -2290,19 +2097,9 @@
2290 2097 var content = message.content;
2291 2098 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2292 2099 content = decodeHTMLEntities(content);
2293 2100
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")) {
2101 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2305 2102 messageElement.html(content);
2306 2103 } else {
2307 2104 var formattedContent = linkify(content);
2308 2105 messageElement.html(formattedContent);
@@ -2342,17 +2139,13 @@
2342 2139 instance.chatHistoryLoaded = true;
2343 2140 }
2344 2141 }
2345 2142 }
2346 - if (onComplete) onComplete();
2347 2143 },
2348 2144 error: function(xhr, status, error) {
2349 2145 // Error loading chat history - silently continue
2350 - if (onComplete) onComplete();
2351 2146 }
2352 2147 });
2353 - } else {
2354 - if (onComplete) onComplete();
2355 2148 }
2356 2149 }
2357 2150
2358 2151
@@ -2528,35 +2321,45 @@
2528 2321 // ====================================
2529 2322
2530 2323 function checkPreChatDismissal(botId) {
2531 2324 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) {
2325 + $.ajax({
2326 + url: mxchatChat.ajax_url,
2327 + type: 'POST',
2328 + data: {
2329 + action: 'mxchat_check_pre_chat_message_status',
2330 + _ajax_nonce: mxchatChat.nonce
2331 + },
2332 + success: function(response) {
2333 + if (response.success && !response.data.dismissed) {
2334 + getElement(botId, 'pre-chat-message').fadeIn(250);
2335 + } else {
2538 2336 getElement(botId, 'pre-chat-message').hide();
2539 - return;
2540 2337 }
2541 - // Expired — clear and show again
2542 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2338 + },
2339 + error: function() {
2340 + // Error checking pre-chat dismissal - silently continue
2543 2341 }
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 - }
2342 + });
2549 2343 }
2550 2344
2551 2345 function handlePreChatDismissal(botId) {
2552 2346 botId = botId || 'default';
2553 2347 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 - }
2348 + $.ajax({
2349 + url: mxchatChat.ajax_url,
2350 + type: 'POST',
2351 + data: {
2352 + action: 'mxchat_dismiss_pre_chat_message',
2353 + _ajax_nonce: mxchatChat.nonce
2354 + },
2355 + success: function() {
2356 + $('#pre-chat-message').hide();
2357 + },
2358 + error: function() {
2359 + // Error dismissing pre-chat message - silently continue
2360 + }
2361 + });
2559 2362 }
2560 2363
2561 2364
2562 2365 // ====================================
@@ -2623,26 +2426,8 @@
2623 2426 $(this).addClass('hidden');
2624 2427 $badge.hide(); // Hide notification when opening chat
2625 2428 disableScroll();
2626 2429 $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 2430 } else {
2646 2431 $chatbot.removeClass('visible').addClass('hidden');
2647 2432 $(this).removeClass('hidden');
2648 2433 enableScroll();
@@ -2660,9 +2445,11 @@
2660 2445
2661 2446 $(document).on('click', '.close-pre-chat-message', function(e) {
2662 2447 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2663 2448 var botId = getBotIdFromElement(this);
2664 - handlePreChatDismissal(botId);
2449 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2450 + $(this).remove();
2451 + });
2665 2452 });
2666 2453
2667 2454
2668 2455 // PDF upload button handlers - use class selector
@@ -2863,59 +2650,8 @@
2863 2650 });
2864 2651
2865 2652
2866 2653 // ====================================
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 2654 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2919 2655 // ====================================
2920 2656 // Only run email collection setup if it's enabled
2921 2657 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -2951,8 +2687,28 @@
2951 2687 `;
2952 2688 document.head.appendChild(style);
2953 2689 }
2954 2690
2691 + // Helper functions for email collection (multi-instance aware)
2692 + function showEmailFormForBot(botId) {
2693 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2694 + var chatContainer = getElementDOM(botId, 'chat-container');
2695 + if (emailBlocker) emailBlocker.style.display = 'flex';
2696 + if (chatContainer) chatContainer.style.display = 'none';
2697 + }
2698 +
2699 + function showChatContainerForBot(botId) {
2700 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2701 + var chatContainer = getElementDOM(botId, 'chat-container');
2702 + if (emailBlocker) emailBlocker.style.display = 'none';
2703 + if (chatContainer) chatContainer.style.display = 'flex';
2704 +
2705 + // Load chat history for this bot
2706 + if (typeof loadChatHistory === 'function') {
2707 + loadChatHistory(botId);
2708 + }
2709 + }
2710 +
2955 2711 function isValidEmailAddress(email) {
2956 2712 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2957 2713 return emailRegex.test(email.trim()) && email.length <= 254;
2958 2714 }
@@ -3074,31 +2830,11 @@
3074 2830 existingErrors.forEach(error => error.remove());
3075 2831 }
3076 2832 }
3077 2833
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 2834 function checkSessionAndEmailForBot(botId) {
3092 - const sessionId = MxChatInstances.ensureSession(botId);
2835 + const sessionId = getChatSession(botId);
3093 2836
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 2837 fetch(mxchatChat.ajax_url, {
3102 2838 method: 'POST',
3103 2839 headers: {
3104 2840 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3146,9 +2882,9 @@
3146 2882 var emailInput = getElementDOM(botId, 'user-email');
3147 2883 var nameInput = getElementDOM(botId, 'user-name');
3148 2884 var userEmail = emailInput ? emailInput.value.trim() : '';
3149 2885 var userName = nameInput ? nameInput.value.trim() : '';
3150 - var sessionId = MxChatInstances.ensureSession(botId);
2886 + var sessionId = getChatSession(botId);
3151 2887
3152 2888 // Validate email
3153 2889 if (!userEmail) {
3154 2890 showEmailError(botId, 'Please enter your email address.');
@@ -3271,27 +3007,25 @@
3271 3007 }
3272 3008 });
3273 3009
3274 3010 // 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 3011 $('.mxchat-chatbot-wrapper').each(function() {
3278 3012 var botId = $(this).data('bot-id') || 'default';
3279 3013 var emailBlocker = getElementDOM(botId, 'email-blocker');
3280 3014
3015 + // Only check if email blocker exists for this bot
3281 3016 if (emailBlocker) {
3282 - if (isEmbeddedBot(botId)) {
3283 - // Embedded bots are always visible — check now
3284 - resolveEmailState(botId);
3017 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3018 + if (mxchatChat.initial_email_state.show_email_form) {
3019 + showEmailFormForBot(botId);
3020 + } else {
3021 + showChatContainerForBot(botId);
3022 + }
3023 + } else {
3024 + setTimeout(function() {
3025 + checkSessionAndEmailForBot(botId);
3026 + }, 100);
3285 3027 }
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 3028 }
3295 3029 });
3296 3030 }
3297 3031
@@ -3301,32 +3035,39 @@
3301 3035 var $chatbot = getElement(botId, 'floating-chatbot');
3302 3036 if ($chatbot.hasClass('hidden')) {
3303 3037 $chatbot.removeClass('hidden').addClass('visible');
3304 3038 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3305 - handlePreChatDismissal(botId);
3039 + $(this).fadeOut(250); // Hide pre-chat message
3306 3040 disableScroll(); // Disable scroll when chatbot opens
3041 + }
3042 + });
3307 3043
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 - }
3044 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
3045 + // This is a fallback for legacy support
3046 + $(document).on('click', '.close-pre-chat-message', function() {
3047 + var botId = getBotIdFromElement(this);
3048 + var $preChat = getElement(botId, 'pre-chat-message');
3049 + $preChat.fadeOut(200); // Hide the message
3313 3050
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);
3051 + // Send an AJAX request to set the transient flag for 24 hours
3052 + $.ajax({
3053 + url: mxchatChat.ajax_url,
3054 + type: 'POST',
3055 + data: {
3056 + action: 'mxchat_dismiss_pre_chat_message',
3057 + _ajax_nonce: mxchatChat.nonce
3058 + },
3059 + success: function() {
3060 + // Ensure the message is hidden after dismissal
3061 + $preChat.hide();
3062 + },
3063 + error: function() {
3064 + // Error dismissing pre-chat message - silently continue
3322 3065 }
3323 - }
3066 + });
3324 3067 });
3325 3068
3326 - // Legacy duplicate close handler removed — handled by single event delegation above
3327 3069
3328 -
3329 3070 function hasQuickQuestions(botId) {
3330 3071 botId = botId || 'default';
3331 3072 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3332 3073 if (!questionsContainer) return false;
@@ -3462,11 +3203,18 @@
3462 3203 });
3463 3204
3464 3205 // Initialize when document is ready
3465 3206 setFullHeight();
3207 + trackOriginatingPage();
3466 3208
3467 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3468 - // until the user's first interaction via MxChatInstances.ensureSession()
3209 + // Only load chat history if email collection is disabled
3210 + if (mxchatChat.email_collection_enabled !== 'on') {
3211 + // Load history for all instances
3212 + $('.mxchat-chatbot-wrapper').each(function() {
3213 + var botId = $(this).data('bot-id') || 'default';
3214 + loadChatHistory(botId);
3215 + });
3216 + }
3469 3217
3470 3218 // Initialize chat visibility for all instances
3471 3219 $('.mxchat-chatbot-wrapper').each(function() {
3472 3220 var botId = $(this).data('bot-id') || 'default';