PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.5
MxChat – AI Chatbot & Content Generation for WordPress v3.0.5
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 +214 -1092 3.2.63.0.5 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
@@ -45,15 +10,11 @@
45 10
46 11 // Initialize an instance for a bot
47 12 init: function(botId) {
48 13 if (!this.instances[botId]) {
49 - // When persistence is OFF, track when this session started
50 - // so the AI only sees messages from this page load
51 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
52 -
53 14 this.instances[botId] = {
54 15 botId: botId,
55 - sessionId: null,
16 + sessionId: this.getChatSession(botId),
56 17 lastSeenMessageId: '',
57 18 notificationCheckInterval: null,
58 19 pollingInterval: null,
59 20 processedMessageIds: new Set(),
@@ -59,11 +20,9 @@
59 20 processedMessageIds: new Set(),
60 21 activePdfFile: null,
61 22 activeWordFile: null,
62 23 chatHistoryLoaded: false,
63 - isStreaming: false,
64 - // Fresh context timestamp - only used when persistence is OFF
65 - sessionStartTimestamp: chatPersistenceEnabled ? 0 : Date.now()
24 + isStreaming: false
66 25 };
67 26 }
68 27 return this.instances[botId];
69 28 },
@@ -78,77 +37,23 @@
78 37 return Object.keys(this.instances);
79 38 },
80 39
81 40 // 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 41 getChatSession: function(botId) {
85 42 var cookieName = 'mxchat_session_id_' + botId;
86 - var storageKey = 'mxchat_session_id_' + botId;
87 43 var sessionId = getCookie(cookieName);
88 44
89 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
90 45 if (!sessionId) {
91 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
46 + sessionId = generateSessionId();
47 + this.setChatSession(botId, sessionId);
92 48 }
93 49
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;
50 + return sessionId;
113 51 },
114 52
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 53 setChatSession: function(botId, sessionId) {
147 54 var cookieName = 'mxchat_session_id_' + botId;
148 - var storageKey = 'mxchat_session_id_' + botId;
149 55 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
150 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
151 56 if (this.instances[botId]) {
152 57 this.instances[botId].sessionId = sessionId;
153 58 }
154 59 },
@@ -153,10 +58,8 @@
153 58 }
154 59 },
155 60
156 61 resetChatSession: function(botId) {
157 - // Clear old session from localStorage before setting new one
158 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
159 62 var newSessionId = generateSessionId();
160 63 this.setChatSession(botId, newSessionId);
161 64 var $chatBox = getElement(botId, 'chat-box');
162 65 if ($chatBox.length) {
@@ -165,20 +68,8 @@
165 68 if (this.instances[botId]) {
166 69 this.instances[botId].chatHistoryLoaded = false;
167 70 this.instances[botId].processedMessageIds = new Set();
168 71 }
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 72 }
182 73 };
183 74
184 75 // ====================================
@@ -483,9 +374,8 @@
483 374
484 375 // Update your existing sendMessage function
485 376 function sendMessage(botId) {
486 377 botId = botId || 'default';
487 - MxChatInstances.ensureSession(botId);
488 378 var $chatInput = getElement(botId, 'chat-input');
489 379 var message = $chatInput.val();
490 380
491 381 // ADD PROMPT HOOK HERE
@@ -493,14 +383,10 @@
493 383 message = customMxChatFilter(message, "prompt");
494 384 }
495 385
496 386 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 - }
387 + // Disable input while waiting for response
388 + disableChatInput(botId);
503 389
504 390 appendMessage("user", message, '', [], false, botId);
505 391 $chatInput.val('');
506 392 $chatInput.css('height', 'auto');
@@ -510,9 +396,9 @@
510 396 }
511 397 appendThinkingMessage(botId);
512 398 scrollToBottom(botId);
513 399
514 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
400 + const currentModel = mxchatChat.model || 'gpt-4o';
515 401
516 402 // Check if streaming is enabled AND supported for this model
517 403 if (shouldUseStreaming(currentModel)) {
518 404 callMxChatStream(message, function(response) {
@@ -528,9 +414,8 @@
528 414
529 415 // Update your existing sendMessageToChatbot function
530 416 function sendMessageToChatbot(message, botId) {
531 417 botId = botId || 'default';
532 - MxChatInstances.ensureSession(botId);
533 418
534 419 // ADD PROMPT HOOK HERE
535 420 if (typeof customMxChatFilter === 'function') {
536 421 message = customMxChatFilter(message, "prompt");
@@ -535,14 +420,10 @@
535 420 if (typeof customMxChatFilter === 'function') {
536 421 message = customMxChatFilter(message, "prompt");
537 422 }
538 423
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 - }
424 + // Disable input while waiting for response
425 + disableChatInput(botId);
545 426
546 427 var sessionId = getChatSession(botId);
547 428
548 429 if (hasQuickQuestions(botId)) {
@@ -550,9 +431,9 @@
550 431 }
551 432 appendThinkingMessage(botId);
552 433 scrollToBottom(botId);
553 434
554 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
435 + const currentModel = mxchatChat.model || 'gpt-4o';
555 436
556 437 // Check if streaming is enabled AND supported for this model
557 438 if (shouldUseStreaming(currentModel)) {
558 439 callMxChatStream(message, function(response) {
@@ -627,44 +508,24 @@
627 508
628 509 // Get page context if contextual awareness is enabled
629 510 const pageContext = getPageContext();
630 511
631 - // Get instance for session start timestamp (used when persistence is OFF)
632 - var instance = MxChatInstances.get(botId);
633 -
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 512 // Prepare AJAX data
650 513 const ajaxData = {
651 514 action: 'mxchat_handle_chat_request',
652 515 message: message,
653 - session_id: sessionId,
516 + session_id: getChatSession(botId),
654 517 nonce: mxchatChat.nonce,
655 518 current_page_url: window.location.href,
656 519 current_page_title: document.title,
657 - bot_id: botId,
658 - // Pass session start timestamp so AI context matches what user sees
659 - session_start_timestamp: instance.sessionStartTimestamp || 0
520 + bot_id: botId
660 521 };
661 -
522 +
662 523 // Add page context if available
663 524 if (pageContext) {
664 525 ajaxData.page_context = JSON.stringify(pageContext);
665 526 }
666 -
527 +
667 528 // CHECK FOR VISION FLAGS AND ADD THEM
668 529 if (window.mxchatVisionProcessed) {
669 530 ajaxData.vision_processed = true;
670 531 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -673,9 +534,9 @@
673 534 window.mxchatVisionProcessed = false;
674 535 window.mxchatOriginalMessage = null;
675 536 window.mxchatVisionImagesCount = 0;
676 537 }
677 -
538 +
678 539 $.ajax({
679 540 url: mxchatChat.ajax_url,
680 541 type: 'POST',
681 542 dataType: 'json',
@@ -713,16 +574,23 @@
713 574 errorMessage = "An error occurred. Please try again or contact support.";
714 575 }
715 576
716 577 // Handle session reset action (IP changed, session expired, etc.)
717 - // Silent reset — keep chat UI intact, just get a new session and retry
718 578 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)
579 + // Clear the old session and generate a new one
580 + resetChatSession(botId);
581 + // Remove the temporary loading message
582 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
583 + // Re-send the original message with the new session
721 584 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
722 585 if (originalMessage) {
723 586 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
724 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
587 + // Re-add the user message and thinking indicator
588 + appendMessage("user", originalMessage, '', [], false, botId);
589 + appendThinkingMessage(botId);
590 + scrollToBottom(botId);
591 + // Determine whether to use streaming
592 + const currentModel = mxchatChat.model || 'gpt-4o';
725 593 if (shouldUseStreaming(currentModel)) {
726 594 callMxChatStream(originalMessage, function(response) {
727 595 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
728 596 }, botId);
@@ -779,11 +647,9 @@
779 647 }
780 648
781 649 // Check for live agent response
782 650 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
783 - removeThinkingDots(botId);
784 651 updateChatModeIndicator('agent', botId);
785 - enableChatInput(botId);
786 652 return;
787 653 }
788 654
789 655 // Handle the message and show notification if chat is hidden
@@ -816,13 +682,9 @@
816 682 $badge.show();
817 683 }
818 684 }
819 685 } 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);
686 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
825 687 }
826 688
827 689 if (response.message_id) {
828 690 var instance = MxChatInstances.get(botId);
@@ -864,9 +726,8 @@
864 726
865 727 replaceLastMessage("bot", errorMessage, '', [], botId);
866 728 }
867 729 });
868 - }); // refreshNonceIfNeeded
869 730 }
870 731
871 732 function callMxChatStream(message, callback, botId) {
872 733 botId = botId || getMxChatBotId();
@@ -873,9 +734,9 @@
873 734
874 735 // Store the message in case we need to retry after session reset
875 736 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
876 737
877 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
738 + const currentModel = mxchatChat.model || 'gpt-4o';
878 739 if (!isStreamingSupported(currentModel)) {
879 740 callMxChat(message, callback, botId);
880 741 return;
881 742 }
@@ -882,35 +743,17 @@
882 743
883 744 // Get page context if contextual awareness is enabled
884 745 const pageContext = getPageContext();
885 746
886 - // Get instance for session start timestamp (used when persistence is OFF)
887 - var instance = MxChatInstances.get(botId);
888 -
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 747 const formData = new FormData();
903 748 formData.append('action', 'mxchat_stream_chat');
904 749 formData.append('message', message);
905 - formData.append('session_id', streamSessionId);
750 + formData.append('session_id', getChatSession(botId));
906 751 formData.append('nonce', mxchatChat.nonce);
907 752 formData.append('current_page_url', window.location.href);
908 753 formData.append('current_page_title', document.title);
909 754 formData.append('bot_id', botId);
910 - // Pass session start timestamp so AI context matches what user sees
911 - formData.append('session_start_timestamp', instance.sessionStartTimestamp || 0);
912 -
755 +
913 756 // Add page context if available
914 757 if (pageContext) {
915 758 formData.append('page_context', JSON.stringify(pageContext));
916 759 }
@@ -1003,16 +846,8 @@
1003 846
1004 847 // Re-enable chat input when stream ends with content
1005 848 enableChatInput(botId);
1006 849
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 850 if (callback) {
1016 851 callback(accumulatedContent);
1017 852 }
1018 853 return;
@@ -1035,16 +870,8 @@
1035 870
1036 871 // Re-enable chat input after streaming completes
1037 872 enableChatInput(botId);
1038 873
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 874 if (callback) {
1048 875 callback(accumulatedContent);
1049 876 }
1050 877 return;
@@ -1123,9 +950,8 @@
1123 950 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1124 951 callMxChat(message, callback, botId);
1125 952 }
1126 953 });
1127 - }); // refreshNonceIfNeeded
1128 954 }
1129 955
1130 956 // Helper function to handle non-streaming responses
1131 957 function handleNonStreamResponse(data, callback, botId) {
@@ -1164,16 +990,21 @@
1164 990 errorMessage = "An error occurred. Please try again or contact support.";
1165 991 }
1166 992
1167 993 // Handle session reset action (IP changed, session expired, etc.)
1168 - // Silent reset — keep chat UI intact, just get a new session and retry
1169 994 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)
995 + // Clear the old session and generate a new one
996 + resetChatSession(botId);
997 + // Re-send the original message with the new session
1172 998 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1173 999 if (originalMessage) {
1174 1000 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1175 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1001 + // Re-add the user message and thinking indicator
1002 + appendMessage("user", originalMessage, '', [], false, botId);
1003 + appendThinkingMessage(botId);
1004 + scrollToBottom(botId);
1005 + // Determine whether to use streaming
1006 + const currentModel = mxchatChat.model || 'gpt-4o';
1176 1007 if (shouldUseStreaming(currentModel)) {
1177 1008 callMxChatStream(originalMessage, callback, botId);
1178 1009 } else {
1179 1010 callMxChat(originalMessage, callback, botId);
@@ -1195,22 +1026,8 @@
1195 1026 }
1196 1027 return; // Exit early for errors
1197 1028 }
1198 1029
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 1030 // Handle different response formats
1214 1031 if (data.text || data.html || data.message) {
1215 1032
1216 1033 // Apply response hooks
@@ -1255,15 +1072,19 @@
1255 1072 }
1256 1073
1257 1074 // Enhanced updateChatModeIndicator function for immediate DOM updates
1258 1075 function updateChatModeIndicator(mode, botId) {
1076 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1259 1077 botId = botId || 'default';
1260 1078 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1079 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1261 1080 if (indicator) {
1262 1081 const oldText = indicator.textContent;
1082 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1263 1083
1264 1084 if (mode === 'agent') {
1265 1085 indicator.textContent = 'Live Agent';
1086 + console.log('[MxChat] Mode is agent, calling startPolling...');
1266 1087 startPolling(botId);
1267 1088 } else {
1268 1089 // Everything else is AI mode
1269 1090 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1334,12 +1155,9 @@
1334 1155 // Update the event handlers to use the correct function names (using event delegation)
1335 1156 // Use class-based selectors for multi-instance support
1336 1157 $(document).on('click', '.send-button', function() {
1337 1158 var botId = getBotIdFromElement(this);
1338 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1339 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1340 - disableChatInput(botId);
1341 - }
1159 + disableChatInput(botId);
1342 1160 sendMessage(botId);
1343 1161 });
1344 1162
1345 1163 // Override enter key handler (using event delegation)
@@ -1346,237 +1164,14 @@
1346 1164 $(document).on('keypress', '.chat-input', function(e) {
1347 1165 if (e.which == 13 && !e.shiftKey) {
1348 1166 e.preventDefault();
1349 1167 var botId = getBotIdFromElement(this);
1350 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1351 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1352 - disableChatInput(botId);
1353 - }
1168 + disableChatInput(botId);
1354 1169 sendMessage(botId);
1355 1170 }
1356 1171 });
1357 1172
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 -
1173 +
1579 1174 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1580 1175 try {
1581 1176 // Determine styles based on sender type
1582 1177 let messageClass, bgColor, fontColor;
@@ -1614,12 +1209,17 @@
1614 1209 'margin-bottom': '1em'
1615 1210 });
1616 1211 }
1617 1212
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);
1213 + // Process the message content based on sender
1214 + let fullMessage;
1215 + if (sender === "user") {
1216 + // For user messages, apply linkify after sanitization
1217 + fullMessage = linkify(messageText);
1218 + } else {
1219 + // For bot/agent messages, preserve HTML
1220 + fullMessage = messageText;
1221 + }
1622 1222
1623 1223 // Add images if provided
1624 1224 if (images && images.length > 0) {
1625 1225 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1668,12 +1268,8 @@
1668 1268 if (lastUserMessage.length) {
1669 1269 scrollElementToTop(lastUserMessage, botId);
1670 1270 }
1671 1271 }
1672 -
1673 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1674 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1675 - }
1676 1272 });
1677 1273
1678 1274 if (messageText.id) {
1679 1275 var instance = MxChatInstances.get(botId);
@@ -1758,12 +1354,26 @@
1758 1354 bgColor = botMessageBgColor;
1759 1355 fontColor = botMessageFontColor;
1760 1356 }
1761 1357
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);
1358 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1359 + // This prevents double-processing of URLs that are already formatted as HTML
1360 + var fullMessage;
1361 + if (sender === "user") {
1362 + // Always linkify user messages (they're plain text)
1363 + fullMessage = linkify(responseText);
1364 + } else {
1365 + // For bot/agent messages, check if HTML already exists
1366 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1367 + responseText.includes('<img') || responseText.includes('<div') ||
1368 + responseText.includes('<p>') || responseText.includes('<br>')) {
1369 + // Response already has HTML, don't process it
1370 + fullMessage = responseText;
1371 + } else {
1372 + // Plain text response, apply linkify
1373 + fullMessage = linkify(responseText);
1374 + }
1375 + }
1766 1376
1767 1377 if (responseHtml) {
1768 1378 // Only add line breaks if there's actual text content before the HTML
1769 1379 if (fullMessage && fullMessage.trim()) {
@@ -1820,12 +1430,8 @@
1820 1430 }
1821 1431
1822 1432 // Re-enable chat input after response is displayed
1823 1433 enableChatInput(botId);
1824 -
1825 - if (sender === "bot" || sender === "agent") {
1826 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1827 - }
1828 1434 } else {
1829 1435 appendMessage(sender, responseText, responseHtml, images, false, botId);
1830 1436 // Re-enable chat input after response is displayed
1831 1437 enableChatInput(botId);
@@ -1834,15 +1440,8 @@
1834 1440
1835 1441
1836 1442 function appendThinkingMessage(botId) {
1837 1443 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 1444 var $chatBox = getElement(botId, 'chat-box');
1846 1445
1847 1446 // Remove any existing thinking dots in this bot's chat first
1848 1447 $chatBox.find('.thinking-dots').remove();
@@ -1864,9 +1463,9 @@
1864 1463 '</div>' +
1865 1464 '</div>';
1866 1465
1867 1466 // 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 + ';"';
1467 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
1869 1468 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1870 1469 scrollToBottom(botId);
1871 1470 }
1872 1471
@@ -1872,11 +1471,9 @@
1872 1471
1873 1472 function removeThinkingDots(botId) {
1874 1473 botId = botId || 'default';
1875 1474 var $chatBox = getElement(botId, 'chat-box');
1876 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1877 1475 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1878 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1879 1476 }
1880 1477
1881 1478 // ====================================
1882 1479 // TEXT FORMATTING & PROCESSING
@@ -1910,12 +1507,9 @@
1910 1507 processedText = formatTextStyling(processedText);
1911 1508
1912 1509 // Process code blocks BEFORE processing links
1913 1510 processedText = formatCodeBlocks(processedText);
1914 -
1915 - // Process markdown tables BEFORE converting newlines to paragraphs
1916 - processedText = formatMarkdownTables(processedText);
1917 -
1511 +
1918 1512 // NOW convert to paragraphs
1919 1513 processedText = convertNewlinesToBreaks(processedText);
1920 1514
1921 1515 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1928,63 +1522,37 @@
1928 1522 // Return as a proper link without the brackets
1929 1523 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1930 1524 });
1931 1525
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 - }
1526 + // Process proper markdown links with text: [text](url)
1527 + // This MUST have non-empty text in the first brackets
1528 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1529 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1530 + // Make sure we have actual text (not just whitespace)
1531 + if (!text || !text.trim()) {
1532 + // If no text, treat the URL as the text
1533 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1534 + const safeUrl = safeEncodeUrl(cleanUrl);
1535 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1983 1536 }
1984 - return result;
1985 - })(processedText);
1537 +
1538 + // Clean the URL
1539 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1540 + const safeUrl = safeEncodeUrl(cleanUrl);
1541 + const safeText = sanitizeUserInput(text);
1542 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1543 + });
1986 1544
1545 + // Handle empty markdown links: [](url)
1546 + // This is a specific case where there's no text
1547 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1548 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1549 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1550 + const safeUrl = safeEncodeUrl(cleanUrl);
1551 + // Use the URL itself as the link text
1552 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1553 + });
1554 +
1987 1555 // Process phone numbers: [text](tel:number)
1988 1556 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1989 1557 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1990 1558 const safePhone = safeEncodeUrl(phone);
@@ -2136,78 +1704,9 @@
2136 1704 });
2137 1705
2138 1706 return text;
2139 1707 }
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 -
1708 +
2210 1709 function sanitizeUserInput(text) {
2211 1710 const div = document.createElement('div');
2212 1711 div.textContent = text;
2213 1712 return div.innerHTML;
@@ -2278,14 +1777,13 @@
2278 1777 requestAnimationFrame(smoothScroll);
2279 1778 }
2280 1779 }
2281 1780
2282 - function scrollElementToTop(element, botId, topOffset) {
1781 + function scrollElementToTop(element, botId) {
2283 1782 botId = botId || 'default';
2284 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2285 1783 var chatBox = getElement(botId, 'chat-box');
2286 1784 var elementTop = element.position().top + chatBox.scrollTop();
2287 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1785 + chatBox.animate({ scrollTop: elementTop }, 500);
2288 1786 }
2289 1787
2290 1788 function showChatWidget(botId) {
2291 1789 botId = botId || 'default';
@@ -2429,12 +1927,15 @@
2429 1927 // LIVE AGENT FUNCTIONALITY
2430 1928 // ====================================
2431 1929
2432 1930 function startPolling(botId) {
1931 + console.log('[MxChat] startPolling called for botId:', botId);
2433 1932 botId = botId || 'default';
2434 1933 var instance = MxChatInstances.get(botId);
2435 1934 // Clear any existing interval first
2436 1935 stopPolling(botId);
1936 + // Start new polling interval
1937 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2437 1938 instance.pollingInterval = setInterval(function() {
2438 1939 checkForAgentMessages(botId);
2439 1940 }, 5000);
2440 1941 }
@@ -2439,17 +1940,20 @@
2439 1940 }, 5000);
2440 1941 }
2441 1942
2442 1943 function stopPolling(botId) {
1944 + console.log('[MxChat] stopPolling called for botId:', botId);
2443 1945 botId = botId || 'default';
2444 1946 var instance = MxChatInstances.get(botId);
2445 1947 if (instance.pollingInterval) {
2446 1948 clearInterval(instance.pollingInterval);
2447 1949 instance.pollingInterval = null;
1950 + console.log('[MxChat] Polling stopped for botId:', botId);
2448 1951 }
2449 1952 }
2450 1953
2451 1954 function checkForAgentMessages(botId) {
1955 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2452 1956 botId = botId || 'default';
2453 1957 var instance = MxChatInstances.get(botId);
2454 1958 const sessionId = getChatSession(botId);
2455 1959 $.ajax({
@@ -2475,12 +1979,8 @@
2475 1979 instance.processedMessageIds.add(message.id);
2476 1980 }
2477 1981 });
2478 1982
2479 - if (hasNewMessage) {
2480 - enableChatInput(botId);
2481 - }
2482 -
2483 1983 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2484 1984 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2485 1985 showNotification(botId);
2486 1986 }
@@ -2486,13 +1986,8 @@
2486 1986 }
2487 1987
2488 1988 scrollToBottom(botId, true);
2489 1989 }
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 1990 },
2496 1991 error: function (xhr, status, error) {
2497 1992 // Polling error - silently continue
2498 1993 }
@@ -2502,29 +1997,20 @@
2502 1997 // ====================================
2503 1998 // CHAT HISTORY & PERSISTENCE
2504 1999 // ====================================
2505 2000
2506 -function loadChatHistory(botId, onComplete) {
2001 +function loadChatHistory(botId) {
2507 2002 botId = botId || 'default';
2508 2003 var instance = MxChatInstances.get(botId);
2509 2004
2510 2005 // Prevent duplicate loading
2511 2006 if (instance.chatHistoryLoaded) {
2512 - if (onComplete) onComplete();
2513 2007 return;
2514 2008 }
2515 2009
2516 - // Use getChatSession which returns null if no session exists (does NOT create one)
2517 2010 var sessionId = getChatSession(botId);
2518 2011 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2519 2012
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 2013 if (chatPersistenceEnabled && sessionId) {
2528 2014 $.ajax({
2529 2015 url: mxchatChat.ajax_url,
2530 2016 type: 'POST',
@@ -2535,12 +2021,11 @@
2535 2021 },
2536 2022 success: function(response) {
2537 2023 // Handle session reset (IP changed while user was away)
2538 2024 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);
2025 + // Silently reset session - user will start fresh
2026 + resetChatSession(botId);
2541 2027 instance.chatHistoryLoaded = true; // Prevent retry loop
2542 - if (onComplete) onComplete();
2543 2028 return;
2544 2029 }
2545 2030
2546 2031 // Check if the response indicates success
@@ -2596,19 +2081,9 @@
2596 2081 var content = message.content;
2597 2082 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2598 2083 content = decodeHTMLEntities(content);
2599 2084
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")) {
2085 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2611 2086 messageElement.html(content);
2612 2087 } else {
2613 2088 var formattedContent = linkify(content);
2614 2089 messageElement.html(formattedContent);
@@ -2648,17 +2123,13 @@
2648 2123 instance.chatHistoryLoaded = true;
2649 2124 }
2650 2125 }
2651 2126 }
2652 - if (onComplete) onComplete();
2653 2127 },
2654 2128 error: function(xhr, status, error) {
2655 2129 // Error loading chat history - silently continue
2656 - if (onComplete) onComplete();
2657 2130 }
2658 2131 });
2659 - } else {
2660 - if (onComplete) onComplete();
2661 2132 }
2662 2133 }
2663 2134
2664 2135
@@ -2834,35 +2305,45 @@
2834 2305 // ====================================
2835 2306
2836 2307 function checkPreChatDismissal(botId) {
2837 2308 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) {
2309 + $.ajax({
2310 + url: mxchatChat.ajax_url,
2311 + type: 'POST',
2312 + data: {
2313 + action: 'mxchat_check_pre_chat_message_status',
2314 + _ajax_nonce: mxchatChat.nonce
2315 + },
2316 + success: function(response) {
2317 + if (response.success && !response.data.dismissed) {
2318 + getElement(botId, 'pre-chat-message').fadeIn(250);
2319 + } else {
2844 2320 getElement(botId, 'pre-chat-message').hide();
2845 - return;
2846 2321 }
2847 - // Expired — clear and show again
2848 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2322 + },
2323 + error: function() {
2324 + // Error checking pre-chat dismissal - silently continue
2849 2325 }
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 - }
2326 + });
2855 2327 }
2856 2328
2857 2329 function handlePreChatDismissal(botId) {
2858 2330 botId = botId || 'default';
2859 2331 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 - }
2332 + $.ajax({
2333 + url: mxchatChat.ajax_url,
2334 + type: 'POST',
2335 + data: {
2336 + action: 'mxchat_dismiss_pre_chat_message',
2337 + _ajax_nonce: mxchatChat.nonce
2338 + },
2339 + success: function() {
2340 + $('#pre-chat-message').hide();
2341 + },
2342 + error: function() {
2343 + // Error dismissing pre-chat message - silently continue
2344 + }
2345 + });
2865 2346 }
2866 2347
2867 2348
2868 2349 // ====================================
@@ -2929,26 +2410,8 @@
2929 2410 $(this).addClass('hidden');
2930 2411 $badge.hide(); // Hide notification when opening chat
2931 2412 disableScroll();
2932 2413 $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 2414 } else {
2952 2415 $chatbot.removeClass('visible').addClass('hidden');
2953 2416 $(this).removeClass('hidden');
2954 2417 enableScroll();
@@ -2966,9 +2429,11 @@
2966 2429
2967 2430 $(document).on('click', '.close-pre-chat-message', function(e) {
2968 2431 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2969 2432 var botId = getBotIdFromElement(this);
2970 - handlePreChatDismissal(botId);
2433 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2434 + $(this).remove();
2435 + });
2971 2436 });
2972 2437
2973 2438
2974 2439 // PDF upload button handlers - use class selector
@@ -3009,10 +2474,8 @@
3009 2474 const sendBtn = document.getElementById('send-button');
3010 2475 const originalBtnContent = uploadBtn.innerHTML;
3011 2476
3012 2477 try {
3013 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3014 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3015 2478 const formData = new FormData();
3016 2479 formData.append('action', 'mxchat_upload_pdf');
3017 2480 formData.append('pdf_file', file);
3018 2481 formData.append('session_id', sessionId);
@@ -3076,10 +2539,8 @@
3076 2539 const sendBtn = document.getElementById('send-button');
3077 2540 const originalBtnContent = uploadBtn.innerHTML;
3078 2541
3079 2542 try {
3080 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3081 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3082 2543 const formData = new FormData();
3083 2544 formData.append('action', 'mxchat_upload_word');
3084 2545 formData.append('word_file', file);
3085 2546 formData.append('session_id', sessionId);
@@ -3173,59 +2634,8 @@
3173 2634 });
3174 2635
3175 2636
3176 2637 // ====================================
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 2638 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3229 2639 // ====================================
3230 2640 // Only run email collection setup if it's enabled
3231 2641 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3261,8 +2671,28 @@
3261 2671 `;
3262 2672 document.head.appendChild(style);
3263 2673 }
3264 2674
2675 + // Helper functions for email collection (multi-instance aware)
2676 + function showEmailFormForBot(botId) {
2677 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2678 + var chatContainer = getElementDOM(botId, 'chat-container');
2679 + if (emailBlocker) emailBlocker.style.display = 'flex';
2680 + if (chatContainer) chatContainer.style.display = 'none';
2681 + }
2682 +
2683 + function showChatContainerForBot(botId) {
2684 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2685 + var chatContainer = getElementDOM(botId, 'chat-container');
2686 + if (emailBlocker) emailBlocker.style.display = 'none';
2687 + if (chatContainer) chatContainer.style.display = 'flex';
2688 +
2689 + // Load chat history for this bot
2690 + if (typeof loadChatHistory === 'function') {
2691 + loadChatHistory(botId);
2692 + }
2693 + }
2694 +
3265 2695 function isValidEmailAddress(email) {
3266 2696 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3267 2697 return emailRegex.test(email.trim()) && email.length <= 254;
3268 2698 }
@@ -3270,41 +2700,8 @@
3270 2700 function isValidNameInput(name) {
3271 2701 return name && name.trim().length >= 2 && name.trim().length <= 100;
3272 2702 }
3273 2703
3274 - /**
3275 - * Replace {visitor_name} placeholder in intro message with actual visitor name
3276 - * @param {string} botId - The bot instance ID
3277 - * @param {string} visitorName - The visitor's name to insert
3278 - */
3279 - function replaceVisitorNamePlaceholder(botId, visitorName) {
3280 - var chatBox = getElementDOM(botId, 'chat-box');
3281 - if (!chatBox) return;
3282 -
3283 - // Find the first bot message (intro message)
3284 - var introMessage = chatBox.querySelector('.bot-message');
3285 - if (!introMessage) return;
3286 -
3287 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3288 - if (!messageContent) return;
3289 -
3290 - var html = messageContent.innerHTML;
3291 -
3292 - // Replace {visitor_name} placeholder (case-insensitive)
3293 - if (visitorName && visitorName.trim()) {
3294 - // Escape HTML to prevent XSS
3295 - var safeName = $('<div>').text(visitorName.trim()).html();
3296 - html = html.replace(/\{visitor_name\}/gi, safeName);
3297 - } else {
3298 - // Remove placeholder and clean up spacing if no name provided
3299 - html = html.replace(/\{visitor_name\}/gi, '');
3300 - // Clean up any double spaces that might result
3301 - html = html.replace(/\s{2,}/g, ' ').trim();
3302 - }
3303 -
3304 - messageContent.innerHTML = html;
3305 - }
3306 -
3307 2704 function setEmailSubmissionState(botId, loading) {
3308 2705 var submitButton = getElementDOM(botId, 'email-submit-button');
3309 2706 var emailInput = getElementDOM(botId, 'user-email');
3310 2707 var nameInput = getElementDOM(botId, 'user-name');
@@ -3384,31 +2781,11 @@
3384 2781 existingErrors.forEach(error => error.remove());
3385 2782 }
3386 2783 }
3387 2784
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 2785 function checkSessionAndEmailForBot(botId) {
3402 - const sessionId = MxChatInstances.ensureSession(botId);
2786 + const sessionId = getChatSession(botId);
3403 2787
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 2788 fetch(mxchatChat.ajax_url, {
3412 2789 method: 'POST',
3413 2790 headers: {
3414 2791 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3456,9 +2833,9 @@
3456 2833 var emailInput = getElementDOM(botId, 'user-email');
3457 2834 var nameInput = getElementDOM(botId, 'user-name');
3458 2835 var userEmail = emailInput ? emailInput.value.trim() : '';
3459 2836 var userName = nameInput ? nameInput.value.trim() : '';
3460 - var sessionId = MxChatInstances.ensureSession(botId);
2837 + var sessionId = getChatSession(botId);
3461 2838
3462 2839 // Validate email
3463 2840 if (!userEmail) {
3464 2841 showEmailError(botId, 'Please enter your email address.');
@@ -3509,16 +2886,8 @@
3509 2886
3510 2887 if (data.success) {
3511 2888 showChatContainerForBot(botId);
3512 2889
3513 - // Replace {visitor_name} placeholder in intro message with actual name
3514 - if (userName) {
3515 - replaceVisitorNamePlaceholder(botId, userName);
3516 - } else {
3517 - // Remove placeholder if no name provided
3518 - replaceVisitorNamePlaceholder(botId, '');
3519 - }
3520 -
3521 2890 if (data.message && typeof appendMessage === 'function') {
3522 2891 setTimeout(() => {
3523 2892 appendMessage('bot', data.message, '', [], false, botId);
3524 2893 if (typeof scrollToBottom === 'function') {
@@ -3581,27 +2950,25 @@
3581 2950 }
3582 2951 });
3583 2952
3584 2953 // 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 2954 $('.mxchat-chatbot-wrapper').each(function() {
3588 2955 var botId = $(this).data('bot-id') || 'default';
3589 2956 var emailBlocker = getElementDOM(botId, 'email-blocker');
3590 2957
2958 + // Only check if email blocker exists for this bot
3591 2959 if (emailBlocker) {
3592 - if (isEmbeddedBot(botId)) {
3593 - // Embedded bots are always visible — check now
3594 - resolveEmailState(botId);
2960 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2961 + if (mxchatChat.initial_email_state.show_email_form) {
2962 + showEmailFormForBot(botId);
2963 + } else {
2964 + showChatContainerForBot(botId);
2965 + }
2966 + } else {
2967 + setTimeout(function() {
2968 + checkSessionAndEmailForBot(botId);
2969 + }, 100);
3595 2970 }
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 2971 }
3605 2972 });
3606 2973 }
3607 2974
@@ -3611,32 +2978,39 @@
3611 2978 var $chatbot = getElement(botId, 'floating-chatbot');
3612 2979 if ($chatbot.hasClass('hidden')) {
3613 2980 $chatbot.removeClass('hidden').addClass('visible');
3614 2981 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3615 - handlePreChatDismissal(botId);
2982 + $(this).fadeOut(250); // Hide pre-chat message
3616 2983 disableScroll(); // Disable scroll when chatbot opens
2984 + }
2985 + });
3617 2986
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 - }
2987 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
2988 + // This is a fallback for legacy support
2989 + $(document).on('click', '.close-pre-chat-message', function() {
2990 + var botId = getBotIdFromElement(this);
2991 + var $preChat = getElement(botId, 'pre-chat-message');
2992 + $preChat.fadeOut(200); // Hide the message
3623 2993
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);
2994 + // Send an AJAX request to set the transient flag for 24 hours
2995 + $.ajax({
2996 + url: mxchatChat.ajax_url,
2997 + type: 'POST',
2998 + data: {
2999 + action: 'mxchat_dismiss_pre_chat_message',
3000 + _ajax_nonce: mxchatChat.nonce
3001 + },
3002 + success: function() {
3003 + // Ensure the message is hidden after dismissal
3004 + $preChat.hide();
3005 + },
3006 + error: function() {
3007 + // Error dismissing pre-chat message - silently continue
3632 3008 }
3633 - }
3009 + });
3634 3010 });
3635 3011
3636 - // Legacy duplicate close handler removed — handled by single event delegation above
3637 3012
3638 -
3639 3013 function hasQuickQuestions(botId) {
3640 3014 botId = botId || 'default';
3641 3015 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3642 3016 if (!questionsContainer) return false;
@@ -3772,11 +3146,18 @@
3772 3146 });
3773 3147
3774 3148 // Initialize when document is ready
3775 3149 setFullHeight();
3150 + trackOriginatingPage();
3776 3151
3777 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3778 - // until the user's first interaction via MxChatInstances.ensureSession()
3152 + // Only load chat history if email collection is disabled
3153 + if (mxchatChat.email_collection_enabled !== 'on') {
3154 + // Load history for all instances
3155 + $('.mxchat-chatbot-wrapper').each(function() {
3156 + var botId = $(this).data('bot-id') || 'default';
3157 + loadChatHistory(botId);
3158 + });
3159 + }
3779 3160
3780 3161 // Initialize chat visibility for all instances
3781 3162 $('.mxchat-chatbot-wrapper').each(function() {
3782 3163 var botId = $(this).data('bot-id') || 'default';
@@ -3829,265 +3210,6 @@
3829 3210 }, 2000);
3830 3211 });
3831 3212 }
3832 3213 }
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 3214 });
4093 3215