PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.9
MxChat – AI Chatbot & Content Generation for WordPress v3.0.9
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 +216 -1178 3.2.83.0.9 View file →
@@ -1,111 +1,6 @@
1 1 jQuery(document).ready(function($) {
2 2
3 - // Nonce refresh — v2 (plan-6a68c9).
4 - //
5 - // The widget no longer relies on a nonce embedded in inline cached HTML.
6 - // Before each chat-send / stream-send / upload, we call the REST endpoint
7 - // GET /wp-json/mxchat/v1/nonce and use the freshly-issued value. The
8 - // endpoint creates the nonce with action `mxchat_chat_send`; the server-side
9 - // verifier ALSO still accepts the legacy `mxchat_chat_nonce` action for a
10 - // 30-day backwards-compat window so cached pages still in users' browsers
11 - // (which carry the legacy inline-localized nonce) keep working.
12 - //
13 - // Cache: a single module-scoped slot. TTL 12h conservatively (WP nonces are
14 - // 24h but we refetch at half-life so a freshly-cached-page user never sees
15 - // a borderline-stale nonce).
16 - var cachedFreshNonce = null;
17 - var cachedFreshNonceFetchedAt = 0;
18 - var NONCE_TTL_MS = 12 * 60 * 60 * 1000;
19 - var nonceRefreshState = 'idle'; // 'idle' | 'pending' | 'done'
20 - var nonceRefreshCallbacks = [];
21 -
22 - function getRestNonceUrl() {
23 - if (typeof mxchatChat !== 'undefined' && mxchatChat.rest_url) {
24 - return mxchatChat.rest_url.replace(/\/+$/, '') + '/nonce';
25 - }
26 - // Fallback: derive from current origin if mxchatChat.rest_url isn't set.
27 - return window.location.origin + '/wp-json/mxchat/v1/nonce';
28 - }
29 -
30 - function fetchFreshNonceFromRest() {
31 - return fetch(getRestNonceUrl(), {
32 - credentials: 'same-origin',
33 - headers: { 'Accept': 'application/json' }
34 - }).then(function (resp) {
35 - if (!resp.ok) {
36 - throw new Error('REST nonce fetch failed: ' + resp.status);
37 - }
38 - return resp.json();
39 - }).then(function (data) {
40 - if (data && data.nonce) {
41 - return data.nonce;
42 - }
43 - throw new Error('REST nonce response had no nonce field.');
44 - });
45 - }
46 -
47 - /**
48 - * withFreshNonce(cb) — invoke cb() after ensuring mxchatChat.nonce is fresh.
49 - * Tries REST endpoint first (cache-bypass design); falls back to the legacy
50 - * admin-ajax refresh path if REST is unavailable. Idempotent — concurrent
51 - * calls share the same in-flight refresh.
52 - */
53 - function withFreshNonce(callback) {
54 - if (typeof mxchatChat === 'undefined') {
55 - if (callback) callback();
56 - return;
57 - }
58 - var now = Date.now();
59 - if (cachedFreshNonce && (now - cachedFreshNonceFetchedAt) < NONCE_TTL_MS) {
60 - mxchatChat.nonce = cachedFreshNonce;
61 - if (callback) callback();
62 - return;
63 - }
64 - if (callback) nonceRefreshCallbacks.push(callback);
65 - if (nonceRefreshState === 'pending') return;
66 - nonceRefreshState = 'pending';
67 -
68 - var resolved = function (nonce) {
69 - if (nonce) {
70 - cachedFreshNonce = nonce;
71 - cachedFreshNonceFetchedAt = Date.now();
72 - mxchatChat.nonce = nonce;
73 - }
74 - nonceRefreshState = 'done';
75 - var pending = nonceRefreshCallbacks;
76 - nonceRefreshCallbacks = [];
77 - pending.forEach(function (cb) { try { cb(); } catch (e) {} });
78 - };
79 -
80 - fetchFreshNonceFromRest()
81 - .then(resolved)
82 - .catch(function () {
83 - // Fallback to the legacy admin-ajax refresh path (issued with the
84 - // old action `mxchat_chat_nonce`; the server still accepts both
85 - // during the compat window).
86 - if (mxchatChat.ajax_url) {
87 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' })
88 - .done(function (res) {
89 - if (res && res.success && res.data && res.data.nonce) {
90 - resolved(res.data.nonce);
91 - return;
92 - }
93 - resolved(null);
94 - })
95 - .fail(function () { resolved(null); });
96 - } else {
97 - resolved(null);
98 - }
99 - });
100 - }
101 -
102 - // Backwards-compat alias — every existing caller in this file (and any
103 - // out-of-tree consumer that hit this internal API) keeps working unchanged.
104 - function refreshNonceIfNeeded(callback) {
105 - return withFreshNonce(callback);
106 - }
107 -
108 3 // ====================================
109 4 // MULTI-INSTANCE MANAGEMENT SYSTEM
110 5 // ====================================
111 6
@@ -121,9 +16,9 @@
121 16 var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
122 17
123 18 this.instances[botId] = {
124 19 botId: botId,
125 - sessionId: null,
20 + sessionId: this.getChatSession(botId),
126 21 lastSeenMessageId: '',
127 22 notificationCheckInterval: null,
128 23 pollingInterval: null,
129 24 processedMessageIds: new Set(),
@@ -148,77 +43,23 @@
148 43 return Object.keys(this.instances);
149 44 },
150 45
151 46 // Session management per bot
152 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
153 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
154 47 getChatSession: function(botId) {
155 48 var cookieName = 'mxchat_session_id_' + botId;
156 - var storageKey = 'mxchat_session_id_' + botId;
157 49 var sessionId = getCookie(cookieName);
158 50
159 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
160 51 if (!sessionId) {
161 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
52 + sessionId = generateSessionId();
53 + this.setChatSession(botId, sessionId);
162 54 }
163 55
164 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
165 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
166 - // storage). Without this, ensureSession() can generate and store an ID that
167 - // getChatSession() then can't read back, causing null session_ids on send.
168 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
169 - sessionId = this.instances[botId].sessionId;
170 - }
171 -
172 - // Guard against stored sentinel values that indicate earlier broken writes.
173 - if (sessionId === 'null' || sessionId === 'undefined') {
174 - sessionId = null;
175 - }
176 -
177 - // Re-sync cookie from localStorage if cookie was lost
178 - if (sessionId && !getCookie(cookieName)) {
179 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
180 - }
181 -
182 - return sessionId || null;
56 + return sessionId;
183 57 },
184 58
185 - // Lazy session initializer — called on first user interaction
186 - ensureSession: function(botId) {
187 - botId = botId || 'default';
188 - var instance = this.instances[botId] || this.init(botId);
189 -
190 - if (instance.sessionId) {
191 - return instance.sessionId;
192 - }
193 -
194 - // Check for existing session from cookie or localStorage
195 - var existingSession = this.getChatSession(botId);
196 -
197 - if (existingSession) {
198 - instance.sessionId = existingSession;
199 - } else {
200 - // Brand new session
201 - var newId = generateSessionId();
202 - this.setChatSession(botId, newId);
203 - instance.sessionId = newId;
204 - }
205 -
206 - // Now that we have a session, do the deferred work
207 - refreshNonceIfNeeded();
208 - trackOriginatingPage();
209 -
210 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
211 - // so we do NOT call it here to avoid a race condition.
212 -
213 - return instance.sessionId;
214 - },
215 -
216 59 setChatSession: function(botId, sessionId) {
217 60 var cookieName = 'mxchat_session_id_' + botId;
218 - var storageKey = 'mxchat_session_id_' + botId;
219 61 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
220 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
221 62 if (this.instances[botId]) {
222 63 this.instances[botId].sessionId = sessionId;
223 64 }
224 65 },
@@ -223,10 +64,8 @@
223 64 }
224 65 },
225 66
226 67 resetChatSession: function(botId) {
227 - // Clear old session from localStorage before setting new one
228 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
229 68 var newSessionId = generateSessionId();
230 69 this.setChatSession(botId, newSessionId);
231 70 var $chatBox = getElement(botId, 'chat-box');
232 71 if ($chatBox.length) {
@@ -235,20 +74,8 @@
235 74 if (this.instances[botId]) {
236 75 this.instances[botId].chatHistoryLoaded = false;
237 76 this.instances[botId].processedMessageIds = new Set();
238 77 }
239 - },
240 -
241 - // Silent reset — new session ID without clearing the chat UI
242 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
243 - silentResetSession: function(botId) {
244 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
245 - var newSessionId = generateSessionId();
246 - this.setChatSession(botId, newSessionId);
247 - if (this.instances[botId]) {
248 - this.instances[botId].sessionId = newSessionId;
249 - }
250 - return newSessionId;
251 78 }
252 79 };
253 80
254 81 // ====================================
@@ -553,9 +380,8 @@
553 380
554 381 // Update your existing sendMessage function
555 382 function sendMessage(botId) {
556 383 botId = botId || 'default';
557 - MxChatInstances.ensureSession(botId);
558 384 var $chatInput = getElement(botId, 'chat-input');
559 385 var message = $chatInput.val();
560 386
561 387 // ADD PROMPT HOOK HERE
@@ -563,14 +389,10 @@
563 389 message = customMxChatFilter(message, "prompt");
564 390 }
565 391
566 392 if (message) {
567 - // Don't disable input in live agent mode - let users chat freely
568 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
569 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
570 - if (!isAgentMode) {
571 - disableChatInput(botId);
572 - }
393 + // Disable input while waiting for response
394 + disableChatInput(botId);
573 395
574 396 appendMessage("user", message, '', [], false, botId);
575 397 $chatInput.val('');
576 398 $chatInput.css('height', 'auto');
@@ -598,9 +420,8 @@
598 420
599 421 // Update your existing sendMessageToChatbot function
600 422 function sendMessageToChatbot(message, botId) {
601 423 botId = botId || 'default';
602 - MxChatInstances.ensureSession(botId);
603 424
604 425 // ADD PROMPT HOOK HERE
605 426 if (typeof customMxChatFilter === 'function') {
606 427 message = customMxChatFilter(message, "prompt");
@@ -605,14 +426,10 @@
605 426 if (typeof customMxChatFilter === 'function') {
606 427 message = customMxChatFilter(message, "prompt");
607 428 }
608 429
609 - // Don't disable input in live agent mode - let users chat freely
610 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
611 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
612 - if (!isAgentMode) {
613 - disableChatInput(botId);
614 - }
430 + // Disable input while waiting for response
431 + disableChatInput(botId);
615 432
616 433 var sessionId = getChatSession(botId);
617 434
618 435 if (hasQuickQuestions(botId)) {
@@ -700,28 +517,13 @@
700 517
701 518 // Get instance for session start timestamp (used when persistence is OFF)
702 519 var instance = MxChatInstances.get(botId);
703 520
704 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
705 - // and returns the guaranteed-present session id from the in-memory instance even when
706 - // cookie/localStorage writes are silently blocked by the browser.
707 - var sessionId = MxChatInstances.ensureSession(botId);
708 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
709 - // Last-resort generation to ensure we never POST a null marker.
710 - sessionId = generateSessionId();
711 - MxChatInstances.setChatSession(botId, sessionId);
712 - }
713 -
714 - // Wait for the page-cache nonce refresh to complete before firing the
715 - // chat-send AJAX. On cached pages the inline mxchatChat.nonce is stale
716 - // until refreshNonceIfNeeded() returns; constructing ajaxData inside the
717 - // callback guarantees we read the fresh value. See plan-c5457f.
718 - refreshNonceIfNeeded(function() {
719 521 // Prepare AJAX data
720 522 const ajaxData = {
721 523 action: 'mxchat_handle_chat_request',
722 524 message: message,
723 - session_id: sessionId,
525 + session_id: getChatSession(botId),
724 526 nonce: mxchatChat.nonce,
725 527 current_page_url: window.location.href,
726 528 current_page_title: document.title,
727 529 bot_id: botId,
@@ -727,14 +529,14 @@
727 529 bot_id: botId,
728 530 // Pass session start timestamp so AI context matches what user sees
729 531 session_start_timestamp: instance.sessionStartTimestamp || 0
730 532 };
731 -
533 +
732 534 // Add page context if available
733 535 if (pageContext) {
734 536 ajaxData.page_context = JSON.stringify(pageContext);
735 537 }
736 -
538 +
737 539 // CHECK FOR VISION FLAGS AND ADD THEM
738 540 if (window.mxchatVisionProcessed) {
739 541 ajaxData.vision_processed = true;
740 542 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -743,9 +545,9 @@
743 545 window.mxchatVisionProcessed = false;
744 546 window.mxchatOriginalMessage = null;
745 547 window.mxchatVisionImagesCount = 0;
746 548 }
747 -
549 +
748 550 $.ajax({
749 551 url: mxchatChat.ajax_url,
750 552 type: 'POST',
751 553 dataType: 'json',
@@ -783,16 +585,23 @@
783 585 errorMessage = "An error occurred. Please try again or contact support.";
784 586 }
785 587
786 588 // Handle session reset action (IP changed, session expired, etc.)
787 - // Silent reset — keep chat UI intact, just get a new session and retry
788 589 if (response.data && response.data.action === 'reset_session') {
789 - MxChatInstances.silentResetSession(botId);
790 - // 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
791 595 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
792 596 if (originalMessage) {
793 597 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
794 - 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';
795 604 if (shouldUseStreaming(currentModel)) {
796 605 callMxChatStream(originalMessage, function(response) {
797 606 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
798 607 }, botId);
@@ -849,11 +658,9 @@
849 658 }
850 659
851 660 // Check for live agent response
852 661 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
853 - removeThinkingDots(botId);
854 662 updateChatModeIndicator('agent', botId);
855 - enableChatInput(botId);
856 663 return;
857 664 }
858 665
859 666 // Handle the message and show notification if chat is hidden
@@ -886,13 +693,9 @@
886 693 $badge.show();
887 694 }
888 695 }
889 696 } else {
890 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
891 - if (response.vectorstore_error) {
892 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
893 - }
894 - replaceLastMessage("bot", emptyMsg, '', [], botId);
697 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
895 698 }
896 699
897 700 if (response.message_id) {
898 701 var instance = MxChatInstances.get(botId);
@@ -934,9 +737,8 @@
934 737
935 738 replaceLastMessage("bot", errorMessage, '', [], botId);
936 739 }
937 740 });
938 - }); // refreshNonceIfNeeded
939 741 }
940 742
941 743 function callMxChatStream(message, callback, botId) {
942 744 botId = botId || getMxChatBotId();
@@ -955,25 +757,12 @@
955 757
956 758 // Get instance for session start timestamp (used when persistence is OFF)
957 759 var instance = MxChatInstances.get(botId);
958 760
959 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
960 - // non-string value via String(), so passing `null` would POST the literal string "null"
961 - // and land in the transcripts table as a ghost session. ensureSession() always returns
962 - // a real string even when cookies/localStorage are blocked.
963 - var streamSessionId = MxChatInstances.ensureSession(botId);
964 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
965 - streamSessionId = generateSessionId();
966 - MxChatInstances.setChatSession(botId, streamSessionId);
967 - }
968 -
969 - // Wait for the page-cache nonce refresh before constructing formData (which
970 - // captures mxchatChat.nonce by value). Mirrors callMxChat's wrapping. See plan-c5457f.
971 - refreshNonceIfNeeded(function() {
972 761 const formData = new FormData();
973 762 formData.append('action', 'mxchat_stream_chat');
974 763 formData.append('message', message);
975 - formData.append('session_id', streamSessionId);
764 + formData.append('session_id', getChatSession(botId));
976 765 formData.append('nonce', mxchatChat.nonce);
977 766 formData.append('current_page_url', window.location.href);
978 767 formData.append('current_page_title', document.title);
979 768 formData.append('bot_id', botId);
@@ -1073,16 +862,8 @@
1073 862
1074 863 // Re-enable chat input when stream ends with content
1075 864 enableChatInput(botId);
1076 865
1077 - // Scroll the user's last message to the top now that the
1078 - // bot's full reply has rendered (gives max reading room).
1079 - var $chatBoxDone = getElement(botId, 'chat-box');
1080 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
1081 - if ($lastUserMsgDone.length) {
1082 - scrollElementToTop($lastUserMsgDone, botId);
1083 - }
1084 -
1085 866 if (callback) {
1086 867 callback(accumulatedContent);
1087 868 }
1088 869 return;
@@ -1105,16 +886,8 @@
1105 886
1106 887 // Re-enable chat input after streaming completes
1107 888 enableChatInput(botId);
1108 889
1109 - // Scroll the user's last message to the top now
1110 - // that the bot's full reply has rendered.
1111 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1112 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1113 - if ($lastUserMsgStreamDone.length) {
1114 - scrollElementToTop($lastUserMsgStreamDone, botId);
1115 - }
1116 -
1117 890 if (callback) {
1118 891 callback(accumulatedContent);
1119 892 }
1120 893 return;
@@ -1193,9 +966,8 @@
1193 966 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1194 967 callMxChat(message, callback, botId);
1195 968 }
1196 969 });
1197 - }); // refreshNonceIfNeeded
1198 970 }
1199 971
1200 972 // Helper function to handle non-streaming responses
1201 973 function handleNonStreamResponse(data, callback, botId) {
@@ -1234,16 +1006,21 @@
1234 1006 errorMessage = "An error occurred. Please try again or contact support.";
1235 1007 }
1236 1008
1237 1009 // Handle session reset action (IP changed, session expired, etc.)
1238 - // Silent reset — keep chat UI intact, just get a new session and retry
1239 1010 if (data.data && data.data.action === 'reset_session') {
1240 - MxChatInstances.silentResetSession(botId);
1241 - // 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
1242 1014 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1243 1015 if (originalMessage) {
1244 1016 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1245 - 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';
1246 1023 if (shouldUseStreaming(currentModel)) {
1247 1024 callMxChatStream(originalMessage, callback, botId);
1248 1025 } else {
1249 1026 callMxChat(originalMessage, callback, botId);
@@ -1265,22 +1042,8 @@
1265 1042 }
1266 1043 return; // Exit early for errors
1267 1044 }
1268 1045
1269 - // Check for live agent response
1270 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1271 - removeThinkingDots(botId);
1272 - // Also remove any leftover bot-message that lost its temporary-message class
1273 - var $chatBox = getElement(botId, 'chat-box');
1274 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1275 - updateChatModeIndicator('agent', botId);
1276 - enableChatInput(botId);
1277 - if (callback) {
1278 - callback('');
1279 - }
1280 - return;
1281 - }
1282 -
1283 1046 // Handle different response formats
1284 1047 if (data.text || data.html || data.message) {
1285 1048
1286 1049 // Apply response hooks
@@ -1325,15 +1088,19 @@
1325 1088 }
1326 1089
1327 1090 // Enhanced updateChatModeIndicator function for immediate DOM updates
1328 1091 function updateChatModeIndicator(mode, botId) {
1092 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1329 1093 botId = botId || 'default';
1330 1094 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1095 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1331 1096 if (indicator) {
1332 1097 const oldText = indicator.textContent;
1098 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1333 1099
1334 1100 if (mode === 'agent') {
1335 1101 indicator.textContent = 'Live Agent';
1102 + console.log('[MxChat] Mode is agent, calling startPolling...');
1336 1103 startPolling(botId);
1337 1104 } else {
1338 1105 // Everything else is AI mode
1339 1106 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1404,12 +1171,9 @@
1404 1171 // Update the event handlers to use the correct function names (using event delegation)
1405 1172 // Use class-based selectors for multi-instance support
1406 1173 $(document).on('click', '.send-button', function() {
1407 1174 var botId = getBotIdFromElement(this);
1408 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1409 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1410 - disableChatInput(botId);
1411 - }
1175 + disableChatInput(botId);
1412 1176 sendMessage(botId);
1413 1177 });
1414 1178
1415 1179 // Override enter key handler (using event delegation)
@@ -1416,237 +1180,14 @@
1416 1180 $(document).on('keypress', '.chat-input', function(e) {
1417 1181 if (e.which == 13 && !e.shiftKey) {
1418 1182 e.preventDefault();
1419 1183 var botId = getBotIdFromElement(this);
1420 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1421 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1422 - disableChatInput(botId);
1423 - }
1184 + disableChatInput(botId);
1424 1185 sendMessage(botId);
1425 1186 }
1426 1187 });
1427 1188
1428 -// Builds the list of overflow-menu items for a given bot.
1429 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1430 -function mxchatGetHeaderMenuItems(botId) {
1431 - var items = [];
1432 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1433 -
1434 - // The `print_button_*` keys still gate this item for back-compat with
1435 - // existing user options. The action is now a transcript download, not print.
1436 - if (settings.print_button_enabled === 'on') {
1437 - items.push({
1438 - id: 'download-transcript',
1439 - label: settings.print_button_label || 'Download Transcript',
1440 - icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
1441 - action: function() {
1442 - mxchatDownloadTranscript(botId);
1443 - }
1444 - });
1445 - }
1446 -
1447 - return items;
1448 -}
1449 -
1450 -// Builds a clean markdown transcript of the current conversation and triggers
1451 -// a file download. Used by the "Download Transcript" menu item.
1452 -function mxchatDownloadTranscript(botId) {
1453 - var $chatBox = getElement(botId, 'chat-box');
1454 - if (!$chatBox || !$chatBox.length) return;
1455 -
1456 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1457 - var headerTitle = settings.print_header_title || 'Chat transcript';
1458 - var now = new Date();
1459 - var stamp = now.toLocaleString();
1460 -
1461 - var lines = [];
1462 - lines.push('# ' + headerTitle);
1463 - lines.push('');
1464 - lines.push('Exported: ' + stamp);
1465 - lines.push('');
1466 - lines.push('---');
1467 - lines.push('');
1468 -
1469 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1470 - var $msg = $(this);
1471 - // Skip thinking placeholders and any in-flight temporary messages.
1472 - if ($msg.find('.thinking-dots').length) return;
1473 - if ($msg.hasClass('temporary-message')) return;
1474 -
1475 - var sender;
1476 - if ($msg.hasClass('user-message')) sender = 'User';
1477 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1478 - else sender = 'AI Agent';
1479 -
1480 - // Strip interactive UI from the cloned message so we get the conversation text.
1481 - var $clone = $msg.clone();
1482 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1483 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1484 - if (!text) return;
1485 -
1486 - lines.push('**' + sender + '**');
1487 - lines.push('');
1488 - lines.push(text);
1489 - lines.push('');
1490 - });
1491 -
1492 - var content = lines.join('\n');
1493 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1494 - var fname = 'mxchat-transcript-' + iso + '.md';
1495 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1496 - var url = URL.createObjectURL(blob);
1497 - var a = document.createElement('a');
1498 - a.href = url;
1499 - a.download = fname;
1500 - a.style.display = 'none';
1501 - document.body.appendChild(a);
1502 - a.click();
1503 - setTimeout(function() {
1504 - if (a.parentNode) a.parentNode.removeChild(a);
1505 - URL.revokeObjectURL(url);
1506 - }, 100);
1507 -}
1508 -
1509 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1510 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1511 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1512 -function mxchatSyncMenuColors(botId, $wrap) {
1513 - if (!$wrap || !$wrap.length) return;
1514 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1515 - if (!$bot.length) return;
1516 - var cs = window.getComputedStyle($bot[0]);
1517 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1518 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1519 - }
1520 - // Bot text color usually lives on a child div, not .bot-message itself.
1521 - var $textChild = $bot.find('[style*="color"]').first();
1522 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1523 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1524 -}
1525 -
1526 -// One-time per-widget init: renders menu items, wires open/close,
1527 -// outside-click, Escape, and arrow-key navigation. If no items, hides the trigger.
1528 -function mxchatInitHeaderMenu(botId) {
1529 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1530 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1531 -
1532 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1533 - var $menu = $wrap.find('.mxchat-header-menu');
1534 - var items = mxchatGetHeaderMenuItems(botId);
1535 -
1536 - // Initial color sync — covers normal page load.
1537 - mxchatSyncMenuColors(botId, $wrap);
1538 -
1539 - if (!items.length) {
1540 - $trigger.hide();
1541 - $menu.hide();
1542 - $wrap.data('mxchatMenuReady', true);
1543 - return;
1544 - }
1545 -
1546 - // Build the menu items.
1547 - $menu.empty();
1548 - items.forEach(function(item, idx) {
1549 - var $btn = $('<button>', {
1550 - type: 'button',
1551 - 'class': 'mxchat-menu-item',
1552 - 'role': 'menuitem',
1553 - 'tabindex': '-1',
1554 - 'data-menu-id': item.id,
1555 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1556 - '<span class="mxchat-menu-item-label"></span>'
1557 - });
1558 - $btn.find('.mxchat-menu-item-label').text(item.label);
1559 - $btn.on('click', function(e) {
1560 - e.preventDefault();
1561 - e.stopPropagation();
1562 - closeMenu();
1563 - try { item.action(); } catch (err) { /* no-op */ }
1564 - });
1565 - $menu.append($btn);
1566 - });
1567 -
1568 - function openMenu() {
1569 - // Re-sync each open in case the active theme changed since init.
1570 - mxchatSyncMenuColors(botId, $wrap);
1571 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1572 - $trigger.attr('aria-expanded', 'true');
1573 - // Focus the first item for keyboard users
1574 - setTimeout(function() {
1575 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1576 - }, 0);
1577 - }
1578 - function closeMenu(returnFocus) {
1579 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1580 - $trigger.attr('aria-expanded', 'false');
1581 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1582 - if (returnFocus) $trigger.trigger('focus');
1583 - }
1584 -
1585 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1586 - // click-to-collapse handler does not fire.
1587 - $trigger.on('click', function(e) {
1588 - e.preventDefault();
1589 - e.stopPropagation();
1590 - if ($menu.hasClass('is-open')) closeMenu();
1591 - else openMenu();
1592 - });
1593 -
1594 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1595 - $menu.on('click', function(e) {
1596 - e.stopPropagation();
1597 - });
1598 -
1599 - // Outside click closes the menu.
1600 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1601 - if (!$menu.hasClass('is-open')) return;
1602 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1603 - closeMenu();
1604 - });
1605 -
1606 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1607 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1608 - var $items = $menu.find('.mxchat-menu-item');
1609 - var idx = $items.index(this);
1610 - if (e.key === 'Escape') {
1611 - e.preventDefault();
1612 - closeMenu(true);
1613 - } else if (e.key === 'ArrowDown') {
1614 - e.preventDefault();
1615 - var $next = $items.eq((idx + 1) % $items.length);
1616 - $items.attr('tabindex', '-1');
1617 - $next.attr('tabindex', '0').trigger('focus');
1618 - } else if (e.key === 'ArrowUp') {
1619 - e.preventDefault();
1620 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1621 - $items.attr('tabindex', '-1');
1622 - $prev.attr('tabindex', '0').trigger('focus');
1623 - } else if (e.key === 'Enter' || e.key === ' ') {
1624 - e.preventDefault();
1625 - $(this).trigger('click');
1626 - }
1627 - });
1628 - $trigger.on('keydown', function(e) {
1629 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1630 - e.preventDefault();
1631 - closeMenu(true);
1632 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1633 - e.preventDefault();
1634 - openMenu();
1635 - }
1636 - });
1637 -
1638 - $wrap.data('mxchatMenuReady', true);
1639 -}
1640 -
1641 -// Initialize header menus for every rendered widget on DOM ready.
1642 -$(function() {
1643 - $('.mxchat-header-menu-wrap').each(function() {
1644 - var botId = $(this).data('bot-id');
1645 - if (botId) mxchatInitHeaderMenu(botId);
1646 - });
1647 -});
1648 -
1189 +
1649 1190 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1650 1191 try {
1651 1192 // Determine styles based on sender type
1652 1193 let messageClass, bgColor, fontColor;
@@ -1684,12 +1225,17 @@
1684 1225 'margin-bottom': '1em'
1685 1226 });
1686 1227 }
1687 1228
1688 - // Process the message content - always run linkify to convert markdown
1689 - // links and format text. linkify() handles existing HTML safely via
1690 - // negative lookaheads that skip URLs already inside <a> tags.
1691 - 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 + }
1692 1238
1693 1239 // Add images if provided
1694 1240 if (images && images.length > 0) {
1695 1241 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1738,12 +1284,8 @@
1738 1284 if (lastUserMessage.length) {
1739 1285 scrollElementToTop(lastUserMessage, botId);
1740 1286 }
1741 1287 }
1742 -
1743 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1744 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1745 - }
1746 1288 });
1747 1289
1748 1290 if (messageText.id) {
1749 1291 var instance = MxChatInstances.get(botId);
@@ -1828,12 +1370,26 @@
1828 1370 bgColor = botMessageBgColor;
1829 1371 fontColor = botMessageFontColor;
1830 1372 }
1831 1373
1832 - // Always run linkify to convert markdown links and format text.
1833 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1834 - // to avoid double-processing URLs that are already inside <a> tags).
1835 - 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 + }
1836 1392
1837 1393 if (responseHtml) {
1838 1394 // Only add line breaks if there's actual text content before the HTML
1839 1395 if (fullMessage && fullMessage.trim()) {
@@ -1890,12 +1446,8 @@
1890 1446 }
1891 1447
1892 1448 // Re-enable chat input after response is displayed
1893 1449 enableChatInput(botId);
1894 -
1895 - if (sender === "bot" || sender === "agent") {
1896 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1897 - }
1898 1450 } else {
1899 1451 appendMessage(sender, responseText, responseHtml, images, false, botId);
1900 1452 // Re-enable chat input after response is displayed
1901 1453 enableChatInput(botId);
@@ -1904,15 +1456,8 @@
1904 1456
1905 1457
1906 1458 function appendThinkingMessage(botId) {
1907 1459 botId = botId || 'default';
1908 -
1909 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
1910 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
1911 - if (indicator && indicator.textContent === 'Live Agent') {
1912 - return;
1913 - }
1914 -
1915 1460 var $chatBox = getElement(botId, 'chat-box');
1916 1461
1917 1462 // Remove any existing thinking dots in this bot's chat first
1918 1463 $chatBox.find('.thinking-dots').remove();
@@ -1934,9 +1479,9 @@
1934 1479 '</div>' +
1935 1480 '</div>';
1936 1481
1937 1482 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1938 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1483 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
1939 1484 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1940 1485 scrollToBottom(botId);
1941 1486 }
1942 1487
@@ -1942,11 +1487,9 @@
1942 1487
1943 1488 function removeThinkingDots(botId) {
1944 1489 botId = botId || 'default';
1945 1490 var $chatBox = getElement(botId, 'chat-box');
1946 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1947 1491 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1948 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1949 1492 }
1950 1493
1951 1494 // ====================================
1952 1495 // TEXT FORMATTING & PROCESSING
@@ -1980,12 +1523,9 @@
1980 1523 processedText = formatTextStyling(processedText);
1981 1524
1982 1525 // Process code blocks BEFORE processing links
1983 1526 processedText = formatCodeBlocks(processedText);
1984 -
1985 - // Process markdown tables BEFORE converting newlines to paragraphs
1986 - processedText = formatMarkdownTables(processedText);
1987 -
1527 +
1988 1528 // NOW convert to paragraphs
1989 1529 processedText = convertNewlinesToBreaks(processedText);
1990 1530
1991 1531 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1998,63 +1538,37 @@
1998 1538 // Return as a proper link without the brackets
1999 1539 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2000 1540 });
2001 1541
2002 - // Process markdown links: [text](url) and [](url)
2003 - // Uses balanced parenthesis matching to handle URLs containing parens
2004 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
2005 - processedText = (function(input) {
2006 - var result = '';
2007 - var i = 0;
2008 - while (i < input.length) {
2009 - // Look for [ at current position
2010 - if (input[i] === '[') {
2011 - // Find closing ]
2012 - var closeBracket = input.indexOf(']', i + 1);
2013 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
2014 - result += input[i];
2015 - i++;
2016 - continue;
2017 - }
2018 - var linkText = input.substring(i + 1, closeBracket);
2019 - // Check if URL starts with http
2020 - var urlStart = closeBracket + 2;
2021 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
2022 - result += input[i];
2023 - i++;
2024 - continue;
2025 - }
2026 - // Find balanced closing paren
2027 - var depth = 1;
2028 - var j = urlStart;
2029 - while (j < input.length && depth > 0) {
2030 - if (input[j] === '(') depth++;
2031 - else if (input[j] === ')') depth--;
2032 - if (depth > 0) j++;
2033 - }
2034 - if (depth !== 0) {
2035 - result += input[i];
2036 - i++;
2037 - continue;
2038 - }
2039 - var url = input.substring(urlStart, j);
2040 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
2041 - var encodedUrl = safeEncodeUrl(cleanUrl);
2042 - if (!linkText || !linkText.trim()) {
2043 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
2044 - } else {
2045 - var safeText = sanitizeUserInput(linkText);
2046 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
2047 - }
2048 - i = j + 1; // Skip past the closing )
2049 - } else {
2050 - result += input[i];
2051 - i++;
2052 - }
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>`;
2053 1552 }
2054 - return result;
2055 - })(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 + });
2056 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 +
2057 1571 // Process phone numbers: [text](tel:number)
2058 1572 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
2059 1573 processedText = processedText.replace(phonePattern, (match, text, phone) => {
2060 1574 const safePhone = safeEncodeUrl(phone);
@@ -2206,78 +1720,9 @@
2206 1720 });
2207 1721
2208 1722 return text;
2209 1723 }
2210 -
2211 - function formatMarkdownTables(text) {
2212 - var lines = text.split('\n');
2213 - var result = [];
2214 - var i = 0;
2215 -
2216 - while (i < lines.length) {
2217 - // Check for a table: current line has pipes AND next line is a separator row
2218 - if (i + 1 < lines.length &&
2219 - lines[i].indexOf('|') !== -1 &&
2220 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2221 -
2222 - var tableLines = [];
2223 - var headerLine = lines[i];
2224 - var separatorLine = lines[i + 1];
2225 - tableLines.push(headerLine);
2226 - tableLines.push(separatorLine);
2227 -
2228 - // Collect remaining table rows
2229 - var j = i + 2;
2230 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2231 - tableLines.push(lines[j]);
2232 - j++;
2233 - }
2234 -
2235 - // Parse alignment from separator row
2236 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2237 - var alignments = sepCells.map(function(cell) {
2238 - var trimmed = cell.trim();
2239 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2240 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2241 - return 'left';
2242 - });
2243 -
2244 - // Build HTML table
2245 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2246 -
2247 - // Header row
2248 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2249 - html += '<thead><tr>';
2250 - headerCells.forEach(function(cell, idx) {
2251 - var align = alignments[idx] || 'left';
2252 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2253 - });
2254 - html += '</tr></thead>';
2255 -
2256 - // Body rows
2257 - html += '<tbody>';
2258 - for (var r = 2; r < tableLines.length; r++) {
2259 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2260 - html += '<tr>';
2261 - rowCells.forEach(function(cell, idx) {
2262 - var align = alignments[idx] || 'left';
2263 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2264 - });
2265 - html += '</tr>';
2266 - }
2267 - html += '</tbody></table></div>';
2268 -
2269 - result.push(html);
2270 - i = j;
2271 - } else {
2272 - result.push(lines[i]);
2273 - i++;
2274 - }
2275 - }
2276 -
2277 - return result.join('\n');
2278 - }
2279 -
1724 +
2280 1725 function sanitizeUserInput(text) {
2281 1726 const div = document.createElement('div');
2282 1727 div.textContent = text;
2283 1728 return div.innerHTML;
@@ -2348,14 +1793,13 @@
2348 1793 requestAnimationFrame(smoothScroll);
2349 1794 }
2350 1795 }
2351 1796
2352 - function scrollElementToTop(element, botId, topOffset) {
1797 + function scrollElementToTop(element, botId) {
2353 1798 botId = botId || 'default';
2354 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2355 1799 var chatBox = getElement(botId, 'chat-box');
2356 1800 var elementTop = element.position().top + chatBox.scrollTop();
2357 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1801 + chatBox.animate({ scrollTop: elementTop }, 500);
2358 1802 }
2359 1803
2360 1804 function showChatWidget(botId) {
2361 1805 botId = botId || 'default';
@@ -2499,12 +1943,15 @@
2499 1943 // LIVE AGENT FUNCTIONALITY
2500 1944 // ====================================
2501 1945
2502 1946 function startPolling(botId) {
1947 + console.log('[MxChat] startPolling called for botId:', botId);
2503 1948 botId = botId || 'default';
2504 1949 var instance = MxChatInstances.get(botId);
2505 1950 // Clear any existing interval first
2506 1951 stopPolling(botId);
1952 + // Start new polling interval
1953 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2507 1954 instance.pollingInterval = setInterval(function() {
2508 1955 checkForAgentMessages(botId);
2509 1956 }, 5000);
2510 1957 }
@@ -2509,17 +1956,20 @@
2509 1956 }, 5000);
2510 1957 }
2511 1958
2512 1959 function stopPolling(botId) {
1960 + console.log('[MxChat] stopPolling called for botId:', botId);
2513 1961 botId = botId || 'default';
2514 1962 var instance = MxChatInstances.get(botId);
2515 1963 if (instance.pollingInterval) {
2516 1964 clearInterval(instance.pollingInterval);
2517 1965 instance.pollingInterval = null;
1966 + console.log('[MxChat] Polling stopped for botId:', botId);
2518 1967 }
2519 1968 }
2520 1969
2521 1970 function checkForAgentMessages(botId) {
1971 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2522 1972 botId = botId || 'default';
2523 1973 var instance = MxChatInstances.get(botId);
2524 1974 const sessionId = getChatSession(botId);
2525 1975 $.ajax({
@@ -2545,12 +1995,8 @@
2545 1995 instance.processedMessageIds.add(message.id);
2546 1996 }
2547 1997 });
2548 1998
2549 - if (hasNewMessage) {
2550 - enableChatInput(botId);
2551 - }
2552 -
2553 1999 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2554 2000 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2555 2001 showNotification(botId);
2556 2002 }
@@ -2556,13 +2002,8 @@
2556 2002 }
2557 2003
2558 2004 scrollToBottom(botId, true);
2559 2005 }
2560 -
2561 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2562 - if (response.success && response.data?.chat_mode) {
2563 - updateChatModeIndicator(response.data.chat_mode, botId);
2564 - }
2565 2006 },
2566 2007 error: function (xhr, status, error) {
2567 2008 // Polling error - silently continue
2568 2009 }
@@ -2572,29 +2013,20 @@
2572 2013 // ====================================
2573 2014 // CHAT HISTORY & PERSISTENCE
2574 2015 // ====================================
2575 2016
2576 -function loadChatHistory(botId, onComplete) {
2017 +function loadChatHistory(botId) {
2577 2018 botId = botId || 'default';
2578 2019 var instance = MxChatInstances.get(botId);
2579 2020
2580 2021 // Prevent duplicate loading
2581 2022 if (instance.chatHistoryLoaded) {
2582 - if (onComplete) onComplete();
2583 2023 return;
2584 2024 }
2585 2025
2586 - // Use getChatSession which returns null if no session exists (does NOT create one)
2587 2026 var sessionId = getChatSession(botId);
2588 2027 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2589 2028
2590 - // No session yet — nothing to load. History will load after first message via ensureSession.
2591 - if (!sessionId) {
2592 - instance.chatHistoryLoaded = true;
2593 - if (onComplete) onComplete();
2594 - return;
2595 - }
2596 -
2597 2029 if (chatPersistenceEnabled && sessionId) {
2598 2030 $.ajax({
2599 2031 url: mxchatChat.ajax_url,
2600 2032 type: 'POST',
@@ -2605,12 +2037,11 @@
2605 2037 },
2606 2038 success: function(response) {
2607 2039 // Handle session reset (IP changed while user was away)
2608 2040 if (response.success === false && response.data && response.data.action === 'reset_session') {
2609 - // Silent reset — new session but don't clear UI
2610 - MxChatInstances.silentResetSession(botId);
2041 + // Silently reset session - user will start fresh
2042 + resetChatSession(botId);
2611 2043 instance.chatHistoryLoaded = true; // Prevent retry loop
2612 - if (onComplete) onComplete();
2613 2044 return;
2614 2045 }
2615 2046
2616 2047 // Check if the response indicates success
@@ -2666,19 +2097,9 @@
2666 2097 var content = message.content;
2667 2098 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2668 2099 content = decodeHTMLEntities(content);
2669 2100
2670 - // Skip linkify for messages containing structured HTML
2671 - // (forms, product cards, galleries, etc.) to avoid
2672 - // markdown formatting corrupting HTML attributes
2673 - // (e.g. underscores in name="field_name" becoming <em> tags)
2674 - if (content.includes("mxchat-product-card") ||
2675 - content.includes("mxchat-image-gallery") ||
2676 - content.includes("mxchat-featured-products") ||
2677 - content.includes("<form") ||
2678 - content.includes("<input") ||
2679 - content.includes("<select") ||
2680 - content.includes("<textarea")) {
2101 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2681 2102 messageElement.html(content);
2682 2103 } else {
2683 2104 var formattedContent = linkify(content);
2684 2105 messageElement.html(formattedContent);
@@ -2718,17 +2139,13 @@
2718 2139 instance.chatHistoryLoaded = true;
2719 2140 }
2720 2141 }
2721 2142 }
2722 - if (onComplete) onComplete();
2723 2143 },
2724 2144 error: function(xhr, status, error) {
2725 2145 // Error loading chat history - silently continue
2726 - if (onComplete) onComplete();
2727 2146 }
2728 2147 });
2729 - } else {
2730 - if (onComplete) onComplete();
2731 2148 }
2732 2149 }
2733 2150
2734 2151
@@ -2904,35 +2321,45 @@
2904 2321 // ====================================
2905 2322
2906 2323 function checkPreChatDismissal(botId) {
2907 2324 botId = botId || 'default';
2908 - try {
2909 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2910 - if (dismissedAt) {
2911 - // Re-show after 24 hours
2912 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2913 - 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 {
2914 2336 getElement(botId, 'pre-chat-message').hide();
2915 - return;
2916 2337 }
2917 - // Expired — clear and show again
2918 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2338 + },
2339 + error: function() {
2340 + // Error checking pre-chat dismissal - silently continue
2919 2341 }
2920 - getElement(botId, 'pre-chat-message').fadeIn(250);
2921 - } catch (e) {
2922 - // localStorage unavailable — show the message
2923 - getElement(botId, 'pre-chat-message').fadeIn(250);
2924 - }
2342 + });
2925 2343 }
2926 2344
2927 2345 function handlePreChatDismissal(botId) {
2928 2346 botId = botId || 'default';
2929 2347 getElement(botId, 'pre-chat-message').fadeOut(200);
2930 - try {
2931 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2932 - } catch (e) {
2933 - // localStorage unavailable — dismissal won't persist
2934 - }
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 + });
2935 2362 }
2936 2363
2937 2364
2938 2365 // ====================================
@@ -2987,14 +2414,9 @@
2987 2414 collapseQuickQuestions(botId);
2988 2415 });
2989 2416
2990 2417 // Chatbot visibility toggle handlers - use class selector for multi-instance support
2991 - // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
2992 - $(document).on('click keydown', '.floating-chatbot-button', function(e) {
2993 - if (e.type === 'keydown') {
2994 - if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
2995 - e.preventDefault();
2996 - }
2418 + $(document).on('click', '.floating-chatbot-button', function() {
2997 2419 var botId = getBotIdFromElement(this);
2998 2420 var $chatbot = getElement(botId, 'floating-chatbot');
2999 2421 var $badge = getElement(botId, 'chat-notification-badge');
3000 2422 var $preChat = getElement(botId, 'pre-chat-message');
@@ -2999,83 +2421,35 @@
2999 2421 var $badge = getElement(botId, 'chat-notification-badge');
3000 2422 var $preChat = getElement(botId, 'pre-chat-message');
3001 2423
3002 2424 if ($chatbot.hasClass('hidden')) {
3003 - $chatbot.removeClass('hidden').addClass('visible')
3004 - .attr('aria-modal', 'true').attr('role', 'dialog');
3005 - $(this).addClass('hidden').attr('aria-expanded', 'true');
2425 + $chatbot.removeClass('hidden').addClass('visible');
2426 + $(this).addClass('hidden');
3006 2427 $badge.hide(); // Hide notification when opening chat
3007 2428 disableScroll();
3008 2429 $preChat.fadeOut(250);
3009 -
3010 - // Load chat history for returning visitors (persistence)
3011 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3012 - if (chatPersistenceEnabled) {
3013 - MxChatInstances.ensureSession(botId);
3014 - }
3015 -
3016 - // Deferred email check — only on first widget open
3017 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3018 - var instance = MxChatInstances.get(botId);
3019 - if (emailBlocker && !instance.emailCheckDone) {
3020 - instance.emailCheckDone = true;
3021 - resolveEmailState(botId);
3022 - } else if (!emailBlocker) {
3023 - // No email collection — still route through showChatContainerForBot
3024 - // so the loader is shown while chat history loads
3025 - showChatContainerForBot(botId);
3026 - }
3027 -
3028 - // Move keyboard focus into the message input after the open transition.
3029 - setTimeout(function() {
3030 - var chatInput = getElementDOM(botId, 'chat-input');
3031 - if (chatInput && !chatInput.disabled) {
3032 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
3033 - }
3034 - }, 300);
3035 2430 } else {
3036 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3037 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2431 + $chatbot.removeClass('visible').addClass('hidden');
2432 + $(this).removeClass('hidden');
3038 2433 enableScroll();
3039 2434 checkPreChatDismissal(botId);
3040 2435 }
3041 2436 });
3042 2437
3043 - // Allow clicking anywhere on the title bar to close the chatbot.
3044 - // Returns keyboard focus to the launcher so keyboard users don't get
3045 - // stranded at <body> (WCAG SC 2.4.3 Focus Order). :focus-visible is
3046 - // heuristic-based so mouse-triggered close won't show a focus ring.
2438 + // Allow clicking anywhere on the title bar to close the chatbot
3047 2439 $(document).on('click', '.chatbot-top-bar', function() {
3048 2440 var botId = getBotIdFromElement(this);
3049 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3050 - var $launcher = getElement(botId, 'floating-chatbot-button');
3051 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
2441 + getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2442 + getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3052 2443 enableScroll();
3053 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3054 2444 });
3055 2445
3056 - // Global Escape-key handler — closes any visible chat widget and
3057 - // returns focus to its launcher. Standard modal-dismissal pattern;
3058 - // pairs with aria-modal="true" set on the widget when it opens.
3059 - $(document).on('keydown', function(e) {
3060 - if (e.key !== 'Escape' && e.key !== 'Esc') return;
3061 - var $visible = $('.floating-chatbot.visible');
3062 - if (!$visible.length) return;
3063 - e.preventDefault();
3064 - $visible.each(function() {
3065 - var botId = getBotIdFromElement(this);
3066 - $(this).addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3067 - var $launcher = getElement(botId, 'floating-chatbot-button');
3068 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
3069 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3070 - });
3071 - enableScroll();
3072 - });
3073 -
3074 2446 $(document).on('click', '.close-pre-chat-message', function(e) {
3075 2447 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3076 2448 var botId = getBotIdFromElement(this);
3077 - handlePreChatDismissal(botId);
2449 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2450 + $(this).remove();
2451 + });
3078 2452 });
3079 2453
3080 2454
3081 2455 // PDF upload button handlers - use class selector
@@ -3116,10 +2490,8 @@
3116 2490 const sendBtn = document.getElementById('send-button');
3117 2491 const originalBtnContent = uploadBtn.innerHTML;
3118 2492
3119 2493 try {
3120 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3121 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3122 2494 const formData = new FormData();
3123 2495 formData.append('action', 'mxchat_upload_pdf');
3124 2496 formData.append('pdf_file', file);
3125 2497 formData.append('session_id', sessionId);
@@ -3183,10 +2555,8 @@
3183 2555 const sendBtn = document.getElementById('send-button');
3184 2556 const originalBtnContent = uploadBtn.innerHTML;
3185 2557
3186 2558 try {
3187 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3188 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3189 2559 const formData = new FormData();
3190 2560 formData.append('action', 'mxchat_upload_word');
3191 2561 formData.append('word_file', file);
3192 2562 formData.append('session_id', sessionId);
@@ -3280,59 +2650,8 @@
3280 2650 });
3281 2651
3282 2652
3283 2653 // ====================================
3284 -// INIT LOADER & CHAT CONTAINER HELPERS
3285 -// ====================================
3286 -// These must be outside the email collection block so they're always available
3287 -// (used by persistence loading even when email collection is off)
3288 -
3289 -function showInitLoader(botId) {
3290 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3291 - if (loader) loader.style.display = 'flex';
3292 -}
3293 -
3294 -function hideInitLoader(botId) {
3295 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3296 - if (loader) loader.style.display = 'none';
3297 -}
3298 -
3299 -function showEmailFormForBot(botId) {
3300 - hideInitLoader(botId);
3301 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3302 - var chatContainer = getElementDOM(botId, 'chat-container');
3303 - if (emailBlocker) emailBlocker.style.display = 'flex';
3304 - if (chatContainer) chatContainer.style.display = 'none';
3305 -}
3306 -
3307 -function showChatContainerForBot(botId) {
3308 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3309 - var chatContainer = getElementDOM(botId, 'chat-container');
3310 - if (emailBlocker) emailBlocker.style.display = 'none';
3311 -
3312 - var instance = MxChatInstances.get(botId);
3313 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3314 -
3315 - // If persistence is on and history hasn't loaded yet, show loader
3316 - // while history loads to prevent flash of empty chat
3317 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3318 - if (chatContainer) chatContainer.style.display = 'none';
3319 - showInitLoader(botId);
3320 - loadChatHistory(botId, function() {
3321 - hideInitLoader(botId);
3322 - if (chatContainer) chatContainer.style.display = 'flex';
3323 - scrollToBottom(botId, true);
3324 - });
3325 - } else {
3326 - hideInitLoader(botId);
3327 - if (chatContainer) chatContainer.style.display = 'flex';
3328 - if (typeof loadChatHistory === 'function') {
3329 - loadChatHistory(botId);
3330 - }
3331 - }
3332 -}
3333 -
3334 -// ====================================
3335 2654 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3336 2655 // ====================================
3337 2656 // Only run email collection setup if it's enabled
3338 2657 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3368,8 +2687,28 @@
3368 2687 `;
3369 2688 document.head.appendChild(style);
3370 2689 }
3371 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 +
3372 2711 function isValidEmailAddress(email) {
3373 2712 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3374 2713 return emailRegex.test(email.trim()) && email.length <= 254;
3375 2714 }
@@ -3491,31 +2830,11 @@
3491 2830 existingErrors.forEach(error => error.remove());
3492 2831 }
3493 2832 }
3494 2833
3495 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3496 - function resolveEmailState(botId) {
3497 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3498 - if (mxchatChat.initial_email_state.show_email_form) {
3499 - showEmailFormForBot(botId);
3500 - } else {
3501 - showChatContainerForBot(botId);
3502 - }
3503 - } else {
3504 - checkSessionAndEmailForBot(botId);
3505 - }
3506 - }
3507 -
3508 2834 function checkSessionAndEmailForBot(botId) {
3509 - const sessionId = MxChatInstances.ensureSession(botId);
2835 + const sessionId = getChatSession(botId);
3510 2836
3511 - // Hide both panels while we check — show loader instead
3512 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3513 - var chatContainer = getElementDOM(botId, 'chat-container');
3514 - if (emailBlocker) emailBlocker.style.display = 'none';
3515 - if (chatContainer) chatContainer.style.display = 'none';
3516 - showInitLoader(botId);
3517 -
3518 2837 fetch(mxchatChat.ajax_url, {
3519 2838 method: 'POST',
3520 2839 headers: {
3521 2840 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3563,9 +2882,9 @@
3563 2882 var emailInput = getElementDOM(botId, 'user-email');
3564 2883 var nameInput = getElementDOM(botId, 'user-name');
3565 2884 var userEmail = emailInput ? emailInput.value.trim() : '';
3566 2885 var userName = nameInput ? nameInput.value.trim() : '';
3567 - var sessionId = MxChatInstances.ensureSession(botId);
2886 + var sessionId = getChatSession(botId);
3568 2887
3569 2888 // Validate email
3570 2889 if (!userEmail) {
3571 2890 showEmailError(botId, 'Please enter your email address.');
@@ -3688,27 +3007,25 @@
3688 3007 }
3689 3008 });
3690 3009
3691 3010 // Initialize email check for all bot instances
3692 - // For floating bots: defer until widget is opened (zero passive AJAX)
3693 - // For embedded bots: check immediately since the form is visible
3694 3011 $('.mxchat-chatbot-wrapper').each(function() {
3695 3012 var botId = $(this).data('bot-id') || 'default';
3696 3013 var emailBlocker = getElementDOM(botId, 'email-blocker');
3697 3014
3015 + // Only check if email blocker exists for this bot
3698 3016 if (emailBlocker) {
3699 - if (isEmbeddedBot(botId)) {
3700 - // Embedded bots are always visible — check now
3701 - 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);
3702 3027 }
3703 - // Floating bots: handled in the widget open handler
3704 - } else if (isEmbeddedBot(botId)) {
3705 - // Embedded bot, no email collection — load history with loader
3706 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3707 - if (chatPersistenceEnabled) {
3708 - MxChatInstances.ensureSession(botId);
3709 - showChatContainerForBot(botId);
3710 - }
3711 3028 }
3712 3029 });
3713 3030 }
3714 3031
@@ -3718,32 +3035,39 @@
3718 3035 var $chatbot = getElement(botId, 'floating-chatbot');
3719 3036 if ($chatbot.hasClass('hidden')) {
3720 3037 $chatbot.removeClass('hidden').addClass('visible');
3721 3038 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3722 - handlePreChatDismissal(botId);
3039 + $(this).fadeOut(250); // Hide pre-chat message
3723 3040 disableScroll(); // Disable scroll when chatbot opens
3041 + }
3042 + });
3724 3043
3725 - // Load chat history for returning visitors (persistence)
3726 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3727 - if (chatPersistenceEnabled) {
3728 - MxChatInstances.ensureSession(botId);
3729 - }
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
3730 3050
3731 - // Deferred email check — only on first widget open
3732 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3733 - var instance = MxChatInstances.get(botId);
3734 - if (emailBlocker && !instance.emailCheckDone) {
3735 - instance.emailCheckDone = true;
3736 - resolveEmailState(botId);
3737 - } else if (!emailBlocker) {
3738 - 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
3739 3065 }
3740 - }
3066 + });
3741 3067 });
3742 3068
3743 - // Legacy duplicate close handler removed — handled by single event delegation above
3744 3069
3745 -
3746 3070 function hasQuickQuestions(botId) {
3747 3071 botId = botId || 'default';
3748 3072 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3749 3073 if (!questionsContainer) return false;
@@ -3879,11 +3203,18 @@
3879 3203 });
3880 3204
3881 3205 // Initialize when document is ready
3882 3206 setFullHeight();
3207 + trackOriginatingPage();
3883 3208
3884 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3885 - // 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 + }
3886 3217
3887 3218 // Initialize chat visibility for all instances
3888 3219 $('.mxchat-chatbot-wrapper').each(function() {
3889 3220 var botId = $(this).data('bot-id') || 'default';
@@ -3936,299 +3267,6 @@
3936 3267 }, 2000);
3937 3268 });
3938 3269 }
3939 3270 }
3940 -});
3941 -
3942 -// ============================================================================
3943 -// SATISFACTION RATING (v3.2.6)
3944 -// ============================================================================
3945 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
3946 -// inactivity following a bot reply. One prompt per session, deduped via
3947 -// localStorage. Disabled site-wide when mxchatChat.satisfaction_rating_enabled
3948 -// is exactly false (default ON).
3949 -jQuery(function($) {
3950 - if (typeof mxchatChat === 'undefined') return;
3951 - if (mxchatChat.satisfaction_rating_enabled === false || mxchatChat.satisfaction_rating_enabled === 'off') return;
3952 -
3953 - // wp_localize_script stringifies ints, so accept both number and numeric string.
3954 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
3955 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
3956 - if (!isFinite(idleSeconds)) idleSeconds = 60;
3957 - if (idleSeconds < 5) idleSeconds = 5;
3958 - if (idleSeconds > 600) idleSeconds = 600;
3959 - var IDLE_MS = idleSeconds * 1000;
3960 - var MIN_BOT_REPLIES = 2;
3961 - var ratingState = {};
3962 -
3963 - function getState(botId) {
3964 - if (!ratingState[botId]) {
3965 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
3966 - }
3967 - return ratingState[botId];
3968 - }
3969 -
3970 - function getSessionId(botId) {
3971 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
3972 - return MxChatInstances.getChatSession(botId);
3973 - }
3974 - return null;
3975 - }
3976 -
3977 - function isAlreadyRated(sessionId) {
3978 - if (!sessionId) return false;
3979 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
3980 - }
3981 -
3982 - function markRated(sessionId) {
3983 - if (!sessionId) return;
3984 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
3985 - }
3986 -
3987 - function esc(s) {
3988 - return String(s == null ? '' : s)
3989 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
3990 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
3991 - }
3992 -
3993 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
3994 - function ratingSkipInlineColors(botId) {
3995 - if (mxchatChat.skip_inline_colors) return true;
3996 - var botAssignments = mxchatChat.bot_theme_assignments || {};
3997 - return botAssignments.hasOwnProperty(botId);
3998 - }
3999 -
4000 - function botBubbleStyleAttr(botId) {
4001 - if (ratingSkipInlineColors(botId)) return '';
4002 - var bg = mxchatChat.bot_message_bg_color;
4003 - var fg = mxchatChat.bot_message_font_color;
4004 - if (!bg && !fg) return '';
4005 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
4006 - }
4007 -
4008 - // Reads the rating bubble's actual computed fg+bg (whatever paints it —
4009 - // the inline color pickers OR the mxchat-theme AI customizer's injected CSS)
4010 - // and paints the filled "Send" pill so it fills with the bot font color and
4011 - // labels in the bubble bg. Mirrors mxchatSyncMenuColors(~:1512) for the read.
4012 - // We paint the submit button DIRECTLY (inline longhand) rather than relying
4013 - // on the CSS rule's var()s: Chromium resolves an INHERITED custom property
4014 - // unreliably inside a descendant's `background`, so a bubble-level var would
4015 - // silently fall back to the literal (white-block bug all over again). Inline
4016 - // longhand always wins. Same transparent-guard as the menu so we never paint
4017 - // a see-through value — in that case the CSS literal fallbacks keep it legible.
4018 - function syncRatingBubbleColors(botId) {
4019 - var $chatBox = getChatBoxByBotId(botId);
4020 - if (!$chatBox || !$chatBox.length) return;
4021 - var bubbleEl = $chatBox.find('.mxchat-rating-bot-bubble').last()[0];
4022 - if (!bubbleEl) return;
4023 - var cs = window.getComputedStyle(bubbleEl);
4024 - var fg = cs.color;
4025 - var bg = cs.backgroundColor;
4026 - var hasFg = fg && fg !== 'rgba(0, 0, 0, 0)' && fg !== 'transparent';
4027 - var hasBg = bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent';
4028 - // Expose on the bubble too, for any inheriting styles / future use.
4029 - if (hasFg) bubbleEl.style.setProperty('--mxchat-bot-fg', fg);
4030 - if (hasBg) bubbleEl.style.setProperty('--mxchat-bot-bg', bg);
4031 - // Paint the Send pill directly — the part that actually fixes the bug.
4032 - var submitEl = bubbleEl.querySelector('.mxchat-rating-submit');
4033 - if (submitEl) {
4034 - if (hasFg) submitEl.style.backgroundColor = fg; // fill = bot font color
4035 - if (hasBg) submitEl.style.color = bg; // label = bubble background
4036 - }
4037 - }
4038 -
4039 - function copy(key) {
4040 - var c = mxchatChat.satisfaction_rating_copy || {};
4041 - var d = {
4042 - question: 'Was this helpful?',
4043 - helpful: 'Helpful',
4044 - not_helpful: 'Not helpful',
4045 - dismiss: 'Dismiss',
4046 - thanks: 'Thanks! Anything we should improve? (optional)',
4047 - placeholder: 'Tell us what could be better…',
4048 - send: 'Send',
4049 - skip: 'Skip',
4050 - saved: 'Thanks for the feedback.'
4051 - };
4052 - return c[key] || d[key];
4053 - }
4054 -
4055 - function thumbUpSvg() {
4056 - return '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 0 1 6 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 0 1 2.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 0 0 .322-1.672V2.75A.75.75 0 0 1 15 2a2.25 2.25 0 0 1 2.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 0 1-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 0 0-1.423-.23h-.777Z"/><path d="M2.331 10.977a11.969 11.969 0 0 0-.831 4.398 12 12 0 0 0 .52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 0 1-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227Z"/></svg>';
4057 - }
4058 - function thumbDownSvg() {
4059 - return '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M15.73 5.25h1.035A7.465 7.465 0 0 1 18 9.375a7.465 7.465 0 0 1-1.235 4.125h-.148c-.806 0-1.534.446-2.031 1.08a9.04 9.04 0 0 1-2.861 2.4c-.723.384-1.35.956-1.653 1.715a4.498 4.498 0 0 0-.322 1.672V21a.75.75 0 0 1-.75.75 2.25 2.25 0 0 1-2.25-2.25c0-1.152.26-2.243.723-3.218.266-.558-.107-1.282-.725-1.282H3.622c-1.026 0-1.945-.694-2.054-1.715A12.137 12.137 0 0 1 1.5 12c0-2.848.992-5.464 2.649-7.521C4.537 3.997 5.136 3.75 5.754 3.75h4.541c.483 0 .964.078 1.423.23l3.114 1.04c.46.152.94.23 1.423.23Z"/><path d="M21.669 13.023c.536-1.362.831-2.845.831-4.398 0-1.22-.182-2.398-.52-3.507-.26-.85-1.084-1.368-1.973-1.368H19.1c-.445 0-.72.498-.523.898.591 1.2.924 2.55.924 3.977a8.958 8.958 0 0 1-1.302 4.666c-.245.403.028.959.5.959h1.053c.832 0 1.612-.453 1.918-1.227Z"/></svg>';
4060 - }
4061 -
4062 - function buildPromptHtml(botId) {
4063 - var styleAttr = botBubbleStyleAttr(botId);
4064 - return ''
4065 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4066 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
4067 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
4068 - + '<div class="mxchat-rating-actions">'
4069 - + '<span class="mxchat-rating-buttons">'
4070 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
4071 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
4072 - + '</span>'
4073 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
4074 - + '</div>'
4075 - + '</div>'
4076 - + '</div>';
4077 - }
4078 -
4079 - function buildFeedbackHtml(botId, rating) {
4080 - var styleAttr = botBubbleStyleAttr(botId);
4081 - return ''
4082 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4083 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
4084 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
4085 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
4086 - + '<div class="mxchat-rating-feedback-actions">'
4087 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
4088 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
4089 - + '</div>'
4090 - + '</div>'
4091 - + '</div>';
4092 - }
4093 -
4094 - function buildSavedHtml(botId) {
4095 - var styleAttr = botBubbleStyleAttr(botId);
4096 - return ''
4097 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4098 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
4099 - + '</div>';
4100 - }
4101 -
4102 - function getChatBoxByBotId(botId) {
4103 - var $byId = $('#chat-box-' + botId);
4104 - if ($byId.length) return $byId.first();
4105 - return $('.chat-box').first();
4106 - }
4107 -
4108 - function scrollChatBoxToBottom($chatBox) {
4109 - if (!$chatBox || !$chatBox.length) return;
4110 - $chatBox.scrollTop($chatBox[0].scrollHeight);
4111 - }
4112 -
4113 - function showPrompt(botId) {
4114 - var s = getState(botId);
4115 - if (s.promptShown || s.dismissed) return;
4116 - var sessionId = getSessionId(botId);
4117 - if (!sessionId) return;
4118 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4119 - var $chatBox = getChatBoxByBotId(botId);
4120 - if (!$chatBox.length) return;
4121 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
4122 - $chatBox.append(buildPromptHtml(botId));
4123 - syncRatingBubbleColors(botId);
4124 - s.promptShown = true;
4125 - scrollChatBoxToBottom($chatBox);
4126 - }
4127 -
4128 - function submitRating(botId, rating, feedback) {
4129 - var sessionId = getSessionId(botId);
4130 - if (!sessionId) return;
4131 - $.post(mxchatChat.ajax_url, {
4132 - action: 'mxchat_save_rating',
4133 - session_id: sessionId,
4134 - bot_id: botId,
4135 - rating: rating,
4136 - feedback: feedback || ''
4137 - });
4138 - markRated(sessionId);
4139 - }
4140 -
4141 - function onBotReply(botId) {
4142 - var s = getState(botId);
4143 - s.botReplies += 1;
4144 - if (s.promptShown || s.dismissed) return;
4145 - var sessionId = getSessionId(botId);
4146 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4147 - if (s.botReplies < MIN_BOT_REPLIES) return;
4148 - if (s.idleTimer) clearTimeout(s.idleTimer);
4149 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4150 - }
4151 -
4152 - function onUserMessage(botId) {
4153 - var s = getState(botId);
4154 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4155 - }
4156 -
4157 - function botIdFromChatBox(el) {
4158 - var id = el && el.id ? el.id : '';
4159 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4160 - }
4161 -
4162 - function setupObserver(chatBox) {
4163 - var botId = botIdFromChatBox(chatBox);
4164 - try {
4165 - var observer = new MutationObserver(function(mutations) {
4166 - mutations.forEach(function(m) {
4167 - for (var i = 0; i < m.addedNodes.length; i++) {
4168 - var node = m.addedNodes[i];
4169 - if (!node || node.nodeType !== 1) continue;
4170 - var $n = $(node);
4171 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4172 - if ($n.hasClass('bot-message')) onBotReply(botId); // count at insert time — streaming providers append with .temporary-message first, then remove later (childList observer can't see attr changes)
4173 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4174 - }
4175 - });
4176 - });
4177 - observer.observe(chatBox, { childList: true });
4178 - } catch (e) { /* noop */ }
4179 - }
4180 -
4181 - $('.chat-box').each(function() { setupObserver(this); });
4182 -
4183 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4184 - e.preventDefault();
4185 - var $btn = $(this);
4186 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4187 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4188 - var botId = $prompt.data('bot-id') || 'default';
4189 - var rating = parseInt($btn.attr('data-rating'), 10);
4190 - if (rating !== 1 && rating !== -1) return;
4191 - submitRating(botId, rating, '');
4192 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4193 - syncRatingBubbleColors(botId);
4194 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4195 - });
4196 -
4197 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4198 - e.preventDefault();
4199 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4200 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4201 - var botId = $prompt.data('bot-id') || 'default';
4202 - var s = getState(botId);
4203 - s.dismissed = true;
4204 - markRated(getSessionId(botId));
4205 - ($wrap.length ? $wrap : $prompt).remove();
4206 - });
4207 -
4208 - function closeFeedback($fb) {
4209 - var botId = $fb.data('bot-id') || 'default';
4210 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4211 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4212 - syncRatingBubbleColors(botId);
4213 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4214 - }
4215 -
4216 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4217 - e.preventDefault();
4218 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4219 - });
4220 -
4221 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4222 - e.preventDefault();
4223 - var $fb = $(this).closest('.mxchat-rating-feedback');
4224 - var botId = $fb.data('bot-id') || 'default';
4225 - var rating = parseInt($fb.attr('data-rating'), 10);
4226 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4227 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4228 - if (text !== '') {
4229 - submitRating(botId, rating, text);
4230 - }
4231 - closeFeedback($fb);
4232 - });
4233 3271 });
4234 3272