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 +208 -1029 3.2.63.0.9 View file →
@@ -1,41 +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. The state machine below
5 - // queues callbacks so a chat-send that fires while the refresh AJAX is still
6 - // in flight waits for the fresh nonce instead of racing it with the stale
7 - // cached value (which would 403 as "Access denied" on the first message).
8 - var nonceRefreshState = 'idle'; // 'idle' | 'pending' | 'done'
9 - var nonceRefreshCallbacks = [];
10 - function refreshNonceIfNeeded(callback) {
11 - if (typeof mxchatChat === 'undefined' || !mxchatChat.ajax_url) {
12 - if (callback) callback();
13 - return;
14 - }
15 - if (nonceRefreshState === 'done') {
16 - if (callback) callback();
17 - return;
18 - }
19 - if (callback) nonceRefreshCallbacks.push(callback);
20 - if (nonceRefreshState === 'pending') {
21 - return;
22 - }
23 - nonceRefreshState = 'pending';
24 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' })
25 - .done(function(res) {
26 - if (res && res.success && res.data && res.data.nonce) {
27 - mxchatChat.nonce = res.data.nonce;
28 - }
29 - })
30 - .always(function() {
31 - nonceRefreshState = 'done';
32 - var pending = nonceRefreshCallbacks;
33 - nonceRefreshCallbacks = [];
34 - pending.forEach(function(cb) { try { cb(); } catch (e) {} });
35 - });
36 - }
37 -
38 3 // ====================================
39 4 // MULTI-INSTANCE MANAGEMENT SYSTEM
40 5 // ====================================
41 6
@@ -51,9 +16,9 @@
51 16 var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
52 17
53 18 this.instances[botId] = {
54 19 botId: botId,
55 - sessionId: null,
20 + sessionId: this.getChatSession(botId),
56 21 lastSeenMessageId: '',
57 22 notificationCheckInterval: null,
58 23 pollingInterval: null,
59 24 processedMessageIds: new Set(),
@@ -78,77 +43,23 @@
78 43 return Object.keys(this.instances);
79 44 },
80 45
81 46 // Session management per bot
82 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
83 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
84 47 getChatSession: function(botId) {
85 48 var cookieName = 'mxchat_session_id_' + botId;
86 - var storageKey = 'mxchat_session_id_' + botId;
87 49 var sessionId = getCookie(cookieName);
88 50
89 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
90 51 if (!sessionId) {
91 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
52 + sessionId = generateSessionId();
53 + this.setChatSession(botId, sessionId);
92 54 }
93 55
94 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
95 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
96 - // storage). Without this, ensureSession() can generate and store an ID that
97 - // getChatSession() then can't read back, causing null session_ids on send.
98 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
99 - sessionId = this.instances[botId].sessionId;
100 - }
101 -
102 - // Guard against stored sentinel values that indicate earlier broken writes.
103 - if (sessionId === 'null' || sessionId === 'undefined') {
104 - sessionId = null;
105 - }
106 -
107 - // Re-sync cookie from localStorage if cookie was lost
108 - if (sessionId && !getCookie(cookieName)) {
109 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
110 - }
111 -
112 - return sessionId || null;
56 + return sessionId;
113 57 },
114 58
115 - // Lazy session initializer — called on first user interaction
116 - ensureSession: function(botId) {
117 - botId = botId || 'default';
118 - var instance = this.instances[botId] || this.init(botId);
119 -
120 - if (instance.sessionId) {
121 - return instance.sessionId;
122 - }
123 -
124 - // Check for existing session from cookie or localStorage
125 - var existingSession = this.getChatSession(botId);
126 -
127 - if (existingSession) {
128 - instance.sessionId = existingSession;
129 - } else {
130 - // Brand new session
131 - var newId = generateSessionId();
132 - this.setChatSession(botId, newId);
133 - instance.sessionId = newId;
134 - }
135 -
136 - // Now that we have a session, do the deferred work
137 - refreshNonceIfNeeded();
138 - trackOriginatingPage();
139 -
140 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
141 - // so we do NOT call it here to avoid a race condition.
142 -
143 - return instance.sessionId;
144 - },
145 -
146 59 setChatSession: function(botId, sessionId) {
147 60 var cookieName = 'mxchat_session_id_' + botId;
148 - var storageKey = 'mxchat_session_id_' + botId;
149 61 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
150 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
151 62 if (this.instances[botId]) {
152 63 this.instances[botId].sessionId = sessionId;
153 64 }
154 65 },
@@ -153,10 +64,8 @@
153 64 }
154 65 },
155 66
156 67 resetChatSession: function(botId) {
157 - // Clear old session from localStorage before setting new one
158 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
159 68 var newSessionId = generateSessionId();
160 69 this.setChatSession(botId, newSessionId);
161 70 var $chatBox = getElement(botId, 'chat-box');
162 71 if ($chatBox.length) {
@@ -165,20 +74,8 @@
165 74 if (this.instances[botId]) {
166 75 this.instances[botId].chatHistoryLoaded = false;
167 76 this.instances[botId].processedMessageIds = new Set();
168 77 }
169 - },
170 -
171 - // Silent reset — new session ID without clearing the chat UI
172 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
173 - silentResetSession: function(botId) {
174 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
175 - var newSessionId = generateSessionId();
176 - this.setChatSession(botId, newSessionId);
177 - if (this.instances[botId]) {
178 - this.instances[botId].sessionId = newSessionId;
179 - }
180 - return newSessionId;
181 78 }
182 79 };
183 80
184 81 // ====================================
@@ -483,9 +380,8 @@
483 380
484 381 // Update your existing sendMessage function
485 382 function sendMessage(botId) {
486 383 botId = botId || 'default';
487 - MxChatInstances.ensureSession(botId);
488 384 var $chatInput = getElement(botId, 'chat-input');
489 385 var message = $chatInput.val();
490 386
491 387 // ADD PROMPT HOOK HERE
@@ -493,14 +389,10 @@
493 389 message = customMxChatFilter(message, "prompt");
494 390 }
495 391
496 392 if (message) {
497 - // Don't disable input in live agent mode - let users chat freely
498 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
499 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
500 - if (!isAgentMode) {
501 - disableChatInput(botId);
502 - }
393 + // Disable input while waiting for response
394 + disableChatInput(botId);
503 395
504 396 appendMessage("user", message, '', [], false, botId);
505 397 $chatInput.val('');
506 398 $chatInput.css('height', 'auto');
@@ -528,9 +420,8 @@
528 420
529 421 // Update your existing sendMessageToChatbot function
530 422 function sendMessageToChatbot(message, botId) {
531 423 botId = botId || 'default';
532 - MxChatInstances.ensureSession(botId);
533 424
534 425 // ADD PROMPT HOOK HERE
535 426 if (typeof customMxChatFilter === 'function') {
536 427 message = customMxChatFilter(message, "prompt");
@@ -535,14 +426,10 @@
535 426 if (typeof customMxChatFilter === 'function') {
536 427 message = customMxChatFilter(message, "prompt");
537 428 }
538 429
539 - // Don't disable input in live agent mode - let users chat freely
540 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
541 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
542 - if (!isAgentMode) {
543 - disableChatInput(botId);
544 - }
430 + // Disable input while waiting for response
431 + disableChatInput(botId);
545 432
546 433 var sessionId = getChatSession(botId);
547 434
548 435 if (hasQuickQuestions(botId)) {
@@ -630,28 +517,13 @@
630 517
631 518 // Get instance for session start timestamp (used when persistence is OFF)
632 519 var instance = MxChatInstances.get(botId);
633 520
634 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
635 - // and returns the guaranteed-present session id from the in-memory instance even when
636 - // cookie/localStorage writes are silently blocked by the browser.
637 - var sessionId = MxChatInstances.ensureSession(botId);
638 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
639 - // Last-resort generation to ensure we never POST a null marker.
640 - sessionId = generateSessionId();
641 - MxChatInstances.setChatSession(botId, sessionId);
642 - }
643 -
644 - // Wait for the page-cache nonce refresh to complete before firing the
645 - // chat-send AJAX. On cached pages the inline mxchatChat.nonce is stale
646 - // until refreshNonceIfNeeded() returns; constructing ajaxData inside the
647 - // callback guarantees we read the fresh value. See plan-c5457f.
648 - refreshNonceIfNeeded(function() {
649 521 // Prepare AJAX data
650 522 const ajaxData = {
651 523 action: 'mxchat_handle_chat_request',
652 524 message: message,
653 - session_id: sessionId,
525 + session_id: getChatSession(botId),
654 526 nonce: mxchatChat.nonce,
655 527 current_page_url: window.location.href,
656 528 current_page_title: document.title,
657 529 bot_id: botId,
@@ -657,14 +529,14 @@
657 529 bot_id: botId,
658 530 // Pass session start timestamp so AI context matches what user sees
659 531 session_start_timestamp: instance.sessionStartTimestamp || 0
660 532 };
661 -
533 +
662 534 // Add page context if available
663 535 if (pageContext) {
664 536 ajaxData.page_context = JSON.stringify(pageContext);
665 537 }
666 -
538 +
667 539 // CHECK FOR VISION FLAGS AND ADD THEM
668 540 if (window.mxchatVisionProcessed) {
669 541 ajaxData.vision_processed = true;
670 542 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -673,9 +545,9 @@
673 545 window.mxchatVisionProcessed = false;
674 546 window.mxchatOriginalMessage = null;
675 547 window.mxchatVisionImagesCount = 0;
676 548 }
677 -
549 +
678 550 $.ajax({
679 551 url: mxchatChat.ajax_url,
680 552 type: 'POST',
681 553 dataType: 'json',
@@ -713,16 +585,23 @@
713 585 errorMessage = "An error occurred. Please try again or contact support.";
714 586 }
715 587
716 588 // Handle session reset action (IP changed, session expired, etc.)
717 - // Silent reset — keep chat UI intact, just get a new session and retry
718 589 if (response.data && response.data.action === 'reset_session') {
719 - MxChatInstances.silentResetSession(botId);
720 - // 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
721 595 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
722 596 if (originalMessage) {
723 597 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
724 - 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';
725 604 if (shouldUseStreaming(currentModel)) {
726 605 callMxChatStream(originalMessage, function(response) {
727 606 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
728 607 }, botId);
@@ -779,11 +658,9 @@
779 658 }
780 659
781 660 // Check for live agent response
782 661 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
783 - removeThinkingDots(botId);
784 662 updateChatModeIndicator('agent', botId);
785 - enableChatInput(botId);
786 663 return;
787 664 }
788 665
789 666 // Handle the message and show notification if chat is hidden
@@ -816,13 +693,9 @@
816 693 $badge.show();
817 694 }
818 695 }
819 696 } else {
820 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
821 - if (response.vectorstore_error) {
822 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
823 - }
824 - replaceLastMessage("bot", emptyMsg, '', [], botId);
697 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
825 698 }
826 699
827 700 if (response.message_id) {
828 701 var instance = MxChatInstances.get(botId);
@@ -864,9 +737,8 @@
864 737
865 738 replaceLastMessage("bot", errorMessage, '', [], botId);
866 739 }
867 740 });
868 - }); // refreshNonceIfNeeded
869 741 }
870 742
871 743 function callMxChatStream(message, callback, botId) {
872 744 botId = botId || getMxChatBotId();
@@ -885,25 +757,12 @@
885 757
886 758 // Get instance for session start timestamp (used when persistence is OFF)
887 759 var instance = MxChatInstances.get(botId);
888 760
889 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
890 - // non-string value via String(), so passing `null` would POST the literal string "null"
891 - // and land in the transcripts table as a ghost session. ensureSession() always returns
892 - // a real string even when cookies/localStorage are blocked.
893 - var streamSessionId = MxChatInstances.ensureSession(botId);
894 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
895 - streamSessionId = generateSessionId();
896 - MxChatInstances.setChatSession(botId, streamSessionId);
897 - }
898 -
899 - // Wait for the page-cache nonce refresh before constructing formData (which
900 - // captures mxchatChat.nonce by value). Mirrors callMxChat's wrapping. See plan-c5457f.
901 - refreshNonceIfNeeded(function() {
902 761 const formData = new FormData();
903 762 formData.append('action', 'mxchat_stream_chat');
904 763 formData.append('message', message);
905 - formData.append('session_id', streamSessionId);
764 + formData.append('session_id', getChatSession(botId));
906 765 formData.append('nonce', mxchatChat.nonce);
907 766 formData.append('current_page_url', window.location.href);
908 767 formData.append('current_page_title', document.title);
909 768 formData.append('bot_id', botId);
@@ -1003,16 +862,8 @@
1003 862
1004 863 // Re-enable chat input when stream ends with content
1005 864 enableChatInput(botId);
1006 865
1007 - // Scroll the user's last message to the top now that the
1008 - // bot's full reply has rendered (gives max reading room).
1009 - var $chatBoxDone = getElement(botId, 'chat-box');
1010 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
1011 - if ($lastUserMsgDone.length) {
1012 - scrollElementToTop($lastUserMsgDone, botId);
1013 - }
1014 -
1015 866 if (callback) {
1016 867 callback(accumulatedContent);
1017 868 }
1018 869 return;
@@ -1035,16 +886,8 @@
1035 886
1036 887 // Re-enable chat input after streaming completes
1037 888 enableChatInput(botId);
1038 889
1039 - // Scroll the user's last message to the top now
1040 - // that the bot's full reply has rendered.
1041 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1042 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1043 - if ($lastUserMsgStreamDone.length) {
1044 - scrollElementToTop($lastUserMsgStreamDone, botId);
1045 - }
1046 -
1047 890 if (callback) {
1048 891 callback(accumulatedContent);
1049 892 }
1050 893 return;
@@ -1123,9 +966,8 @@
1123 966 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1124 967 callMxChat(message, callback, botId);
1125 968 }
1126 969 });
1127 - }); // refreshNonceIfNeeded
1128 970 }
1129 971
1130 972 // Helper function to handle non-streaming responses
1131 973 function handleNonStreamResponse(data, callback, botId) {
@@ -1164,16 +1006,21 @@
1164 1006 errorMessage = "An error occurred. Please try again or contact support.";
1165 1007 }
1166 1008
1167 1009 // Handle session reset action (IP changed, session expired, etc.)
1168 - // Silent reset — keep chat UI intact, just get a new session and retry
1169 1010 if (data.data && data.data.action === 'reset_session') {
1170 - MxChatInstances.silentResetSession(botId);
1171 - // 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
1172 1014 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1173 1015 if (originalMessage) {
1174 1016 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1175 - 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';
1176 1023 if (shouldUseStreaming(currentModel)) {
1177 1024 callMxChatStream(originalMessage, callback, botId);
1178 1025 } else {
1179 1026 callMxChat(originalMessage, callback, botId);
@@ -1195,22 +1042,8 @@
1195 1042 }
1196 1043 return; // Exit early for errors
1197 1044 }
1198 1045
1199 - // Check for live agent response
1200 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1201 - removeThinkingDots(botId);
1202 - // Also remove any leftover bot-message that lost its temporary-message class
1203 - var $chatBox = getElement(botId, 'chat-box');
1204 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1205 - updateChatModeIndicator('agent', botId);
1206 - enableChatInput(botId);
1207 - if (callback) {
1208 - callback('');
1209 - }
1210 - return;
1211 - }
1212 -
1213 1046 // Handle different response formats
1214 1047 if (data.text || data.html || data.message) {
1215 1048
1216 1049 // Apply response hooks
@@ -1255,15 +1088,19 @@
1255 1088 }
1256 1089
1257 1090 // Enhanced updateChatModeIndicator function for immediate DOM updates
1258 1091 function updateChatModeIndicator(mode, botId) {
1092 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1259 1093 botId = botId || 'default';
1260 1094 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1095 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1261 1096 if (indicator) {
1262 1097 const oldText = indicator.textContent;
1098 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1263 1099
1264 1100 if (mode === 'agent') {
1265 1101 indicator.textContent = 'Live Agent';
1102 + console.log('[MxChat] Mode is agent, calling startPolling...');
1266 1103 startPolling(botId);
1267 1104 } else {
1268 1105 // Everything else is AI mode
1269 1106 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1334,12 +1171,9 @@
1334 1171 // Update the event handlers to use the correct function names (using event delegation)
1335 1172 // Use class-based selectors for multi-instance support
1336 1173 $(document).on('click', '.send-button', function() {
1337 1174 var botId = getBotIdFromElement(this);
1338 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1339 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1340 - disableChatInput(botId);
1341 - }
1175 + disableChatInput(botId);
1342 1176 sendMessage(botId);
1343 1177 });
1344 1178
1345 1179 // Override enter key handler (using event delegation)
@@ -1346,237 +1180,14 @@
1346 1180 $(document).on('keypress', '.chat-input', function(e) {
1347 1181 if (e.which == 13 && !e.shiftKey) {
1348 1182 e.preventDefault();
1349 1183 var botId = getBotIdFromElement(this);
1350 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1351 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1352 - disableChatInput(botId);
1353 - }
1184 + disableChatInput(botId);
1354 1185 sendMessage(botId);
1355 1186 }
1356 1187 });
1357 1188
1358 -// Builds the list of overflow-menu items for a given bot.
1359 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1360 -function mxchatGetHeaderMenuItems(botId) {
1361 - var items = [];
1362 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1363 -
1364 - // The `print_button_*` keys still gate this item for back-compat with
1365 - // existing user options. The action is now a transcript download, not print.
1366 - if (settings.print_button_enabled === 'on') {
1367 - items.push({
1368 - id: 'download-transcript',
1369 - label: settings.print_button_label || 'Download Transcript',
1370 - 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>',
1371 - action: function() {
1372 - mxchatDownloadTranscript(botId);
1373 - }
1374 - });
1375 - }
1376 -
1377 - return items;
1378 -}
1379 -
1380 -// Builds a clean markdown transcript of the current conversation and triggers
1381 -// a file download. Used by the "Download Transcript" menu item.
1382 -function mxchatDownloadTranscript(botId) {
1383 - var $chatBox = getElement(botId, 'chat-box');
1384 - if (!$chatBox || !$chatBox.length) return;
1385 -
1386 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1387 - var headerTitle = settings.print_header_title || 'Chat transcript';
1388 - var now = new Date();
1389 - var stamp = now.toLocaleString();
1390 -
1391 - var lines = [];
1392 - lines.push('# ' + headerTitle);
1393 - lines.push('');
1394 - lines.push('Exported: ' + stamp);
1395 - lines.push('');
1396 - lines.push('---');
1397 - lines.push('');
1398 -
1399 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1400 - var $msg = $(this);
1401 - // Skip thinking placeholders and any in-flight temporary messages.
1402 - if ($msg.find('.thinking-dots').length) return;
1403 - if ($msg.hasClass('temporary-message')) return;
1404 -
1405 - var sender;
1406 - if ($msg.hasClass('user-message')) sender = 'User';
1407 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1408 - else sender = 'AI Agent';
1409 -
1410 - // Strip interactive UI from the cloned message so we get the conversation text.
1411 - var $clone = $msg.clone();
1412 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1413 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1414 - if (!text) return;
1415 -
1416 - lines.push('**' + sender + '**');
1417 - lines.push('');
1418 - lines.push(text);
1419 - lines.push('');
1420 - });
1421 -
1422 - var content = lines.join('\n');
1423 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1424 - var fname = 'mxchat-transcript-' + iso + '.md';
1425 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1426 - var url = URL.createObjectURL(blob);
1427 - var a = document.createElement('a');
1428 - a.href = url;
1429 - a.download = fname;
1430 - a.style.display = 'none';
1431 - document.body.appendChild(a);
1432 - a.click();
1433 - setTimeout(function() {
1434 - if (a.parentNode) a.parentNode.removeChild(a);
1435 - URL.revokeObjectURL(url);
1436 - }, 100);
1437 -}
1438 -
1439 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1440 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1441 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1442 -function mxchatSyncMenuColors(botId, $wrap) {
1443 - if (!$wrap || !$wrap.length) return;
1444 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1445 - if (!$bot.length) return;
1446 - var cs = window.getComputedStyle($bot[0]);
1447 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1448 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1449 - }
1450 - // Bot text color usually lives on a child div, not .bot-message itself.
1451 - var $textChild = $bot.find('[style*="color"]').first();
1452 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1453 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1454 -}
1455 -
1456 -// One-time per-widget init: renders menu items, wires open/close,
1457 -// outside-click, Escape, and arrow-key navigation. If no items, hides the trigger.
1458 -function mxchatInitHeaderMenu(botId) {
1459 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1460 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1461 -
1462 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1463 - var $menu = $wrap.find('.mxchat-header-menu');
1464 - var items = mxchatGetHeaderMenuItems(botId);
1465 -
1466 - // Initial color sync — covers normal page load.
1467 - mxchatSyncMenuColors(botId, $wrap);
1468 -
1469 - if (!items.length) {
1470 - $trigger.hide();
1471 - $menu.hide();
1472 - $wrap.data('mxchatMenuReady', true);
1473 - return;
1474 - }
1475 -
1476 - // Build the menu items.
1477 - $menu.empty();
1478 - items.forEach(function(item, idx) {
1479 - var $btn = $('<button>', {
1480 - type: 'button',
1481 - 'class': 'mxchat-menu-item',
1482 - 'role': 'menuitem',
1483 - 'tabindex': '-1',
1484 - 'data-menu-id': item.id,
1485 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1486 - '<span class="mxchat-menu-item-label"></span>'
1487 - });
1488 - $btn.find('.mxchat-menu-item-label').text(item.label);
1489 - $btn.on('click', function(e) {
1490 - e.preventDefault();
1491 - e.stopPropagation();
1492 - closeMenu();
1493 - try { item.action(); } catch (err) { /* no-op */ }
1494 - });
1495 - $menu.append($btn);
1496 - });
1497 -
1498 - function openMenu() {
1499 - // Re-sync each open in case the active theme changed since init.
1500 - mxchatSyncMenuColors(botId, $wrap);
1501 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1502 - $trigger.attr('aria-expanded', 'true');
1503 - // Focus the first item for keyboard users
1504 - setTimeout(function() {
1505 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1506 - }, 0);
1507 - }
1508 - function closeMenu(returnFocus) {
1509 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1510 - $trigger.attr('aria-expanded', 'false');
1511 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1512 - if (returnFocus) $trigger.trigger('focus');
1513 - }
1514 -
1515 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1516 - // click-to-collapse handler does not fire.
1517 - $trigger.on('click', function(e) {
1518 - e.preventDefault();
1519 - e.stopPropagation();
1520 - if ($menu.hasClass('is-open')) closeMenu();
1521 - else openMenu();
1522 - });
1523 -
1524 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1525 - $menu.on('click', function(e) {
1526 - e.stopPropagation();
1527 - });
1528 -
1529 - // Outside click closes the menu.
1530 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1531 - if (!$menu.hasClass('is-open')) return;
1532 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1533 - closeMenu();
1534 - });
1535 -
1536 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1537 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1538 - var $items = $menu.find('.mxchat-menu-item');
1539 - var idx = $items.index(this);
1540 - if (e.key === 'Escape') {
1541 - e.preventDefault();
1542 - closeMenu(true);
1543 - } else if (e.key === 'ArrowDown') {
1544 - e.preventDefault();
1545 - var $next = $items.eq((idx + 1) % $items.length);
1546 - $items.attr('tabindex', '-1');
1547 - $next.attr('tabindex', '0').trigger('focus');
1548 - } else if (e.key === 'ArrowUp') {
1549 - e.preventDefault();
1550 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1551 - $items.attr('tabindex', '-1');
1552 - $prev.attr('tabindex', '0').trigger('focus');
1553 - } else if (e.key === 'Enter' || e.key === ' ') {
1554 - e.preventDefault();
1555 - $(this).trigger('click');
1556 - }
1557 - });
1558 - $trigger.on('keydown', function(e) {
1559 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1560 - e.preventDefault();
1561 - closeMenu(true);
1562 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1563 - e.preventDefault();
1564 - openMenu();
1565 - }
1566 - });
1567 -
1568 - $wrap.data('mxchatMenuReady', true);
1569 -}
1570 -
1571 -// Initialize header menus for every rendered widget on DOM ready.
1572 -$(function() {
1573 - $('.mxchat-header-menu-wrap').each(function() {
1574 - var botId = $(this).data('bot-id');
1575 - if (botId) mxchatInitHeaderMenu(botId);
1576 - });
1577 -});
1578 -
1189 +
1579 1190 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1580 1191 try {
1581 1192 // Determine styles based on sender type
1582 1193 let messageClass, bgColor, fontColor;
@@ -1614,12 +1225,17 @@
1614 1225 'margin-bottom': '1em'
1615 1226 });
1616 1227 }
1617 1228
1618 - // Process the message content - always run linkify to convert markdown
1619 - // links and format text. linkify() handles existing HTML safely via
1620 - // negative lookaheads that skip URLs already inside <a> tags.
1621 - 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 + }
1622 1238
1623 1239 // Add images if provided
1624 1240 if (images && images.length > 0) {
1625 1241 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1668,12 +1284,8 @@
1668 1284 if (lastUserMessage.length) {
1669 1285 scrollElementToTop(lastUserMessage, botId);
1670 1286 }
1671 1287 }
1672 -
1673 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1674 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1675 - }
1676 1288 });
1677 1289
1678 1290 if (messageText.id) {
1679 1291 var instance = MxChatInstances.get(botId);
@@ -1758,12 +1370,26 @@
1758 1370 bgColor = botMessageBgColor;
1759 1371 fontColor = botMessageFontColor;
1760 1372 }
1761 1373
1762 - // Always run linkify to convert markdown links and format text.
1763 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1764 - // to avoid double-processing URLs that are already inside <a> tags).
1765 - 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 + }
1766 1392
1767 1393 if (responseHtml) {
1768 1394 // Only add line breaks if there's actual text content before the HTML
1769 1395 if (fullMessage && fullMessage.trim()) {
@@ -1820,12 +1446,8 @@
1820 1446 }
1821 1447
1822 1448 // Re-enable chat input after response is displayed
1823 1449 enableChatInput(botId);
1824 -
1825 - if (sender === "bot" || sender === "agent") {
1826 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1827 - }
1828 1450 } else {
1829 1451 appendMessage(sender, responseText, responseHtml, images, false, botId);
1830 1452 // Re-enable chat input after response is displayed
1831 1453 enableChatInput(botId);
@@ -1834,15 +1456,8 @@
1834 1456
1835 1457
1836 1458 function appendThinkingMessage(botId) {
1837 1459 botId = botId || 'default';
1838 -
1839 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
1840 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
1841 - if (indicator && indicator.textContent === 'Live Agent') {
1842 - return;
1843 - }
1844 -
1845 1460 var $chatBox = getElement(botId, 'chat-box');
1846 1461
1847 1462 // Remove any existing thinking dots in this bot's chat first
1848 1463 $chatBox.find('.thinking-dots').remove();
@@ -1864,9 +1479,9 @@
1864 1479 '</div>' +
1865 1480 '</div>';
1866 1481
1867 1482 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1868 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1483 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
1869 1484 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1870 1485 scrollToBottom(botId);
1871 1486 }
1872 1487
@@ -1872,11 +1487,9 @@
1872 1487
1873 1488 function removeThinkingDots(botId) {
1874 1489 botId = botId || 'default';
1875 1490 var $chatBox = getElement(botId, 'chat-box');
1876 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1877 1491 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1878 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1879 1492 }
1880 1493
1881 1494 // ====================================
1882 1495 // TEXT FORMATTING & PROCESSING
@@ -1910,12 +1523,9 @@
1910 1523 processedText = formatTextStyling(processedText);
1911 1524
1912 1525 // Process code blocks BEFORE processing links
1913 1526 processedText = formatCodeBlocks(processedText);
1914 -
1915 - // Process markdown tables BEFORE converting newlines to paragraphs
1916 - processedText = formatMarkdownTables(processedText);
1917 -
1527 +
1918 1528 // NOW convert to paragraphs
1919 1529 processedText = convertNewlinesToBreaks(processedText);
1920 1530
1921 1531 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1928,63 +1538,37 @@
1928 1538 // Return as a proper link without the brackets
1929 1539 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1930 1540 });
1931 1541
1932 - // Process markdown links: [text](url) and [](url)
1933 - // Uses balanced parenthesis matching to handle URLs containing parens
1934 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
1935 - processedText = (function(input) {
1936 - var result = '';
1937 - var i = 0;
1938 - while (i < input.length) {
1939 - // Look for [ at current position
1940 - if (input[i] === '[') {
1941 - // Find closing ]
1942 - var closeBracket = input.indexOf(']', i + 1);
1943 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
1944 - result += input[i];
1945 - i++;
1946 - continue;
1947 - }
1948 - var linkText = input.substring(i + 1, closeBracket);
1949 - // Check if URL starts with http
1950 - var urlStart = closeBracket + 2;
1951 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
1952 - result += input[i];
1953 - i++;
1954 - continue;
1955 - }
1956 - // Find balanced closing paren
1957 - var depth = 1;
1958 - var j = urlStart;
1959 - while (j < input.length && depth > 0) {
1960 - if (input[j] === '(') depth++;
1961 - else if (input[j] === ')') depth--;
1962 - if (depth > 0) j++;
1963 - }
1964 - if (depth !== 0) {
1965 - result += input[i];
1966 - i++;
1967 - continue;
1968 - }
1969 - var url = input.substring(urlStart, j);
1970 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
1971 - var encodedUrl = safeEncodeUrl(cleanUrl);
1972 - if (!linkText || !linkText.trim()) {
1973 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
1974 - } else {
1975 - var safeText = sanitizeUserInput(linkText);
1976 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
1977 - }
1978 - i = j + 1; // Skip past the closing )
1979 - } else {
1980 - result += input[i];
1981 - i++;
1982 - }
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>`;
1983 1552 }
1984 - return result;
1985 - })(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 + });
1986 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 +
1987 1571 // Process phone numbers: [text](tel:number)
1988 1572 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1989 1573 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1990 1574 const safePhone = safeEncodeUrl(phone);
@@ -2136,78 +1720,9 @@
2136 1720 });
2137 1721
2138 1722 return text;
2139 1723 }
2140 -
2141 - function formatMarkdownTables(text) {
2142 - var lines = text.split('\n');
2143 - var result = [];
2144 - var i = 0;
2145 -
2146 - while (i < lines.length) {
2147 - // Check for a table: current line has pipes AND next line is a separator row
2148 - if (i + 1 < lines.length &&
2149 - lines[i].indexOf('|') !== -1 &&
2150 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2151 -
2152 - var tableLines = [];
2153 - var headerLine = lines[i];
2154 - var separatorLine = lines[i + 1];
2155 - tableLines.push(headerLine);
2156 - tableLines.push(separatorLine);
2157 -
2158 - // Collect remaining table rows
2159 - var j = i + 2;
2160 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2161 - tableLines.push(lines[j]);
2162 - j++;
2163 - }
2164 -
2165 - // Parse alignment from separator row
2166 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2167 - var alignments = sepCells.map(function(cell) {
2168 - var trimmed = cell.trim();
2169 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2170 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2171 - return 'left';
2172 - });
2173 -
2174 - // Build HTML table
2175 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2176 -
2177 - // Header row
2178 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2179 - html += '<thead><tr>';
2180 - headerCells.forEach(function(cell, idx) {
2181 - var align = alignments[idx] || 'left';
2182 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2183 - });
2184 - html += '</tr></thead>';
2185 -
2186 - // Body rows
2187 - html += '<tbody>';
2188 - for (var r = 2; r < tableLines.length; r++) {
2189 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2190 - html += '<tr>';
2191 - rowCells.forEach(function(cell, idx) {
2192 - var align = alignments[idx] || 'left';
2193 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2194 - });
2195 - html += '</tr>';
2196 - }
2197 - html += '</tbody></table></div>';
2198 -
2199 - result.push(html);
2200 - i = j;
2201 - } else {
2202 - result.push(lines[i]);
2203 - i++;
2204 - }
2205 - }
2206 -
2207 - return result.join('\n');
2208 - }
2209 -
1724 +
2210 1725 function sanitizeUserInput(text) {
2211 1726 const div = document.createElement('div');
2212 1727 div.textContent = text;
2213 1728 return div.innerHTML;
@@ -2278,14 +1793,13 @@
2278 1793 requestAnimationFrame(smoothScroll);
2279 1794 }
2280 1795 }
2281 1796
2282 - function scrollElementToTop(element, botId, topOffset) {
1797 + function scrollElementToTop(element, botId) {
2283 1798 botId = botId || 'default';
2284 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2285 1799 var chatBox = getElement(botId, 'chat-box');
2286 1800 var elementTop = element.position().top + chatBox.scrollTop();
2287 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1801 + chatBox.animate({ scrollTop: elementTop }, 500);
2288 1802 }
2289 1803
2290 1804 function showChatWidget(botId) {
2291 1805 botId = botId || 'default';
@@ -2429,12 +1943,15 @@
2429 1943 // LIVE AGENT FUNCTIONALITY
2430 1944 // ====================================
2431 1945
2432 1946 function startPolling(botId) {
1947 + console.log('[MxChat] startPolling called for botId:', botId);
2433 1948 botId = botId || 'default';
2434 1949 var instance = MxChatInstances.get(botId);
2435 1950 // Clear any existing interval first
2436 1951 stopPolling(botId);
1952 + // Start new polling interval
1953 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2437 1954 instance.pollingInterval = setInterval(function() {
2438 1955 checkForAgentMessages(botId);
2439 1956 }, 5000);
2440 1957 }
@@ -2439,17 +1956,20 @@
2439 1956 }, 5000);
2440 1957 }
2441 1958
2442 1959 function stopPolling(botId) {
1960 + console.log('[MxChat] stopPolling called for botId:', botId);
2443 1961 botId = botId || 'default';
2444 1962 var instance = MxChatInstances.get(botId);
2445 1963 if (instance.pollingInterval) {
2446 1964 clearInterval(instance.pollingInterval);
2447 1965 instance.pollingInterval = null;
1966 + console.log('[MxChat] Polling stopped for botId:', botId);
2448 1967 }
2449 1968 }
2450 1969
2451 1970 function checkForAgentMessages(botId) {
1971 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2452 1972 botId = botId || 'default';
2453 1973 var instance = MxChatInstances.get(botId);
2454 1974 const sessionId = getChatSession(botId);
2455 1975 $.ajax({
@@ -2475,12 +1995,8 @@
2475 1995 instance.processedMessageIds.add(message.id);
2476 1996 }
2477 1997 });
2478 1998
2479 - if (hasNewMessage) {
2480 - enableChatInput(botId);
2481 - }
2482 -
2483 1999 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2484 2000 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2485 2001 showNotification(botId);
2486 2002 }
@@ -2486,13 +2002,8 @@
2486 2002 }
2487 2003
2488 2004 scrollToBottom(botId, true);
2489 2005 }
2490 -
2491 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2492 - if (response.success && response.data?.chat_mode) {
2493 - updateChatModeIndicator(response.data.chat_mode, botId);
2494 - }
2495 2006 },
2496 2007 error: function (xhr, status, error) {
2497 2008 // Polling error - silently continue
2498 2009 }
@@ -2502,29 +2013,20 @@
2502 2013 // ====================================
2503 2014 // CHAT HISTORY & PERSISTENCE
2504 2015 // ====================================
2505 2016
2506 -function loadChatHistory(botId, onComplete) {
2017 +function loadChatHistory(botId) {
2507 2018 botId = botId || 'default';
2508 2019 var instance = MxChatInstances.get(botId);
2509 2020
2510 2021 // Prevent duplicate loading
2511 2022 if (instance.chatHistoryLoaded) {
2512 - if (onComplete) onComplete();
2513 2023 return;
2514 2024 }
2515 2025
2516 - // Use getChatSession which returns null if no session exists (does NOT create one)
2517 2026 var sessionId = getChatSession(botId);
2518 2027 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2519 2028
2520 - // No session yet — nothing to load. History will load after first message via ensureSession.
2521 - if (!sessionId) {
2522 - instance.chatHistoryLoaded = true;
2523 - if (onComplete) onComplete();
2524 - return;
2525 - }
2526 -
2527 2029 if (chatPersistenceEnabled && sessionId) {
2528 2030 $.ajax({
2529 2031 url: mxchatChat.ajax_url,
2530 2032 type: 'POST',
@@ -2535,12 +2037,11 @@
2535 2037 },
2536 2038 success: function(response) {
2537 2039 // Handle session reset (IP changed while user was away)
2538 2040 if (response.success === false && response.data && response.data.action === 'reset_session') {
2539 - // Silent reset — new session but don't clear UI
2540 - MxChatInstances.silentResetSession(botId);
2041 + // Silently reset session - user will start fresh
2042 + resetChatSession(botId);
2541 2043 instance.chatHistoryLoaded = true; // Prevent retry loop
2542 - if (onComplete) onComplete();
2543 2044 return;
2544 2045 }
2545 2046
2546 2047 // Check if the response indicates success
@@ -2596,19 +2097,9 @@
2596 2097 var content = message.content;
2597 2098 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2598 2099 content = decodeHTMLEntities(content);
2599 2100
2600 - // Skip linkify for messages containing structured HTML
2601 - // (forms, product cards, galleries, etc.) to avoid
2602 - // markdown formatting corrupting HTML attributes
2603 - // (e.g. underscores in name="field_name" becoming <em> tags)
2604 - if (content.includes("mxchat-product-card") ||
2605 - content.includes("mxchat-image-gallery") ||
2606 - content.includes("mxchat-featured-products") ||
2607 - content.includes("<form") ||
2608 - content.includes("<input") ||
2609 - content.includes("<select") ||
2610 - content.includes("<textarea")) {
2101 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2611 2102 messageElement.html(content);
2612 2103 } else {
2613 2104 var formattedContent = linkify(content);
2614 2105 messageElement.html(formattedContent);
@@ -2648,17 +2139,13 @@
2648 2139 instance.chatHistoryLoaded = true;
2649 2140 }
2650 2141 }
2651 2142 }
2652 - if (onComplete) onComplete();
2653 2143 },
2654 2144 error: function(xhr, status, error) {
2655 2145 // Error loading chat history - silently continue
2656 - if (onComplete) onComplete();
2657 2146 }
2658 2147 });
2659 - } else {
2660 - if (onComplete) onComplete();
2661 2148 }
2662 2149 }
2663 2150
2664 2151
@@ -2834,35 +2321,45 @@
2834 2321 // ====================================
2835 2322
2836 2323 function checkPreChatDismissal(botId) {
2837 2324 botId = botId || 'default';
2838 - try {
2839 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2840 - if (dismissedAt) {
2841 - // Re-show after 24 hours
2842 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2843 - 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 {
2844 2336 getElement(botId, 'pre-chat-message').hide();
2845 - return;
2846 2337 }
2847 - // Expired — clear and show again
2848 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2338 + },
2339 + error: function() {
2340 + // Error checking pre-chat dismissal - silently continue
2849 2341 }
2850 - getElement(botId, 'pre-chat-message').fadeIn(250);
2851 - } catch (e) {
2852 - // localStorage unavailable — show the message
2853 - getElement(botId, 'pre-chat-message').fadeIn(250);
2854 - }
2342 + });
2855 2343 }
2856 2344
2857 2345 function handlePreChatDismissal(botId) {
2858 2346 botId = botId || 'default';
2859 2347 getElement(botId, 'pre-chat-message').fadeOut(200);
2860 - try {
2861 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2862 - } catch (e) {
2863 - // localStorage unavailable — dismissal won't persist
2864 - }
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 + });
2865 2362 }
2866 2363
2867 2364
2868 2365 // ====================================
@@ -2929,26 +2426,8 @@
2929 2426 $(this).addClass('hidden');
2930 2427 $badge.hide(); // Hide notification when opening chat
2931 2428 disableScroll();
2932 2429 $preChat.fadeOut(250);
2933 -
2934 - // Load chat history for returning visitors (persistence)
2935 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
2936 - if (chatPersistenceEnabled) {
2937 - MxChatInstances.ensureSession(botId);
2938 - }
2939 -
2940 - // Deferred email check — only on first widget open
2941 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2942 - var instance = MxChatInstances.get(botId);
2943 - if (emailBlocker && !instance.emailCheckDone) {
2944 - instance.emailCheckDone = true;
2945 - resolveEmailState(botId);
2946 - } else if (!emailBlocker) {
2947 - // No email collection — still route through showChatContainerForBot
2948 - // so the loader is shown while chat history loads
2949 - showChatContainerForBot(botId);
2950 - }
2951 2430 } else {
2952 2431 $chatbot.removeClass('visible').addClass('hidden');
2953 2432 $(this).removeClass('hidden');
2954 2433 enableScroll();
@@ -2966,9 +2445,11 @@
2966 2445
2967 2446 $(document).on('click', '.close-pre-chat-message', function(e) {
2968 2447 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2969 2448 var botId = getBotIdFromElement(this);
2970 - handlePreChatDismissal(botId);
2449 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2450 + $(this).remove();
2451 + });
2971 2452 });
2972 2453
2973 2454
2974 2455 // PDF upload button handlers - use class selector
@@ -3009,10 +2490,8 @@
3009 2490 const sendBtn = document.getElementById('send-button');
3010 2491 const originalBtnContent = uploadBtn.innerHTML;
3011 2492
3012 2493 try {
3013 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3014 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3015 2494 const formData = new FormData();
3016 2495 formData.append('action', 'mxchat_upload_pdf');
3017 2496 formData.append('pdf_file', file);
3018 2497 formData.append('session_id', sessionId);
@@ -3076,10 +2555,8 @@
3076 2555 const sendBtn = document.getElementById('send-button');
3077 2556 const originalBtnContent = uploadBtn.innerHTML;
3078 2557
3079 2558 try {
3080 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3081 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3082 2559 const formData = new FormData();
3083 2560 formData.append('action', 'mxchat_upload_word');
3084 2561 formData.append('word_file', file);
3085 2562 formData.append('session_id', sessionId);
@@ -3173,59 +2650,8 @@
3173 2650 });
3174 2651
3175 2652
3176 2653 // ====================================
3177 -// INIT LOADER & CHAT CONTAINER HELPERS
3178 -// ====================================
3179 -// These must be outside the email collection block so they're always available
3180 -// (used by persistence loading even when email collection is off)
3181 -
3182 -function showInitLoader(botId) {
3183 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3184 - if (loader) loader.style.display = 'flex';
3185 -}
3186 -
3187 -function hideInitLoader(botId) {
3188 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3189 - if (loader) loader.style.display = 'none';
3190 -}
3191 -
3192 -function showEmailFormForBot(botId) {
3193 - hideInitLoader(botId);
3194 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3195 - var chatContainer = getElementDOM(botId, 'chat-container');
3196 - if (emailBlocker) emailBlocker.style.display = 'flex';
3197 - if (chatContainer) chatContainer.style.display = 'none';
3198 -}
3199 -
3200 -function showChatContainerForBot(botId) {
3201 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3202 - var chatContainer = getElementDOM(botId, 'chat-container');
3203 - if (emailBlocker) emailBlocker.style.display = 'none';
3204 -
3205 - var instance = MxChatInstances.get(botId);
3206 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3207 -
3208 - // If persistence is on and history hasn't loaded yet, show loader
3209 - // while history loads to prevent flash of empty chat
3210 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3211 - if (chatContainer) chatContainer.style.display = 'none';
3212 - showInitLoader(botId);
3213 - loadChatHistory(botId, function() {
3214 - hideInitLoader(botId);
3215 - if (chatContainer) chatContainer.style.display = 'flex';
3216 - scrollToBottom(botId, true);
3217 - });
3218 - } else {
3219 - hideInitLoader(botId);
3220 - if (chatContainer) chatContainer.style.display = 'flex';
3221 - if (typeof loadChatHistory === 'function') {
3222 - loadChatHistory(botId);
3223 - }
3224 - }
3225 -}
3226 -
3227 -// ====================================
3228 2654 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3229 2655 // ====================================
3230 2656 // Only run email collection setup if it's enabled
3231 2657 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3261,8 +2687,28 @@
3261 2687 `;
3262 2688 document.head.appendChild(style);
3263 2689 }
3264 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 +
3265 2711 function isValidEmailAddress(email) {
3266 2712 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3267 2713 return emailRegex.test(email.trim()) && email.length <= 254;
3268 2714 }
@@ -3384,31 +2830,11 @@
3384 2830 existingErrors.forEach(error => error.remove());
3385 2831 }
3386 2832 }
3387 2833
3388 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3389 - function resolveEmailState(botId) {
3390 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3391 - if (mxchatChat.initial_email_state.show_email_form) {
3392 - showEmailFormForBot(botId);
3393 - } else {
3394 - showChatContainerForBot(botId);
3395 - }
3396 - } else {
3397 - checkSessionAndEmailForBot(botId);
3398 - }
3399 - }
3400 -
3401 2834 function checkSessionAndEmailForBot(botId) {
3402 - const sessionId = MxChatInstances.ensureSession(botId);
2835 + const sessionId = getChatSession(botId);
3403 2836
3404 - // Hide both panels while we check — show loader instead
3405 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3406 - var chatContainer = getElementDOM(botId, 'chat-container');
3407 - if (emailBlocker) emailBlocker.style.display = 'none';
3408 - if (chatContainer) chatContainer.style.display = 'none';
3409 - showInitLoader(botId);
3410 -
3411 2837 fetch(mxchatChat.ajax_url, {
3412 2838 method: 'POST',
3413 2839 headers: {
3414 2840 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3456,9 +2882,9 @@
3456 2882 var emailInput = getElementDOM(botId, 'user-email');
3457 2883 var nameInput = getElementDOM(botId, 'user-name');
3458 2884 var userEmail = emailInput ? emailInput.value.trim() : '';
3459 2885 var userName = nameInput ? nameInput.value.trim() : '';
3460 - var sessionId = MxChatInstances.ensureSession(botId);
2886 + var sessionId = getChatSession(botId);
3461 2887
3462 2888 // Validate email
3463 2889 if (!userEmail) {
3464 2890 showEmailError(botId, 'Please enter your email address.');
@@ -3581,27 +3007,25 @@
3581 3007 }
3582 3008 });
3583 3009
3584 3010 // Initialize email check for all bot instances
3585 - // For floating bots: defer until widget is opened (zero passive AJAX)
3586 - // For embedded bots: check immediately since the form is visible
3587 3011 $('.mxchat-chatbot-wrapper').each(function() {
3588 3012 var botId = $(this).data('bot-id') || 'default';
3589 3013 var emailBlocker = getElementDOM(botId, 'email-blocker');
3590 3014
3015 + // Only check if email blocker exists for this bot
3591 3016 if (emailBlocker) {
3592 - if (isEmbeddedBot(botId)) {
3593 - // Embedded bots are always visible — check now
3594 - 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);
3595 3027 }
3596 - // Floating bots: handled in the widget open handler
3597 - } else if (isEmbeddedBot(botId)) {
3598 - // Embedded bot, no email collection — load history with loader
3599 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3600 - if (chatPersistenceEnabled) {
3601 - MxChatInstances.ensureSession(botId);
3602 - showChatContainerForBot(botId);
3603 - }
3604 3028 }
3605 3029 });
3606 3030 }
3607 3031
@@ -3611,32 +3035,39 @@
3611 3035 var $chatbot = getElement(botId, 'floating-chatbot');
3612 3036 if ($chatbot.hasClass('hidden')) {
3613 3037 $chatbot.removeClass('hidden').addClass('visible');
3614 3038 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3615 - handlePreChatDismissal(botId);
3039 + $(this).fadeOut(250); // Hide pre-chat message
3616 3040 disableScroll(); // Disable scroll when chatbot opens
3041 + }
3042 + });
3617 3043
3618 - // Load chat history for returning visitors (persistence)
3619 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3620 - if (chatPersistenceEnabled) {
3621 - MxChatInstances.ensureSession(botId);
3622 - }
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
3623 3050
3624 - // Deferred email check — only on first widget open
3625 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3626 - var instance = MxChatInstances.get(botId);
3627 - if (emailBlocker && !instance.emailCheckDone) {
3628 - instance.emailCheckDone = true;
3629 - resolveEmailState(botId);
3630 - } else if (!emailBlocker) {
3631 - 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
3632 3065 }
3633 - }
3066 + });
3634 3067 });
3635 3068
3636 - // Legacy duplicate close handler removed — handled by single event delegation above
3637 3069
3638 -
3639 3070 function hasQuickQuestions(botId) {
3640 3071 botId = botId || 'default';
3641 3072 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3642 3073 if (!questionsContainer) return false;
@@ -3772,11 +3203,18 @@
3772 3203 });
3773 3204
3774 3205 // Initialize when document is ready
3775 3206 setFullHeight();
3207 + trackOriginatingPage();
3776 3208
3777 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3778 - // 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 + }
3779 3217
3780 3218 // Initialize chat visibility for all instances
3781 3219 $('.mxchat-chatbot-wrapper').each(function() {
3782 3220 var botId = $(this).data('bot-id') || 'default';
@@ -3829,265 +3267,6 @@
3829 3267 }, 2000);
3830 3268 });
3831 3269 }
3832 3270 }
3833 -});
3834 -
3835 -// ============================================================================
3836 -// SATISFACTION RATING (v3.2.6)
3837 -// ============================================================================
3838 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
3839 -// inactivity following a bot reply. One prompt per session, deduped via
3840 -// localStorage. Disabled site-wide when mxchatChat.satisfaction_rating_enabled
3841 -// is exactly false (default ON).
3842 -jQuery(function($) {
3843 - if (typeof mxchatChat === 'undefined') return;
3844 - if (mxchatChat.satisfaction_rating_enabled === false || mxchatChat.satisfaction_rating_enabled === 'off') return;
3845 -
3846 - // wp_localize_script stringifies ints, so accept both number and numeric string.
3847 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
3848 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
3849 - if (!isFinite(idleSeconds)) idleSeconds = 60;
3850 - if (idleSeconds < 5) idleSeconds = 5;
3851 - if (idleSeconds > 600) idleSeconds = 600;
3852 - var IDLE_MS = idleSeconds * 1000;
3853 - var MIN_BOT_REPLIES = 2;
3854 - var ratingState = {};
3855 -
3856 - function getState(botId) {
3857 - if (!ratingState[botId]) {
3858 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
3859 - }
3860 - return ratingState[botId];
3861 - }
3862 -
3863 - function getSessionId(botId) {
3864 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
3865 - return MxChatInstances.getChatSession(botId);
3866 - }
3867 - return null;
3868 - }
3869 -
3870 - function isAlreadyRated(sessionId) {
3871 - if (!sessionId) return false;
3872 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
3873 - }
3874 -
3875 - function markRated(sessionId) {
3876 - if (!sessionId) return;
3877 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
3878 - }
3879 -
3880 - function esc(s) {
3881 - return String(s == null ? '' : s)
3882 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
3883 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
3884 - }
3885 -
3886 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
3887 - function ratingSkipInlineColors(botId) {
3888 - if (mxchatChat.skip_inline_colors) return true;
3889 - var botAssignments = mxchatChat.bot_theme_assignments || {};
3890 - return botAssignments.hasOwnProperty(botId);
3891 - }
3892 -
3893 - function botBubbleStyleAttr(botId) {
3894 - if (ratingSkipInlineColors(botId)) return '';
3895 - var bg = mxchatChat.bot_message_bg_color;
3896 - var fg = mxchatChat.bot_message_font_color;
3897 - if (!bg && !fg) return '';
3898 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
3899 - }
3900 -
3901 - function copy(key) {
3902 - var c = mxchatChat.satisfaction_rating_copy || {};
3903 - var d = {
3904 - question: 'Was this helpful?',
3905 - helpful: 'Helpful',
3906 - not_helpful: 'Not helpful',
3907 - dismiss: 'Dismiss',
3908 - thanks: 'Thanks! Anything we should improve? (optional)',
3909 - placeholder: 'Tell us what could be better…',
3910 - send: 'Send',
3911 - skip: 'Skip',
3912 - saved: 'Thanks for the feedback.'
3913 - };
3914 - return c[key] || d[key];
3915 - }
3916 -
3917 - function thumbUpSvg() {
3918 - 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>';
3919 - }
3920 - function thumbDownSvg() {
3921 - 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>';
3922 - }
3923 -
3924 - function buildPromptHtml(botId) {
3925 - var styleAttr = botBubbleStyleAttr(botId);
3926 - return ''
3927 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
3928 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
3929 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
3930 - + '<div class="mxchat-rating-actions">'
3931 - + '<span class="mxchat-rating-buttons">'
3932 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
3933 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
3934 - + '</span>'
3935 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
3936 - + '</div>'
3937 - + '</div>'
3938 - + '</div>';
3939 - }
3940 -
3941 - function buildFeedbackHtml(botId, rating) {
3942 - var styleAttr = botBubbleStyleAttr(botId);
3943 - return ''
3944 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
3945 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
3946 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
3947 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
3948 - + '<div class="mxchat-rating-feedback-actions">'
3949 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
3950 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
3951 - + '</div>'
3952 - + '</div>'
3953 - + '</div>';
3954 - }
3955 -
3956 - function buildSavedHtml(botId) {
3957 - var styleAttr = botBubbleStyleAttr(botId);
3958 - return ''
3959 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
3960 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
3961 - + '</div>';
3962 - }
3963 -
3964 - function getChatBoxByBotId(botId) {
3965 - var $byId = $('#chat-box-' + botId);
3966 - if ($byId.length) return $byId.first();
3967 - return $('.chat-box').first();
3968 - }
3969 -
3970 - function scrollChatBoxToBottom($chatBox) {
3971 - if (!$chatBox || !$chatBox.length) return;
3972 - $chatBox.scrollTop($chatBox[0].scrollHeight);
3973 - }
3974 -
3975 - function showPrompt(botId) {
3976 - var s = getState(botId);
3977 - if (s.promptShown || s.dismissed) return;
3978 - var sessionId = getSessionId(botId);
3979 - if (!sessionId) return;
3980 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
3981 - var $chatBox = getChatBoxByBotId(botId);
3982 - if (!$chatBox.length) return;
3983 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
3984 - $chatBox.append(buildPromptHtml(botId));
3985 - s.promptShown = true;
3986 - scrollChatBoxToBottom($chatBox);
3987 - }
3988 -
3989 - function submitRating(botId, rating, feedback) {
3990 - var sessionId = getSessionId(botId);
3991 - if (!sessionId) return;
3992 - $.post(mxchatChat.ajax_url, {
3993 - action: 'mxchat_save_rating',
3994 - session_id: sessionId,
3995 - bot_id: botId,
3996 - rating: rating,
3997 - feedback: feedback || ''
3998 - });
3999 - markRated(sessionId);
4000 - }
4001 -
4002 - function onBotReply(botId) {
4003 - var s = getState(botId);
4004 - s.botReplies += 1;
4005 - if (s.promptShown || s.dismissed) return;
4006 - var sessionId = getSessionId(botId);
4007 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4008 - if (s.botReplies < MIN_BOT_REPLIES) return;
4009 - if (s.idleTimer) clearTimeout(s.idleTimer);
4010 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4011 - }
4012 -
4013 - function onUserMessage(botId) {
4014 - var s = getState(botId);
4015 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4016 - }
4017 -
4018 - function botIdFromChatBox(el) {
4019 - var id = el && el.id ? el.id : '';
4020 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4021 - }
4022 -
4023 - function setupObserver(chatBox) {
4024 - var botId = botIdFromChatBox(chatBox);
4025 - try {
4026 - var observer = new MutationObserver(function(mutations) {
4027 - mutations.forEach(function(m) {
4028 - for (var i = 0; i < m.addedNodes.length; i++) {
4029 - var node = m.addedNodes[i];
4030 - if (!node || node.nodeType !== 1) continue;
4031 - var $n = $(node);
4032 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4033 - 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)
4034 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4035 - }
4036 - });
4037 - });
4038 - observer.observe(chatBox, { childList: true });
4039 - } catch (e) { /* noop */ }
4040 - }
4041 -
4042 - $('.chat-box').each(function() { setupObserver(this); });
4043 -
4044 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4045 - e.preventDefault();
4046 - var $btn = $(this);
4047 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4048 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4049 - var botId = $prompt.data('bot-id') || 'default';
4050 - var rating = parseInt($btn.attr('data-rating'), 10);
4051 - if (rating !== 1 && rating !== -1) return;
4052 - submitRating(botId, rating, '');
4053 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4054 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4055 - });
4056 -
4057 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4058 - e.preventDefault();
4059 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4060 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4061 - var botId = $prompt.data('bot-id') || 'default';
4062 - var s = getState(botId);
4063 - s.dismissed = true;
4064 - markRated(getSessionId(botId));
4065 - ($wrap.length ? $wrap : $prompt).remove();
4066 - });
4067 -
4068 - function closeFeedback($fb) {
4069 - var botId = $fb.data('bot-id') || 'default';
4070 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4071 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4072 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4073 - }
4074 -
4075 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4076 - e.preventDefault();
4077 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4078 - });
4079 -
4080 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4081 - e.preventDefault();
4082 - var $fb = $(this).closest('.mxchat-rating-feedback');
4083 - var botId = $fb.data('bot-id') || 'default';
4084 - var rating = parseInt($fb.attr('data-rating'), 10);
4085 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4086 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4087 - if (text !== '') {
4088 - submitRating(botId, rating, text);
4089 - }
4090 - closeFeedback($fb);
4091 - });
4092 3271 });
4093 3272