PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.2
MxChat – AI Chatbot & Content Generation for WordPress v3.0.2
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/chat-script.js +506 -1350 3.2.63.0.2 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 }
@@ -1000,19 +843,8 @@
1000 843 });
1001 844 return;
1002 845 }
1003 846
1004 - // Re-enable chat input when stream ends with content
1005 - enableChatInput(botId);
1006 -
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 847 if (callback) {
1016 848 callback(accumulatedContent);
1017 849 }
1018 850 return;
@@ -1035,16 +867,8 @@
1035 867
1036 868 // Re-enable chat input after streaming completes
1037 869 enableChatInput(botId);
1038 870
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 871 if (callback) {
1048 872 callback(accumulatedContent);
1049 873 }
1050 874 return;
@@ -1123,9 +947,8 @@
1123 947 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1124 948 callMxChat(message, callback, botId);
1125 949 }
1126 950 });
1127 - }); // refreshNonceIfNeeded
1128 951 }
1129 952
1130 953 // Helper function to handle non-streaming responses
1131 954 function handleNonStreamResponse(data, callback, botId) {
@@ -1164,16 +987,21 @@
1164 987 errorMessage = "An error occurred. Please try again or contact support.";
1165 988 }
1166 989
1167 990 // Handle session reset action (IP changed, session expired, etc.)
1168 - // Silent reset — keep chat UI intact, just get a new session and retry
1169 991 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)
992 + // Clear the old session and generate a new one
993 + resetChatSession(botId);
994 + // Re-send the original message with the new session
1172 995 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1173 996 if (originalMessage) {
1174 997 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1175 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
998 + // Re-add the user message and thinking indicator
999 + appendMessage("user", originalMessage, '', [], false, botId);
1000 + appendThinkingMessage(botId);
1001 + scrollToBottom(botId);
1002 + // Determine whether to use streaming
1003 + const currentModel = mxchatChat.model || 'gpt-4o';
1176 1004 if (shouldUseStreaming(currentModel)) {
1177 1005 callMxChatStream(originalMessage, callback, botId);
1178 1006 } else {
1179 1007 callMxChat(originalMessage, callback, botId);
@@ -1195,22 +1023,8 @@
1195 1023 }
1196 1024 return; // Exit early for errors
1197 1025 }
1198 1026
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 1027 // Handle different response formats
1214 1028 if (data.text || data.html || data.message) {
1215 1029
1216 1030 // Apply response hooks
@@ -1255,15 +1069,19 @@
1255 1069 }
1256 1070
1257 1071 // Enhanced updateChatModeIndicator function for immediate DOM updates
1258 1072 function updateChatModeIndicator(mode, botId) {
1073 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1259 1074 botId = botId || 'default';
1260 1075 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1076 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1261 1077 if (indicator) {
1262 1078 const oldText = indicator.textContent;
1079 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1263 1080
1264 1081 if (mode === 'agent') {
1265 1082 indicator.textContent = 'Live Agent';
1083 + console.log('[MxChat] Mode is agent, calling startPolling...');
1266 1084 startPolling(botId);
1267 1085 } else {
1268 1086 // Everything else is AI mode
1269 1087 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1334,12 +1152,9 @@
1334 1152 // Update the event handlers to use the correct function names (using event delegation)
1335 1153 // Use class-based selectors for multi-instance support
1336 1154 $(document).on('click', '.send-button', function() {
1337 1155 var botId = getBotIdFromElement(this);
1338 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1339 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1340 - disableChatInput(botId);
1341 - }
1156 + disableChatInput(botId);
1342 1157 sendMessage(botId);
1343 1158 });
1344 1159
1345 1160 // Override enter key handler (using event delegation)
@@ -1346,237 +1161,14 @@
1346 1161 $(document).on('keypress', '.chat-input', function(e) {
1347 1162 if (e.which == 13 && !e.shiftKey) {
1348 1163 e.preventDefault();
1349 1164 var botId = getBotIdFromElement(this);
1350 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1351 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1352 - disableChatInput(botId);
1353 - }
1165 + disableChatInput(botId);
1354 1166 sendMessage(botId);
1355 1167 }
1356 1168 });
1357 1169
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 -
1170 +
1579 1171 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1580 1172 try {
1581 1173 // Determine styles based on sender type
1582 1174 let messageClass, bgColor, fontColor;
@@ -1614,12 +1206,17 @@
1614 1206 'margin-bottom': '1em'
1615 1207 });
1616 1208 }
1617 1209
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);
1210 + // Process the message content based on sender
1211 + let fullMessage;
1212 + if (sender === "user") {
1213 + // For user messages, apply linkify after sanitization
1214 + fullMessage = linkify(messageText);
1215 + } else {
1216 + // For bot/agent messages, preserve HTML
1217 + fullMessage = messageText;
1218 + }
1622 1219
1623 1220 // Add images if provided
1624 1221 if (images && images.length > 0) {
1625 1222 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1668,12 +1265,8 @@
1668 1265 if (lastUserMessage.length) {
1669 1266 scrollElementToTop(lastUserMessage, botId);
1670 1267 }
1671 1268 }
1672 -
1673 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1674 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1675 - }
1676 1269 });
1677 1270
1678 1271 if (messageText.id) {
1679 1272 var instance = MxChatInstances.get(botId);
@@ -1758,12 +1351,26 @@
1758 1351 bgColor = botMessageBgColor;
1759 1352 fontColor = botMessageFontColor;
1760 1353 }
1761 1354
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);
1355 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1356 + // This prevents double-processing of URLs that are already formatted as HTML
1357 + var fullMessage;
1358 + if (sender === "user") {
1359 + // Always linkify user messages (they're plain text)
1360 + fullMessage = linkify(responseText);
1361 + } else {
1362 + // For bot/agent messages, check if HTML already exists
1363 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1364 + responseText.includes('<img') || responseText.includes('<div') ||
1365 + responseText.includes('<p>') || responseText.includes('<br>')) {
1366 + // Response already has HTML, don't process it
1367 + fullMessage = responseText;
1368 + } else {
1369 + // Plain text response, apply linkify
1370 + fullMessage = linkify(responseText);
1371 + }
1372 + }
1766 1373
1767 1374 if (responseHtml) {
1768 1375 // Only add line breaks if there's actual text content before the HTML
1769 1376 if (fullMessage && fullMessage.trim()) {
@@ -1820,12 +1427,8 @@
1820 1427 }
1821 1428
1822 1429 // Re-enable chat input after response is displayed
1823 1430 enableChatInput(botId);
1824 -
1825 - if (sender === "bot" || sender === "agent") {
1826 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1827 - }
1828 1431 } else {
1829 1432 appendMessage(sender, responseText, responseHtml, images, false, botId);
1830 1433 // Re-enable chat input after response is displayed
1831 1434 enableChatInput(botId);
@@ -1834,15 +1437,8 @@
1834 1437
1835 1438
1836 1439 function appendThinkingMessage(botId) {
1837 1440 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 1441 var $chatBox = getElement(botId, 'chat-box');
1846 1442
1847 1443 // Remove any existing thinking dots in this bot's chat first
1848 1444 $chatBox.find('.thinking-dots').remove();
@@ -1864,9 +1460,9 @@
1864 1460 '</div>' +
1865 1461 '</div>';
1866 1462
1867 1463 // 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 + ';"';
1464 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
1869 1465 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1870 1466 scrollToBottom(botId);
1871 1467 }
1872 1468
@@ -1872,11 +1468,9 @@
1872 1468
1873 1469 function removeThinkingDots(botId) {
1874 1470 botId = botId || 'default';
1875 1471 var $chatBox = getElement(botId, 'chat-box');
1876 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1877 1472 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1878 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1879 1473 }
1880 1474
1881 1475 // ====================================
1882 1476 // TEXT FORMATTING & PROCESSING
@@ -1910,12 +1504,9 @@
1910 1504 processedText = formatTextStyling(processedText);
1911 1505
1912 1506 // Process code blocks BEFORE processing links
1913 1507 processedText = formatCodeBlocks(processedText);
1914 -
1915 - // Process markdown tables BEFORE converting newlines to paragraphs
1916 - processedText = formatMarkdownTables(processedText);
1917 -
1508 +
1918 1509 // NOW convert to paragraphs
1919 1510 processedText = convertNewlinesToBreaks(processedText);
1920 1511
1921 1512 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -1928,63 +1519,37 @@
1928 1519 // Return as a proper link without the brackets
1929 1520 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1930 1521 });
1931 1522
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 - }
1523 + // Process proper markdown links with text: [text](url)
1524 + // This MUST have non-empty text in the first brackets
1525 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1526 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1527 + // Make sure we have actual text (not just whitespace)
1528 + if (!text || !text.trim()) {
1529 + // If no text, treat the URL as the text
1530 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1531 + const safeUrl = safeEncodeUrl(cleanUrl);
1532 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1983 1533 }
1984 - return result;
1985 - })(processedText);
1534 +
1535 + // Clean the URL
1536 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1537 + const safeUrl = safeEncodeUrl(cleanUrl);
1538 + const safeText = sanitizeUserInput(text);
1539 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1540 + });
1986 1541
1542 + // Handle empty markdown links: [](url)
1543 + // This is a specific case where there's no text
1544 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1545 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1546 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1547 + const safeUrl = safeEncodeUrl(cleanUrl);
1548 + // Use the URL itself as the link text
1549 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1550 + });
1551 +
1987 1552 // Process phone numbers: [text](tel:number)
1988 1553 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1989 1554 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1990 1555 const safePhone = safeEncodeUrl(phone);
@@ -2136,78 +1701,9 @@
2136 1701 });
2137 1702
2138 1703 return text;
2139 1704 }
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 -
1705 +
2210 1706 function sanitizeUserInput(text) {
2211 1707 const div = document.createElement('div');
2212 1708 div.textContent = text;
2213 1709 return div.innerHTML;
@@ -2278,14 +1774,13 @@
2278 1774 requestAnimationFrame(smoothScroll);
2279 1775 }
2280 1776 }
2281 1777
2282 - function scrollElementToTop(element, botId, topOffset) {
1778 + function scrollElementToTop(element, botId) {
2283 1779 botId = botId || 'default';
2284 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2285 1780 var chatBox = getElement(botId, 'chat-box');
2286 1781 var elementTop = element.position().top + chatBox.scrollTop();
2287 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1782 + chatBox.animate({ scrollTop: elementTop }, 500);
2288 1783 }
2289 1784
2290 1785 function showChatWidget(botId) {
2291 1786 botId = botId || 'default';
@@ -2429,12 +1924,15 @@
2429 1924 // LIVE AGENT FUNCTIONALITY
2430 1925 // ====================================
2431 1926
2432 1927 function startPolling(botId) {
1928 + console.log('[MxChat] startPolling called for botId:', botId);
2433 1929 botId = botId || 'default';
2434 1930 var instance = MxChatInstances.get(botId);
2435 1931 // Clear any existing interval first
2436 1932 stopPolling(botId);
1933 + // Start new polling interval
1934 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2437 1935 instance.pollingInterval = setInterval(function() {
2438 1936 checkForAgentMessages(botId);
2439 1937 }, 5000);
2440 1938 }
@@ -2439,17 +1937,20 @@
2439 1937 }, 5000);
2440 1938 }
2441 1939
2442 1940 function stopPolling(botId) {
1941 + console.log('[MxChat] stopPolling called for botId:', botId);
2443 1942 botId = botId || 'default';
2444 1943 var instance = MxChatInstances.get(botId);
2445 1944 if (instance.pollingInterval) {
2446 1945 clearInterval(instance.pollingInterval);
2447 1946 instance.pollingInterval = null;
1947 + console.log('[MxChat] Polling stopped for botId:', botId);
2448 1948 }
2449 1949 }
2450 1950
2451 1951 function checkForAgentMessages(botId) {
1952 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2452 1953 botId = botId || 'default';
2453 1954 var instance = MxChatInstances.get(botId);
2454 1955 const sessionId = getChatSession(botId);
2455 1956 $.ajax({
@@ -2475,12 +1976,8 @@
2475 1976 instance.processedMessageIds.add(message.id);
2476 1977 }
2477 1978 });
2478 1979
2479 - if (hasNewMessage) {
2480 - enableChatInput(botId);
2481 - }
2482 -
2483 1980 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2484 1981 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2485 1982 showNotification(botId);
2486 1983 }
@@ -2486,13 +1983,8 @@
2486 1983 }
2487 1984
2488 1985 scrollToBottom(botId, true);
2489 1986 }
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 1987 },
2496 1988 error: function (xhr, status, error) {
2497 1989 // Polling error - silently continue
2498 1990 }
@@ -2502,29 +1994,20 @@
2502 1994 // ====================================
2503 1995 // CHAT HISTORY & PERSISTENCE
2504 1996 // ====================================
2505 1997
2506 -function loadChatHistory(botId, onComplete) {
1998 +function loadChatHistory(botId) {
2507 1999 botId = botId || 'default';
2508 2000 var instance = MxChatInstances.get(botId);
2509 2001
2510 2002 // Prevent duplicate loading
2511 2003 if (instance.chatHistoryLoaded) {
2512 - if (onComplete) onComplete();
2513 2004 return;
2514 2005 }
2515 2006
2516 - // Use getChatSession which returns null if no session exists (does NOT create one)
2517 2007 var sessionId = getChatSession(botId);
2518 2008 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2519 2009
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 2010 if (chatPersistenceEnabled && sessionId) {
2528 2011 $.ajax({
2529 2012 url: mxchatChat.ajax_url,
2530 2013 type: 'POST',
@@ -2535,12 +2018,11 @@
2535 2018 },
2536 2019 success: function(response) {
2537 2020 // Handle session reset (IP changed while user was away)
2538 2021 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);
2022 + // Silently reset session - user will start fresh
2023 + resetChatSession(botId);
2541 2024 instance.chatHistoryLoaded = true; // Prevent retry loop
2542 - if (onComplete) onComplete();
2543 2025 return;
2544 2026 }
2545 2027
2546 2028 // Check if the response indicates success
@@ -2596,19 +2078,9 @@
2596 2078 var content = message.content;
2597 2079 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2598 2080 content = decodeHTMLEntities(content);
2599 2081
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")) {
2082 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2611 2083 messageElement.html(content);
2612 2084 } else {
2613 2085 var formattedContent = linkify(content);
2614 2086 messageElement.html(formattedContent);
@@ -2648,17 +2120,13 @@
2648 2120 instance.chatHistoryLoaded = true;
2649 2121 }
2650 2122 }
2651 2123 }
2652 - if (onComplete) onComplete();
2653 2124 },
2654 2125 error: function(xhr, status, error) {
2655 2126 // Error loading chat history - silently continue
2656 - if (onComplete) onComplete();
2657 2127 }
2658 2128 });
2659 - } else {
2660 - if (onComplete) onComplete();
2661 2129 }
2662 2130 }
2663 2131
2664 2132
@@ -2834,35 +2302,45 @@
2834 2302 // ====================================
2835 2303
2836 2304 function checkPreChatDismissal(botId) {
2837 2305 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) {
2306 + $.ajax({
2307 + url: mxchatChat.ajax_url,
2308 + type: 'POST',
2309 + data: {
2310 + action: 'mxchat_check_pre_chat_message_status',
2311 + _ajax_nonce: mxchatChat.nonce
2312 + },
2313 + success: function(response) {
2314 + if (response.success && !response.data.dismissed) {
2315 + getElement(botId, 'pre-chat-message').fadeIn(250);
2316 + } else {
2844 2317 getElement(botId, 'pre-chat-message').hide();
2845 - return;
2846 2318 }
2847 - // Expired — clear and show again
2848 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2319 + },
2320 + error: function() {
2321 + // Error checking pre-chat dismissal - silently continue
2849 2322 }
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 - }
2323 + });
2855 2324 }
2856 2325
2857 2326 function handlePreChatDismissal(botId) {
2858 2327 botId = botId || 'default';
2859 2328 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 - }
2329 + $.ajax({
2330 + url: mxchatChat.ajax_url,
2331 + type: 'POST',
2332 + data: {
2333 + action: 'mxchat_dismiss_pre_chat_message',
2334 + _ajax_nonce: mxchatChat.nonce
2335 + },
2336 + success: function() {
2337 + $('#pre-chat-message').hide();
2338 + },
2339 + error: function() {
2340 + // Error dismissing pre-chat message - silently continue
2341 + }
2342 + });
2865 2343 }
2866 2344
2867 2345
2868 2346 // ====================================
@@ -2929,26 +2407,8 @@
2929 2407 $(this).addClass('hidden');
2930 2408 $badge.hide(); // Hide notification when opening chat
2931 2409 disableScroll();
2932 2410 $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 2411 } else {
2952 2412 $chatbot.removeClass('visible').addClass('hidden');
2953 2413 $(this).removeClass('hidden');
2954 2414 enableScroll();
@@ -2966,9 +2426,11 @@
2966 2426
2967 2427 $(document).on('click', '.close-pre-chat-message', function(e) {
2968 2428 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2969 2429 var botId = getBotIdFromElement(this);
2970 - handlePreChatDismissal(botId);
2430 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2431 + $(this).remove();
2432 + });
2971 2433 });
2972 2434
2973 2435
2974 2436 // PDF upload button handlers - use class selector
@@ -3009,10 +2471,8 @@
3009 2471 const sendBtn = document.getElementById('send-button');
3010 2472 const originalBtnContent = uploadBtn.innerHTML;
3011 2473
3012 2474 try {
3013 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3014 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3015 2475 const formData = new FormData();
3016 2476 formData.append('action', 'mxchat_upload_pdf');
3017 2477 formData.append('pdf_file', file);
3018 2478 formData.append('session_id', sessionId);
@@ -3076,10 +2536,8 @@
3076 2536 const sendBtn = document.getElementById('send-button');
3077 2537 const originalBtnContent = uploadBtn.innerHTML;
3078 2538
3079 2539 try {
3080 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3081 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3082 2540 const formData = new FormData();
3083 2541 formData.append('action', 'mxchat_upload_word');
3084 2542 formData.append('word_file', file);
3085 2543 formData.append('session_id', sessionId);
@@ -3173,437 +2631,380 @@
3173 2631 });
3174 2632
3175 2633
3176 2634 // ====================================
3177 -// INIT LOADER & CHAT CONTAINER HELPERS
2635 +// EMAIL COLLECTION SETUP - FIXED VERSION
3178 2636 // ====================================
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 -// EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3229 -// ====================================
3230 2637 // Only run email collection setup if it's enabled
3231 2638 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2639 + // Email collection form setup and handlers
2640 + const emailForm = document.getElementById('email-collection-form');
2641 + const emailBlocker = document.getElementById('email-blocker');
2642 + const chatbotWrapper = document.getElementById('chat-container');
3232 2643
3233 - // Track submitting state per bot
3234 - const emailSubmittingState = {};
2644 + if (emailForm && emailBlocker && chatbotWrapper) {
2645 +
2646 + // Add loading state management
2647 + let isSubmitting = false;
2648 +
2649 + // Optimized UI transition functions
2650 + function showEmailForm() {
2651 + emailBlocker.style.display = 'flex';
2652 + chatbotWrapper.style.display = 'none';
2653 + }
3235 2654
3236 - // Add CSS animations for email form (once globally)
3237 - if (!document.getElementById('email-error-styles')) {
3238 - const style = document.createElement('style');
3239 - style.id = 'email-error-styles';
3240 - style.textContent = `
3241 - @keyframes fadeInError {
3242 - from { opacity: 0; transform: translateY(-5px); }
3243 - to { opacity: 1; transform: translateY(0); }
2655 + function showChatContainer() {
2656 + // Show chat immediately without delay
2657 + emailBlocker.style.display = 'none';
2658 + chatbotWrapper.style.display = 'flex';
2659 +
2660 + // Load chat history only after showing chat container
2661 + if (typeof loadChatHistory === 'function') {
2662 + loadChatHistory();
3244 2663 }
3245 - .email-input-shake {
3246 - animation: shake 0.5s ease-in-out;
3247 - }
3248 - @keyframes shake {
3249 - 0%, 100% { transform: translateX(0); }
3250 - 25% { transform: translateX(-5px); }
3251 - 75% { transform: translateX(5px); }
3252 - }
3253 - @keyframes spin {
3254 - from { transform: rotate(0deg); }
3255 - to { transform: rotate(360deg); }
3256 - }
3257 - .email-spinner {
3258 - display: inline-block;
3259 - vertical-align: middle;
3260 - }
3261 - `;
3262 - document.head.appendChild(style);
3263 - }
2664 + }
3264 2665
3265 - function isValidEmailAddress(email) {
3266 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3267 - return emailRegex.test(email.trim()) && email.length <= 254;
3268 - }
2666 + // Enhanced email validation
2667 + function isValidEmail(email) {
2668 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2669 + return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2670 + }
3269 2671
3270 - function isValidNameInput(name) {
3271 - return name && name.trim().length >= 2 && name.trim().length <= 100;
3272 - }
2672 + // Enhanced name validation
2673 + function isValidName(name) {
2674 + return name && name.trim().length >= 2 && name.trim().length <= 100;
2675 + }
3273 2676
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();
2677 + // Show loading state with spinner
2678 + function setSubmissionState(loading) {
2679 + const submitButton = document.getElementById('email-submit-button');
2680 + const emailInput = document.getElementById('user-email');
2681 + const nameInput = document.getElementById('user-name');
2682 +
2683 + if (loading) {
2684 + isSubmitting = true;
2685 + if (submitButton) submitButton.disabled = true;
2686 + if (emailInput) emailInput.disabled = true;
2687 + if (nameInput) nameInput.disabled = true;
2688 +
2689 + // Store original content and add spinner
2690 + if (submitButton && !submitButton.getAttribute('data-original-html')) {
2691 + submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2692 +
2693 + // Add loading spinner while keeping original text
2694 + const originalText = submitButton.textContent;
2695 + submitButton.innerHTML = `
2696 + <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2697 + <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2698 + <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2699 + <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2700 + </circle>
2701 + </svg>
2702 + ${originalText}
2703 + `;
2704 +
2705 + submitButton.style.opacity = '0.8';
2706 + }
2707 + } else {
2708 + isSubmitting = false;
2709 + if (submitButton) submitButton.disabled = false;
2710 + if (emailInput) emailInput.disabled = false;
2711 + if (nameInput) nameInput.disabled = false;
2712 +
2713 + // Restore original content
2714 + if (submitButton) {
2715 + const originalHtml = submitButton.getAttribute('data-original-html');
2716 + if (originalHtml) {
2717 + submitButton.innerHTML = originalHtml;
2718 + }
2719 + submitButton.style.opacity = '1';
2720 + }
2721 + }
3302 2722 }
3303 2723
3304 - messageContent.innerHTML = html;
3305 - }
3306 -
3307 - function setEmailSubmissionState(botId, loading) {
3308 - var submitButton = getElementDOM(botId, 'email-submit-button');
3309 - var emailInput = getElementDOM(botId, 'user-email');
3310 - var nameInput = getElementDOM(botId, 'user-name');
3311 -
3312 - if (loading) {
3313 - emailSubmittingState[botId] = true;
3314 - if (submitButton) submitButton.disabled = true;
3315 - if (emailInput) emailInput.disabled = true;
3316 - if (nameInput) nameInput.disabled = true;
3317 -
3318 - if (submitButton && !submitButton.getAttribute('data-original-html')) {
3319 - submitButton.setAttribute('data-original-html', submitButton.innerHTML);
3320 - const originalText = submitButton.textContent;
3321 - submitButton.innerHTML = `
3322 - <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
3323 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
3324 - <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
3325 - <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
3326 - </circle>
3327 - </svg>
3328 - ${originalText}
2724 + // Error display functions
2725 + function showEmailError(message) {
2726 + clearEmailError();
2727 +
2728 + const errorDiv = document.createElement('div');
2729 + errorDiv.className = 'email-error';
2730 + errorDiv.style.cssText = `
2731 + color: #e74c3c;
2732 + font-size: 12px;
2733 + margin-top: 8px;
2734 + padding: 4px 0;
2735 + animation: fadeInError 0.3s ease;
2736 + `;
2737 + errorDiv.textContent = message;
2738 +
2739 + // Add CSS animation if not already present
2740 + if (!document.getElementById('email-error-styles')) {
2741 + const style = document.createElement('style');
2742 + style.id = 'email-error-styles';
2743 + style.textContent = `
2744 + @keyframes fadeInError {
2745 + from { opacity: 0; transform: translateY(-5px); }
2746 + to { opacity: 1; transform: translateY(0); }
2747 + }
2748 + .email-input-shake {
2749 + animation: shake 0.5s ease-in-out;
2750 + }
2751 + @keyframes shake {
2752 + 0%, 100% { transform: translateX(0); }
2753 + 25% { transform: translateX(-5px); }
2754 + 75% { transform: translateX(5px); }
2755 + }
2756 + @keyframes spin {
2757 + from { transform: rotate(0deg); }
2758 + to { transform: rotate(360deg); }
2759 + }
2760 + .email-spinner {
2761 + display: inline-block;
2762 + vertical-align: middle;
2763 + }
3329 2764 `;
3330 - submitButton.style.opacity = '0.8';
2765 + document.head.appendChild(style);
3331 2766 }
3332 - } else {
3333 - emailSubmittingState[botId] = false;
3334 - if (submitButton) submitButton.disabled = false;
3335 - if (emailInput) emailInput.disabled = false;
3336 - if (nameInput) nameInput.disabled = false;
3337 -
3338 - if (submitButton) {
3339 - const originalHtml = submitButton.getAttribute('data-original-html');
3340 - if (originalHtml) {
3341 - submitButton.innerHTML = originalHtml;
3342 - }
3343 - submitButton.style.opacity = '1';
2767 +
2768 + emailForm.appendChild(errorDiv);
2769 +
2770 + // Add shake animation to inputs
2771 + const emailInput = document.getElementById('user-email');
2772 + const nameInput = document.getElementById('user-name');
2773 +
2774 + if (emailInput) {
2775 + emailInput.classList.add('email-input-shake');
2776 + setTimeout(() => {
2777 + emailInput.classList.remove('email-input-shake');
2778 + }, 500);
3344 2779 }
2780 +
2781 + if (nameInput) {
2782 + nameInput.classList.add('email-input-shake');
2783 + setTimeout(() => {
2784 + nameInput.classList.remove('email-input-shake');
2785 + }, 500);
2786 + }
3345 2787 }
3346 - }
3347 2788
3348 - function showEmailError(botId, message) {
3349 - clearEmailError(botId);
3350 -
3351 - var emailForm = getElementDOM(botId, 'email-collection-form');
3352 - if (!emailForm) return;
3353 -
3354 - const errorDiv = document.createElement('div');
3355 - errorDiv.className = 'email-error';
3356 - errorDiv.style.cssText = `
3357 - color: #e74c3c;
3358 - font-size: 12px;
3359 - margin-top: 8px;
3360 - padding: 4px 0;
3361 - animation: fadeInError 0.3s ease;
3362 - `;
3363 - errorDiv.textContent = message;
3364 - emailForm.appendChild(errorDiv);
3365 -
3366 - // Add shake animation to inputs
3367 - var emailInput = getElementDOM(botId, 'user-email');
3368 - var nameInput = getElementDOM(botId, 'user-name');
3369 -
3370 - if (emailInput) {
3371 - emailInput.classList.add('email-input-shake');
3372 - setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
3373 - }
3374 - if (nameInput) {
3375 - nameInput.classList.add('email-input-shake');
3376 - setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
3377 - }
3378 - }
3379 -
3380 - function clearEmailError(botId) {
3381 - var emailForm = getElementDOM(botId, 'email-collection-form');
3382 - if (emailForm) {
2789 + function clearEmailError() {
3383 2790 const existingErrors = emailForm.querySelectorAll('.email-error');
3384 2791 existingErrors.forEach(error => error.remove());
3385 2792 }
3386 - }
3387 2793
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 - }
2794 + // MAIN FORM SUBMIT HANDLER
2795 + // Remove any existing event listeners first
2796 + emailForm.removeEventListener('submit', handleFormSubmit);
3400 2797
3401 - function checkSessionAndEmailForBot(botId) {
3402 - const sessionId = MxChatInstances.ensureSession(botId);
2798 + // Add the form submit handler
2799 + emailForm.addEventListener('submit', handleFormSubmit);
3403 2800
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);
2801 + function handleFormSubmit(event) {
2802 + event.preventDefault();
2803 + event.stopPropagation();
3410 2804
3411 - fetch(mxchatChat.ajax_url, {
3412 - method: 'POST',
3413 - headers: {
3414 - 'Content-Type': 'application/x-www-form-urlencoded',
3415 - },
3416 - body: new URLSearchParams({
3417 - action: 'mxchat_check_email_provided',
3418 - session_id: sessionId,
3419 - nonce: mxchatChat.nonce,
3420 - })
3421 - })
3422 - .then((response) => {
3423 - if (!response.ok) {
3424 - throw new Error(`HTTP error! status: ${response.status}`);
2805 + // Prevent double submission
2806 + if (isSubmitting) {
2807 + return false;
3425 2808 }
3426 - return response.json();
3427 - })
3428 - .then((data) => {
3429 - if (data.success) {
3430 - if (data.data.logged_in || data.data.email) {
3431 - showChatContainerForBot(botId);
3432 - } else {
3433 - showEmailFormForBot(botId);
3434 - }
3435 - } else {
3436 - showEmailFormForBot(botId);
3437 - }
3438 - })
3439 - .catch((error) => {
3440 - showEmailFormForBot(botId);
3441 - });
3442 - }
3443 2809
3444 - // Event delegation for email form submission
3445 - $(document).on('submit', '.email-collection-form', function(e) {
3446 - e.preventDefault();
3447 - e.stopPropagation();
2810 + const userEmail = document.getElementById('user-email').value.trim();
2811 + const nameInput = document.getElementById('user-name');
2812 + const userName = nameInput ? nameInput.value.trim() : '';
2813 + const sessionId = getChatSession();
3448 2814
3449 - var botId = getBotIdFromElement(this);
2815 + // Validate email before submission
2816 + if (!userEmail) {
2817 + showEmailError('Please enter your email address.');
2818 + return false;
2819 + }
3450 2820
3451 - // Prevent double submission
3452 - if (emailSubmittingState[botId]) {
3453 - return false;
3454 - }
2821 + if (!isValidEmail(userEmail)) {
2822 + showEmailError('Please enter a valid email address.');
2823 + return false;
2824 + }
3455 2825
3456 - var emailInput = getElementDOM(botId, 'user-email');
3457 - var nameInput = getElementDOM(botId, 'user-name');
3458 - var userEmail = emailInput ? emailInput.value.trim() : '';
3459 - var userName = nameInput ? nameInput.value.trim() : '';
3460 - var sessionId = MxChatInstances.ensureSession(botId);
2826 + // Validate name if field exists
2827 + if (nameInput && !isValidName(userName)) {
2828 + showEmailError('Please enter a valid name (2-100 characters).');
2829 + return false;
2830 + }
3461 2831
3462 - // Validate email
3463 - if (!userEmail) {
3464 - showEmailError(botId, 'Please enter your email address.');
3465 - return false;
3466 - }
2832 + // Clear any existing errors
2833 + clearEmailError();
2834 + setSubmissionState(true);
3467 2835
3468 - if (!isValidEmailAddress(userEmail)) {
3469 - showEmailError(botId, 'Please enter a valid email address.');
3470 - return false;
3471 - }
2836 + // Prepare form data with optional name
2837 + const formData = new URLSearchParams({
2838 + action: 'mxchat_handle_save_email_and_response',
2839 + email: userEmail,
2840 + session_id: sessionId,
2841 + nonce: mxchatChat.nonce,
2842 + });
3472 2843
3473 - // Validate name if field exists and has content
3474 - if (nameInput && userName && !isValidNameInput(userName)) {
3475 - showEmailError(botId, 'Please enter a valid name (2-100 characters).');
3476 - return false;
3477 - }
2844 + // Add name to form data if provided
2845 + if (userName) {
2846 + formData.append('name', userName);
2847 + }
3478 2848
3479 - clearEmailError(botId);
3480 - setEmailSubmissionState(botId, true);
2849 + fetch(mxchatChat.ajax_url, {
2850 + method: 'POST',
2851 + headers: {
2852 + 'Content-Type': 'application/x-www-form-urlencoded',
2853 + },
2854 + body: formData
2855 + })
2856 + .then((response) => {
2857 + if (!response.ok) {
2858 + throw new Error(`HTTP error! status: ${response.status}`);
2859 + }
2860 + return response.json();
2861 + })
2862 + .then((data) => {
2863 + setSubmissionState(false);
3481 2864
3482 - // Prepare form data
3483 - const formData = new URLSearchParams({
3484 - action: 'mxchat_handle_save_email_and_response',
3485 - email: userEmail,
3486 - session_id: sessionId,
3487 - nonce: mxchatChat.nonce,
3488 - });
2865 + if (data.success) {
2866 + // Show chat immediately
2867 + showChatContainer();
3489 2868
3490 - if (userName) {
3491 - formData.append('name', userName);
2869 + // Handle bot response if provided
2870 + if (data.message && typeof appendMessage === 'function') {
2871 + setTimeout(() => {
2872 + appendMessage('bot', data.message);
2873 + if (typeof scrollToBottom === 'function') {
2874 + scrollToBottom();
2875 + }
2876 + }, 100);
2877 + }
2878 + } else {
2879 + showEmailError(data.message || 'Failed to save email. Please try again.');
2880 + }
2881 + })
2882 + .catch((error) => {
2883 + setSubmissionState(false);
2884 + showEmailError('An error occurred. Please try again.');
2885 + });
2886 +
2887 + return false; // Extra prevention
3492 2888 }
3493 2889
3494 - fetch(mxchatChat.ajax_url, {
3495 - method: 'POST',
3496 - headers: {
3497 - 'Content-Type': 'application/x-www-form-urlencoded',
3498 - },
3499 - body: formData
3500 - })
3501 - .then((response) => {
3502 - if (!response.ok) {
3503 - throw new Error(`HTTP error! status: ${response.status}`);
3504 - }
3505 - return response.json();
3506 - })
3507 - .then((data) => {
3508 - setEmailSubmissionState(botId, false);
2890 + // Real-time email validation
2891 + const emailInput = document.getElementById('user-email');
2892 + if (emailInput) {
2893 + let validationTimeout;
2894 +
2895 + emailInput.addEventListener('input', function() {
2896 + // Clear previous validation timeout
2897 + if (validationTimeout) {
2898 + clearTimeout(validationTimeout);
2899 + }
2900 +
2901 + // Debounce validation
2902 + validationTimeout = setTimeout(() => {
2903 + const email = this.value.trim();
2904 + clearEmailError();
2905 +
2906 + if (email && !isValidEmail(email)) {
2907 + showEmailError('Please enter a valid email address.');
2908 + }
2909 + }, 500);
2910 + });
3509 2911
3510 - if (data.success) {
3511 - showChatContainerForBot(botId);
2912 + // Handle Enter key
2913 + emailInput.addEventListener('keypress', function(e) {
2914 + if (e.key === 'Enter' && !isSubmitting) {
2915 + e.preventDefault();
2916 + emailForm.dispatchEvent(new Event('submit'));
2917 + }
2918 + });
2919 + }
3512 2920
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, '');
2921 + // Real-time name validation
2922 + const nameInput = document.getElementById('user-name');
2923 + if (nameInput) {
2924 + let nameValidationTimeout;
2925 +
2926 + nameInput.addEventListener('input', function() {
2927 + // Clear previous validation timeout
2928 + if (nameValidationTimeout) {
2929 + clearTimeout(nameValidationTimeout);
3519 2930 }
2931 +
2932 + // Debounce validation
2933 + nameValidationTimeout = setTimeout(() => {
2934 + const name = this.value.trim();
2935 + clearEmailError();
2936 +
2937 + if (name && !isValidName(name)) {
2938 + showEmailError('Name must be between 2 and 100 characters.');
2939 + }
2940 + }, 500);
2941 + });
3520 2942
3521 - if (data.message && typeof appendMessage === 'function') {
3522 - setTimeout(() => {
3523 - appendMessage('bot', data.message, '', [], false, botId);
3524 - if (typeof scrollToBottom === 'function') {
3525 - scrollToBottom(botId);
3526 - }
3527 - }, 100);
2943 + // Handle Enter key
2944 + nameInput.addEventListener('keypress', function(e) {
2945 + if (e.key === 'Enter' && !isSubmitting) {
2946 + e.preventDefault();
2947 + emailForm.dispatchEvent(new Event('submit'));
3528 2948 }
2949 + });
2950 + }
2951 +
2952 + // Initial state check
2953 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2954 + const emailState = mxchatChat.initial_email_state;
2955 + if (emailState.show_email_form) {
2956 + showEmailForm();
3529 2957 } else {
3530 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
2958 + showChatContainer();
3531 2959 }
3532 - })
3533 - .catch((error) => {
3534 - setEmailSubmissionState(botId, false);
3535 - showEmailError(botId, 'An error occurred. Please try again.');
3536 - });
3537 -
3538 - return false;
3539 - });
3540 -
3541 - // Real-time email validation using event delegation
3542 - $(document).on('input', '.mxchat-email-input', function() {
3543 - var botId = getBotIdFromElement(this);
3544 - var $input = $(this);
3545 -
3546 - // Clear previous timeout
3547 - clearTimeout($input.data('validationTimeout'));
3548 -
3549 - // Debounce validation
3550 - var timeout = setTimeout(() => {
3551 - var email = this.value.trim();
3552 - clearEmailError(botId);
3553 -
3554 - if (email && !isValidEmailAddress(email)) {
3555 - showEmailError(botId, 'Please enter a valid email address.');
3556 - }
3557 - }, 500);
3558 -
3559 - $input.data('validationTimeout', timeout);
3560 - });
3561 -
3562 - // Handle Enter key in email input
3563 - $(document).on('keypress', '.mxchat-email-input', function(e) {
3564 - if (e.key === 'Enter') {
3565 - e.preventDefault();
3566 - var botId = getBotIdFromElement(this);
3567 - if (!emailSubmittingState[botId]) {
3568 - $(this).closest('.email-collection-form').submit();
3569 - }
2960 + } else {
2961 + // Check email status via AJAX
2962 + setTimeout(checkSessionAndEmail, 100);
3570 2963 }
3571 - });
3572 2964
3573 - // Handle Enter key in name input
3574 - $(document).on('keypress', '.mxchat-name-input', function(e) {
3575 - if (e.key === 'Enter') {
3576 - e.preventDefault();
3577 - var botId = getBotIdFromElement(this);
3578 - if (!emailSubmittingState[botId]) {
3579 - $(this).closest('.email-collection-form').submit();
3580 - }
2965 + // Check if email exists for the current session
2966 + function checkSessionAndEmail() {
2967 + const sessionId = getChatSession();
2968 +
2969 + fetch(mxchatChat.ajax_url, {
2970 + method: 'POST',
2971 + headers: {
2972 + 'Content-Type': 'application/x-www-form-urlencoded',
2973 + },
2974 + body: new URLSearchParams({
2975 + action: 'mxchat_check_email_provided',
2976 + session_id: sessionId,
2977 + nonce: mxchatChat.nonce,
2978 + })
2979 + })
2980 + .then((response) => {
2981 + if (!response.ok) {
2982 + throw new Error(`HTTP error! status: ${response.status}`);
2983 + }
2984 + return response.json();
2985 + })
2986 + .then((data) => {
2987 + if (data.success) {
2988 + if (data.data.logged_in || data.data.email) {
2989 + showChatContainer();
2990 + } else {
2991 + showEmailForm();
2992 + }
2993 + } else {
2994 + // On error, default to showing email form
2995 + showEmailForm();
2996 + }
2997 + })
2998 + .catch((error) => {
2999 + // Email check failed - default to email form
3000 + showEmailForm();
3001 + });
3581 3002 }
3582 - });
3583 3003
3584 - // 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 - $('.mxchat-chatbot-wrapper').each(function() {
3588 - var botId = $(this).data('bot-id') || 'default';
3589 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3590 -
3591 - if (emailBlocker) {
3592 - if (isEmbeddedBot(botId)) {
3593 - // Embedded bots are always visible — check now
3594 - resolveEmailState(botId);
3595 - }
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 - }
3605 - });
3004 + } else {
3005 + // Email collection is enabled but essential elements are missing - silently continue
3006 + }
3606 3007 }
3607 3008
3608 3009 // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
3609 3010 $(document).on('click', '.pre-chat-message', function() {
@@ -3611,32 +3012,39 @@
3611 3012 var $chatbot = getElement(botId, 'floating-chatbot');
3612 3013 if ($chatbot.hasClass('hidden')) {
3613 3014 $chatbot.removeClass('hidden').addClass('visible');
3614 3015 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3615 - handlePreChatDismissal(botId);
3016 + $(this).fadeOut(250); // Hide pre-chat message
3616 3017 disableScroll(); // Disable scroll when chatbot opens
3018 + }
3019 + });
3617 3020
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 - }
3021 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
3022 + // This is a fallback for legacy support
3023 + $(document).on('click', '.close-pre-chat-message', function() {
3024 + var botId = getBotIdFromElement(this);
3025 + var $preChat = getElement(botId, 'pre-chat-message');
3026 + $preChat.fadeOut(200); // Hide the message
3623 3027
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);
3028 + // Send an AJAX request to set the transient flag for 24 hours
3029 + $.ajax({
3030 + url: mxchatChat.ajax_url,
3031 + type: 'POST',
3032 + data: {
3033 + action: 'mxchat_dismiss_pre_chat_message',
3034 + _ajax_nonce: mxchatChat.nonce
3035 + },
3036 + success: function() {
3037 + // Ensure the message is hidden after dismissal
3038 + $preChat.hide();
3039 + },
3040 + error: function() {
3041 + // Error dismissing pre-chat message - silently continue
3632 3042 }
3633 - }
3043 + });
3634 3044 });
3635 3045
3636 - // Legacy duplicate close handler removed — handled by single event delegation above
3637 3046
3638 -
3639 3047 function hasQuickQuestions(botId) {
3640 3048 botId = botId || 'default';
3641 3049 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3642 3050 if (!questionsContainer) return false;
@@ -3772,11 +3180,18 @@
3772 3180 });
3773 3181
3774 3182 // Initialize when document is ready
3775 3183 setFullHeight();
3184 + trackOriginatingPage();
3776 3185
3777 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3778 - // until the user's first interaction via MxChatInstances.ensureSession()
3186 + // Only load chat history if email collection is disabled
3187 + if (mxchatChat.email_collection_enabled !== 'on') {
3188 + // Load history for all instances
3189 + $('.mxchat-chatbot-wrapper').each(function() {
3190 + var botId = $(this).data('bot-id') || 'default';
3191 + loadChatHistory(botId);
3192 + });
3193 + }
3779 3194
3780 3195 // Initialize chat visibility for all instances
3781 3196 $('.mxchat-chatbot-wrapper').each(function() {
3782 3197 var botId = $(this).data('bot-id') || 'default';
@@ -3829,265 +3244,6 @@
3829 3244 }, 2000);
3830 3245 });
3831 3246 }
3832 3247 }
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 3248 });
4093 3249