PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.4
MxChat – AI Chatbot & Content Generation for WordPress v3.1.4
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 +184 -919 3.2.73.1.4 View file →
@@ -1,111 +1,23 @@
1 1 jQuery(document).ready(function($) {
2 2
3 - // Nonce refresh — v2 (plan-6a68c9).
4 - //
5 - // The widget no longer relies on a nonce embedded in inline cached HTML.
6 - // Before each chat-send / stream-send / upload, we call the REST endpoint
7 - // GET /wp-json/mxchat/v1/nonce and use the freshly-issued value. The
8 - // endpoint creates the nonce with action `mxchat_chat_send`; the server-side
9 - // verifier ALSO still accepts the legacy `mxchat_chat_nonce` action for a
10 - // 30-day backwards-compat window so cached pages still in users' browsers
11 - // (which carry the legacy inline-localized nonce) keep working.
12 - //
13 - // Cache: a single module-scoped slot. TTL 12h conservatively (WP nonces are
14 - // 24h but we refetch at half-life so a freshly-cached-page user never sees
15 - // a borderline-stale nonce).
16 - var cachedFreshNonce = null;
17 - var cachedFreshNonceFetchedAt = 0;
18 - var NONCE_TTL_MS = 12 * 60 * 60 * 1000;
19 - var nonceRefreshState = 'idle'; // 'idle' | 'pending' | 'done'
20 - var nonceRefreshCallbacks = [];
21 -
22 - function getRestNonceUrl() {
23 - if (typeof mxchatChat !== 'undefined' && mxchatChat.rest_url) {
24 - return mxchatChat.rest_url.replace(/\/+$/, '') + '/nonce';
25 - }
26 - // Fallback: derive from current origin if mxchatChat.rest_url isn't set.
27 - return window.location.origin + '/wp-json/mxchat/v1/nonce';
28 - }
29 -
30 - function fetchFreshNonceFromRest() {
31 - return fetch(getRestNonceUrl(), {
32 - credentials: 'same-origin',
33 - headers: { 'Accept': 'application/json' }
34 - }).then(function (resp) {
35 - if (!resp.ok) {
36 - throw new Error('REST nonce fetch failed: ' + resp.status);
37 - }
38 - return resp.json();
39 - }).then(function (data) {
40 - if (data && data.nonce) {
41 - return data.nonce;
42 - }
43 - throw new Error('REST nonce response had no nonce field.');
44 - });
45 - }
46 -
47 - /**
48 - * withFreshNonce(cb) — invoke cb() after ensuring mxchatChat.nonce is fresh.
49 - * Tries REST endpoint first (cache-bypass design); falls back to the legacy
50 - * admin-ajax refresh path if REST is unavailable. Idempotent — concurrent
51 - * calls share the same in-flight refresh.
52 - */
53 - function withFreshNonce(callback) {
54 - if (typeof mxchatChat === 'undefined') {
3 + // Nonce refresh is deferred until first user interaction (ensureSession)
4 + // to avoid admin-ajax calls on passive page loads.
5 + var nonceRefreshed = false;
6 + function refreshNonceIfNeeded(callback) {
7 + if (nonceRefreshed || typeof mxchatChat === 'undefined' || !mxchatChat.ajax_url) {
55 8 if (callback) callback();
56 9 return;
57 10 }
58 - var now = Date.now();
59 - if (cachedFreshNonce && (now - cachedFreshNonceFetchedAt) < NONCE_TTL_MS) {
60 - mxchatChat.nonce = cachedFreshNonce;
11 + nonceRefreshed = true;
12 + $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' }, function(res) {
13 + if (res && res.success && res.data && res.data.nonce) {
14 + mxchatChat.nonce = res.data.nonce;
15 + }
61 16 if (callback) callback();
62 - return;
63 - }
64 - if (callback) nonceRefreshCallbacks.push(callback);
65 - if (nonceRefreshState === 'pending') return;
66 - nonceRefreshState = 'pending';
67 -
68 - var resolved = function (nonce) {
69 - if (nonce) {
70 - cachedFreshNonce = nonce;
71 - cachedFreshNonceFetchedAt = Date.now();
72 - mxchatChat.nonce = nonce;
73 - }
74 - nonceRefreshState = 'done';
75 - var pending = nonceRefreshCallbacks;
76 - nonceRefreshCallbacks = [];
77 - pending.forEach(function (cb) { try { cb(); } catch (e) {} });
78 - };
79 -
80 - fetchFreshNonceFromRest()
81 - .then(resolved)
82 - .catch(function () {
83 - // Fallback to the legacy admin-ajax refresh path (issued with the
84 - // old action `mxchat_chat_nonce`; the server still accepts both
85 - // during the compat window).
86 - if (mxchatChat.ajax_url) {
87 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' })
88 - .done(function (res) {
89 - if (res && res.success && res.data && res.data.nonce) {
90 - resolved(res.data.nonce);
91 - return;
92 - }
93 - resolved(null);
94 - })
95 - .fail(function () { resolved(null); });
96 - } else {
97 - resolved(null);
98 - }
99 - });
17 + });
100 18 }
101 19
102 - // Backwards-compat alias — every existing caller in this file (and any
103 - // out-of-tree consumer that hit this internal API) keeps working unchanged.
104 - function refreshNonceIfNeeded(callback) {
105 - return withFreshNonce(callback);
106 - }
107 -
108 20 // ====================================
109 21 // MULTI-INSTANCE MANAGEMENT SYSTEM
110 22 // ====================================
111 23
@@ -148,39 +60,18 @@
148 60 return Object.keys(this.instances);
149 61 },
150 62
151 63 // Session management per bot
152 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
153 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
154 64 getChatSession: function(botId) {
155 65 var cookieName = 'mxchat_session_id_' + botId;
156 - var storageKey = 'mxchat_session_id_' + botId;
157 66 var sessionId = getCookie(cookieName);
158 67
159 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
160 68 if (!sessionId) {
161 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
69 + sessionId = generateSessionId();
70 + this.setChatSession(botId, sessionId);
162 71 }
163 72
164 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
165 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
166 - // storage). Without this, ensureSession() can generate and store an ID that
167 - // getChatSession() then can't read back, causing null session_ids on send.
168 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
169 - sessionId = this.instances[botId].sessionId;
170 - }
171 -
172 - // Guard against stored sentinel values that indicate earlier broken writes.
173 - if (sessionId === 'null' || sessionId === 'undefined') {
174 - sessionId = null;
175 - }
176 -
177 - // Re-sync cookie from localStorage if cookie was lost
178 - if (sessionId && !getCookie(cookieName)) {
179 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
180 - }
181 -
182 - return sessionId || null;
73 + return sessionId;
183 74 },
184 75
185 76 // Lazy session initializer — called on first user interaction
186 77 ensureSession: function(botId) {
@@ -190,10 +81,11 @@
190 81 if (instance.sessionId) {
191 82 return instance.sessionId;
192 83 }
193 84
194 - // Check for existing session from cookie or localStorage
195 - var existingSession = this.getChatSession(botId);
85 + // Check if a cookie already exists from a prior visit
86 + var cookieName = 'mxchat_session_id_' + botId;
87 + var existingSession = getCookie(cookieName);
196 88
197 89 if (existingSession) {
198 90 instance.sessionId = existingSession;
199 91 } else {
@@ -206,10 +98,12 @@
206 98 // Now that we have a session, do the deferred work
207 99 refreshNonceIfNeeded();
208 100 trackOriginatingPage();
209 101
210 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
211 - // so we do NOT call it here to avoid a race condition.
102 + var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
103 + if (chatPersistenceEnabled && mxchatChat.email_collection_enabled !== 'on') {
104 + loadChatHistory(botId);
105 + }
212 106
213 107 return instance.sessionId;
214 108 },
215 109
@@ -214,11 +108,9 @@
214 108 },
215 109
216 110 setChatSession: function(botId, sessionId) {
217 111 var cookieName = 'mxchat_session_id_' + botId;
218 - var storageKey = 'mxchat_session_id_' + botId;
219 112 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
220 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
221 113 if (this.instances[botId]) {
222 114 this.instances[botId].sessionId = sessionId;
223 115 }
224 116 },
@@ -223,10 +115,8 @@
223 115 }
224 116 },
225 117
226 118 resetChatSession: function(botId) {
227 - // Clear old session from localStorage before setting new one
228 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
229 119 var newSessionId = generateSessionId();
230 120 this.setChatSession(botId, newSessionId);
231 121 var $chatBox = getElement(botId, 'chat-box');
232 122 if ($chatBox.length) {
@@ -235,20 +125,8 @@
235 125 if (this.instances[botId]) {
236 126 this.instances[botId].chatHistoryLoaded = false;
237 127 this.instances[botId].processedMessageIds = new Set();
238 128 }
239 - },
240 -
241 - // Silent reset — new session ID without clearing the chat UI
242 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
243 - silentResetSession: function(botId) {
244 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
245 - var newSessionId = generateSessionId();
246 - this.setChatSession(botId, newSessionId);
247 - if (this.instances[botId]) {
248 - this.instances[botId].sessionId = newSessionId;
249 - }
250 - return newSessionId;
251 129 }
252 130 };
253 131
254 132 // ====================================
@@ -700,28 +578,13 @@
700 578
701 579 // Get instance for session start timestamp (used when persistence is OFF)
702 580 var instance = MxChatInstances.get(botId);
703 581
704 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
705 - // and returns the guaranteed-present session id from the in-memory instance even when
706 - // cookie/localStorage writes are silently blocked by the browser.
707 - var sessionId = MxChatInstances.ensureSession(botId);
708 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
709 - // Last-resort generation to ensure we never POST a null marker.
710 - sessionId = generateSessionId();
711 - MxChatInstances.setChatSession(botId, sessionId);
712 - }
713 -
714 - // Wait for the page-cache nonce refresh to complete before firing the
715 - // chat-send AJAX. On cached pages the inline mxchatChat.nonce is stale
716 - // until refreshNonceIfNeeded() returns; constructing ajaxData inside the
717 - // callback guarantees we read the fresh value. See plan-c5457f.
718 - refreshNonceIfNeeded(function() {
719 582 // Prepare AJAX data
720 583 const ajaxData = {
721 584 action: 'mxchat_handle_chat_request',
722 585 message: message,
723 - session_id: sessionId,
586 + session_id: getChatSession(botId),
724 587 nonce: mxchatChat.nonce,
725 588 current_page_url: window.location.href,
726 589 current_page_title: document.title,
727 590 bot_id: botId,
@@ -727,14 +590,14 @@
727 590 bot_id: botId,
728 591 // Pass session start timestamp so AI context matches what user sees
729 592 session_start_timestamp: instance.sessionStartTimestamp || 0
730 593 };
731 -
594 +
732 595 // Add page context if available
733 596 if (pageContext) {
734 597 ajaxData.page_context = JSON.stringify(pageContext);
735 598 }
736 -
599 +
737 600 // CHECK FOR VISION FLAGS AND ADD THEM
738 601 if (window.mxchatVisionProcessed) {
739 602 ajaxData.vision_processed = true;
740 603 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -743,9 +606,9 @@
743 606 window.mxchatVisionProcessed = false;
744 607 window.mxchatOriginalMessage = null;
745 608 window.mxchatVisionImagesCount = 0;
746 609 }
747 -
610 +
748 611 $.ajax({
749 612 url: mxchatChat.ajax_url,
750 613 type: 'POST',
751 614 dataType: 'json',
@@ -783,16 +646,23 @@
783 646 errorMessage = "An error occurred. Please try again or contact support.";
784 647 }
785 648
786 649 // Handle session reset action (IP changed, session expired, etc.)
787 - // Silent reset — keep chat UI intact, just get a new session and retry
788 650 if (response.data && response.data.action === 'reset_session') {
789 - MxChatInstances.silentResetSession(botId);
790 - // Re-send the original message with the new session (user message is already displayed)
651 + // Clear the old session and generate a new one
652 + resetChatSession(botId);
653 + // Remove the temporary loading message
654 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
655 + // Re-send the original message with the new session
791 656 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
792 657 if (originalMessage) {
793 658 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
794 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
659 + // Re-add the user message and thinking indicator
660 + appendMessage("user", originalMessage, '', [], false, botId);
661 + appendThinkingMessage(botId);
662 + scrollToBottom(botId);
663 + // Determine whether to use streaming
664 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
795 665 if (shouldUseStreaming(currentModel)) {
796 666 callMxChatStream(originalMessage, function(response) {
797 667 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
798 668 }, botId);
@@ -934,9 +804,8 @@
934 804
935 805 replaceLastMessage("bot", errorMessage, '', [], botId);
936 806 }
937 807 });
938 - }); // refreshNonceIfNeeded
939 808 }
940 809
941 810 function callMxChatStream(message, callback, botId) {
942 811 botId = botId || getMxChatBotId();
@@ -955,25 +824,12 @@
955 824
956 825 // Get instance for session start timestamp (used when persistence is OFF)
957 826 var instance = MxChatInstances.get(botId);
958 827
959 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
960 - // non-string value via String(), so passing `null` would POST the literal string "null"
961 - // and land in the transcripts table as a ghost session. ensureSession() always returns
962 - // a real string even when cookies/localStorage are blocked.
963 - var streamSessionId = MxChatInstances.ensureSession(botId);
964 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
965 - streamSessionId = generateSessionId();
966 - MxChatInstances.setChatSession(botId, streamSessionId);
967 - }
968 -
969 - // Wait for the page-cache nonce refresh before constructing formData (which
970 - // captures mxchatChat.nonce by value). Mirrors callMxChat's wrapping. See plan-c5457f.
971 - refreshNonceIfNeeded(function() {
972 828 const formData = new FormData();
973 829 formData.append('action', 'mxchat_stream_chat');
974 830 formData.append('message', message);
975 - formData.append('session_id', streamSessionId);
831 + formData.append('session_id', getChatSession(botId));
976 832 formData.append('nonce', mxchatChat.nonce);
977 833 formData.append('current_page_url', window.location.href);
978 834 formData.append('current_page_title', document.title);
979 835 formData.append('bot_id', botId);
@@ -1073,16 +929,8 @@
1073 929
1074 930 // Re-enable chat input when stream ends with content
1075 931 enableChatInput(botId);
1076 932
1077 - // Scroll the user's last message to the top now that the
1078 - // bot's full reply has rendered (gives max reading room).
1079 - var $chatBoxDone = getElement(botId, 'chat-box');
1080 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
1081 - if ($lastUserMsgDone.length) {
1082 - scrollElementToTop($lastUserMsgDone, botId);
1083 - }
1084 -
1085 933 if (callback) {
1086 934 callback(accumulatedContent);
1087 935 }
1088 936 return;
@@ -1105,16 +953,8 @@
1105 953
1106 954 // Re-enable chat input after streaming completes
1107 955 enableChatInput(botId);
1108 956
1109 - // Scroll the user's last message to the top now
1110 - // that the bot's full reply has rendered.
1111 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1112 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1113 - if ($lastUserMsgStreamDone.length) {
1114 - scrollElementToTop($lastUserMsgStreamDone, botId);
1115 - }
1116 -
1117 957 if (callback) {
1118 958 callback(accumulatedContent);
1119 959 }
1120 960 return;
@@ -1193,9 +1033,8 @@
1193 1033 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1194 1034 callMxChat(message, callback, botId);
1195 1035 }
1196 1036 });
1197 - }); // refreshNonceIfNeeded
1198 1037 }
1199 1038
1200 1039 // Helper function to handle non-streaming responses
1201 1040 function handleNonStreamResponse(data, callback, botId) {
@@ -1234,16 +1073,21 @@
1234 1073 errorMessage = "An error occurred. Please try again or contact support.";
1235 1074 }
1236 1075
1237 1076 // Handle session reset action (IP changed, session expired, etc.)
1238 - // Silent reset — keep chat UI intact, just get a new session and retry
1239 1077 if (data.data && data.data.action === 'reset_session') {
1240 - MxChatInstances.silentResetSession(botId);
1241 - // Re-send the original message with the new session (user message is already displayed)
1078 + // Clear the old session and generate a new one
1079 + resetChatSession(botId);
1080 + // Re-send the original message with the new session
1242 1081 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1243 1082 if (originalMessage) {
1244 1083 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1245 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1084 + // Re-add the user message and thinking indicator
1085 + appendMessage("user", originalMessage, '', [], false, botId);
1086 + appendThinkingMessage(botId);
1087 + scrollToBottom(botId);
1088 + // Determine whether to use streaming
1089 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1246 1090 if (shouldUseStreaming(currentModel)) {
1247 1091 callMxChatStream(originalMessage, callback, botId);
1248 1092 } else {
1249 1093 callMxChat(originalMessage, callback, botId);
@@ -1424,229 +1268,9 @@
1424 1268 sendMessage(botId);
1425 1269 }
1426 1270 });
1427 1271
1428 -// Builds the list of overflow-menu items for a given bot.
1429 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1430 -function mxchatGetHeaderMenuItems(botId) {
1431 - var items = [];
1432 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1433 -
1434 - // The `print_button_*` keys still gate this item for back-compat with
1435 - // existing user options. The action is now a transcript download, not print.
1436 - if (settings.print_button_enabled === 'on') {
1437 - items.push({
1438 - id: 'download-transcript',
1439 - label: settings.print_button_label || 'Download Transcript',
1440 - icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
1441 - action: function() {
1442 - mxchatDownloadTranscript(botId);
1443 - }
1444 - });
1445 - }
1446 -
1447 - return items;
1448 -}
1449 -
1450 -// Builds a clean markdown transcript of the current conversation and triggers
1451 -// a file download. Used by the "Download Transcript" menu item.
1452 -function mxchatDownloadTranscript(botId) {
1453 - var $chatBox = getElement(botId, 'chat-box');
1454 - if (!$chatBox || !$chatBox.length) return;
1455 -
1456 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1457 - var headerTitle = settings.print_header_title || 'Chat transcript';
1458 - var now = new Date();
1459 - var stamp = now.toLocaleString();
1460 -
1461 - var lines = [];
1462 - lines.push('# ' + headerTitle);
1463 - lines.push('');
1464 - lines.push('Exported: ' + stamp);
1465 - lines.push('');
1466 - lines.push('---');
1467 - lines.push('');
1468 -
1469 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1470 - var $msg = $(this);
1471 - // Skip thinking placeholders and any in-flight temporary messages.
1472 - if ($msg.find('.thinking-dots').length) return;
1473 - if ($msg.hasClass('temporary-message')) return;
1474 -
1475 - var sender;
1476 - if ($msg.hasClass('user-message')) sender = 'User';
1477 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1478 - else sender = 'AI Agent';
1479 -
1480 - // Strip interactive UI from the cloned message so we get the conversation text.
1481 - var $clone = $msg.clone();
1482 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1483 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1484 - if (!text) return;
1485 -
1486 - lines.push('**' + sender + '**');
1487 - lines.push('');
1488 - lines.push(text);
1489 - lines.push('');
1490 - });
1491 -
1492 - var content = lines.join('\n');
1493 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1494 - var fname = 'mxchat-transcript-' + iso + '.md';
1495 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1496 - var url = URL.createObjectURL(blob);
1497 - var a = document.createElement('a');
1498 - a.href = url;
1499 - a.download = fname;
1500 - a.style.display = 'none';
1501 - document.body.appendChild(a);
1502 - a.click();
1503 - setTimeout(function() {
1504 - if (a.parentNode) a.parentNode.removeChild(a);
1505 - URL.revokeObjectURL(url);
1506 - }, 100);
1507 -}
1508 -
1509 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1510 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1511 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1512 -function mxchatSyncMenuColors(botId, $wrap) {
1513 - if (!$wrap || !$wrap.length) return;
1514 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1515 - if (!$bot.length) return;
1516 - var cs = window.getComputedStyle($bot[0]);
1517 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1518 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1519 - }
1520 - // Bot text color usually lives on a child div, not .bot-message itself.
1521 - var $textChild = $bot.find('[style*="color"]').first();
1522 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1523 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1524 -}
1525 -
1526 -// One-time per-widget init: renders menu items, wires open/close,
1527 -// outside-click, Escape, and arrow-key navigation. If no items, hides the trigger.
1528 -function mxchatInitHeaderMenu(botId) {
1529 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1530 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1531 -
1532 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1533 - var $menu = $wrap.find('.mxchat-header-menu');
1534 - var items = mxchatGetHeaderMenuItems(botId);
1535 -
1536 - // Initial color sync — covers normal page load.
1537 - mxchatSyncMenuColors(botId, $wrap);
1538 -
1539 - if (!items.length) {
1540 - $trigger.hide();
1541 - $menu.hide();
1542 - $wrap.data('mxchatMenuReady', true);
1543 - return;
1544 - }
1545 -
1546 - // Build the menu items.
1547 - $menu.empty();
1548 - items.forEach(function(item, idx) {
1549 - var $btn = $('<button>', {
1550 - type: 'button',
1551 - 'class': 'mxchat-menu-item',
1552 - 'role': 'menuitem',
1553 - 'tabindex': '-1',
1554 - 'data-menu-id': item.id,
1555 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1556 - '<span class="mxchat-menu-item-label"></span>'
1557 - });
1558 - $btn.find('.mxchat-menu-item-label').text(item.label);
1559 - $btn.on('click', function(e) {
1560 - e.preventDefault();
1561 - e.stopPropagation();
1562 - closeMenu();
1563 - try { item.action(); } catch (err) { /* no-op */ }
1564 - });
1565 - $menu.append($btn);
1566 - });
1567 -
1568 - function openMenu() {
1569 - // Re-sync each open in case the active theme changed since init.
1570 - mxchatSyncMenuColors(botId, $wrap);
1571 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1572 - $trigger.attr('aria-expanded', 'true');
1573 - // Focus the first item for keyboard users
1574 - setTimeout(function() {
1575 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1576 - }, 0);
1577 - }
1578 - function closeMenu(returnFocus) {
1579 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1580 - $trigger.attr('aria-expanded', 'false');
1581 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1582 - if (returnFocus) $trigger.trigger('focus');
1583 - }
1584 -
1585 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1586 - // click-to-collapse handler does not fire.
1587 - $trigger.on('click', function(e) {
1588 - e.preventDefault();
1589 - e.stopPropagation();
1590 - if ($menu.hasClass('is-open')) closeMenu();
1591 - else openMenu();
1592 - });
1593 -
1594 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1595 - $menu.on('click', function(e) {
1596 - e.stopPropagation();
1597 - });
1598 -
1599 - // Outside click closes the menu.
1600 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1601 - if (!$menu.hasClass('is-open')) return;
1602 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1603 - closeMenu();
1604 - });
1605 -
1606 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1607 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1608 - var $items = $menu.find('.mxchat-menu-item');
1609 - var idx = $items.index(this);
1610 - if (e.key === 'Escape') {
1611 - e.preventDefault();
1612 - closeMenu(true);
1613 - } else if (e.key === 'ArrowDown') {
1614 - e.preventDefault();
1615 - var $next = $items.eq((idx + 1) % $items.length);
1616 - $items.attr('tabindex', '-1');
1617 - $next.attr('tabindex', '0').trigger('focus');
1618 - } else if (e.key === 'ArrowUp') {
1619 - e.preventDefault();
1620 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1621 - $items.attr('tabindex', '-1');
1622 - $prev.attr('tabindex', '0').trigger('focus');
1623 - } else if (e.key === 'Enter' || e.key === ' ') {
1624 - e.preventDefault();
1625 - $(this).trigger('click');
1626 - }
1627 - });
1628 - $trigger.on('keydown', function(e) {
1629 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1630 - e.preventDefault();
1631 - closeMenu(true);
1632 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1633 - e.preventDefault();
1634 - openMenu();
1635 - }
1636 - });
1637 -
1638 - $wrap.data('mxchatMenuReady', true);
1639 -}
1640 -
1641 -// Initialize header menus for every rendered widget on DOM ready.
1642 -$(function() {
1643 - $('.mxchat-header-menu-wrap').each(function() {
1644 - var botId = $(this).data('bot-id');
1645 - if (botId) mxchatInitHeaderMenu(botId);
1646 - });
1647 -});
1648 -
1272 +
1649 1273 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1650 1274 try {
1651 1275 // Determine styles based on sender type
1652 1276 let messageClass, bgColor, fontColor;
@@ -1684,12 +1308,17 @@
1684 1308 'margin-bottom': '1em'
1685 1309 });
1686 1310 }
1687 1311
1688 - // Process the message content - always run linkify to convert markdown
1689 - // links and format text. linkify() handles existing HTML safely via
1690 - // negative lookaheads that skip URLs already inside <a> tags.
1691 - let fullMessage = linkify(messageText);
1312 + // Process the message content based on sender
1313 + let fullMessage;
1314 + if (sender === "user") {
1315 + // For user messages, apply linkify after sanitization
1316 + fullMessage = linkify(messageText);
1317 + } else {
1318 + // For bot/agent messages, preserve HTML
1319 + fullMessage = messageText;
1320 + }
1692 1321
1693 1322 // Add images if provided
1694 1323 if (images && images.length > 0) {
1695 1324 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1738,12 +1367,8 @@
1738 1367 if (lastUserMessage.length) {
1739 1368 scrollElementToTop(lastUserMessage, botId);
1740 1369 }
1741 1370 }
1742 -
1743 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1744 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1745 - }
1746 1371 });
1747 1372
1748 1373 if (messageText.id) {
1749 1374 var instance = MxChatInstances.get(botId);
@@ -1828,12 +1453,26 @@
1828 1453 bgColor = botMessageBgColor;
1829 1454 fontColor = botMessageFontColor;
1830 1455 }
1831 1456
1832 - // Always run linkify to convert markdown links and format text.
1833 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1834 - // to avoid double-processing URLs that are already inside <a> tags).
1835 - var fullMessage = linkify(responseText);
1457 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1458 + // This prevents double-processing of URLs that are already formatted as HTML
1459 + var fullMessage;
1460 + if (sender === "user") {
1461 + // Always linkify user messages (they're plain text)
1462 + fullMessage = linkify(responseText);
1463 + } else {
1464 + // For bot/agent messages, check if HTML already exists
1465 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1466 + responseText.includes('<img') || responseText.includes('<div') ||
1467 + responseText.includes('<p>') || responseText.includes('<br>')) {
1468 + // Response already has HTML, don't process it
1469 + fullMessage = responseText;
1470 + } else {
1471 + // Plain text response, apply linkify
1472 + fullMessage = linkify(responseText);
1473 + }
1474 + }
1836 1475
1837 1476 if (responseHtml) {
1838 1477 // Only add line breaks if there's actual text content before the HTML
1839 1478 if (fullMessage && fullMessage.trim()) {
@@ -1890,12 +1529,8 @@
1890 1529 }
1891 1530
1892 1531 // Re-enable chat input after response is displayed
1893 1532 enableChatInput(botId);
1894 -
1895 - if (sender === "bot" || sender === "agent") {
1896 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1897 - }
1898 1533 } else {
1899 1534 appendMessage(sender, responseText, responseHtml, images, false, botId);
1900 1535 // Re-enable chat input after response is displayed
1901 1536 enableChatInput(botId);
@@ -1998,63 +1633,37 @@
1998 1633 // Return as a proper link without the brackets
1999 1634 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2000 1635 });
2001 1636
2002 - // Process markdown links: [text](url) and [](url)
2003 - // Uses balanced parenthesis matching to handle URLs containing parens
2004 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
2005 - processedText = (function(input) {
2006 - var result = '';
2007 - var i = 0;
2008 - while (i < input.length) {
2009 - // Look for [ at current position
2010 - if (input[i] === '[') {
2011 - // Find closing ]
2012 - var closeBracket = input.indexOf(']', i + 1);
2013 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
2014 - result += input[i];
2015 - i++;
2016 - continue;
2017 - }
2018 - var linkText = input.substring(i + 1, closeBracket);
2019 - // Check if URL starts with http
2020 - var urlStart = closeBracket + 2;
2021 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
2022 - result += input[i];
2023 - i++;
2024 - continue;
2025 - }
2026 - // Find balanced closing paren
2027 - var depth = 1;
2028 - var j = urlStart;
2029 - while (j < input.length && depth > 0) {
2030 - if (input[j] === '(') depth++;
2031 - else if (input[j] === ')') depth--;
2032 - if (depth > 0) j++;
2033 - }
2034 - if (depth !== 0) {
2035 - result += input[i];
2036 - i++;
2037 - continue;
2038 - }
2039 - var url = input.substring(urlStart, j);
2040 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
2041 - var encodedUrl = safeEncodeUrl(cleanUrl);
2042 - if (!linkText || !linkText.trim()) {
2043 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
2044 - } else {
2045 - var safeText = sanitizeUserInput(linkText);
2046 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
2047 - }
2048 - i = j + 1; // Skip past the closing )
2049 - } else {
2050 - result += input[i];
2051 - i++;
2052 - }
1637 + // Process proper markdown links with text: [text](url)
1638 + // This MUST have non-empty text in the first brackets
1639 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1640 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1641 + // Make sure we have actual text (not just whitespace)
1642 + if (!text || !text.trim()) {
1643 + // If no text, treat the URL as the text
1644 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1645 + const safeUrl = safeEncodeUrl(cleanUrl);
1646 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2053 1647 }
2054 - return result;
2055 - })(processedText);
1648 +
1649 + // Clean the URL
1650 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1651 + const safeUrl = safeEncodeUrl(cleanUrl);
1652 + const safeText = sanitizeUserInput(text);
1653 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1654 + });
2056 1655
1656 + // Handle empty markdown links: [](url)
1657 + // This is a specific case where there's no text
1658 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1659 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1660 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1661 + const safeUrl = safeEncodeUrl(cleanUrl);
1662 + // Use the URL itself as the link text
1663 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1664 + });
1665 +
2057 1666 // Process phone numbers: [text](tel:number)
2058 1667 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
2059 1668 processedText = processedText.replace(phonePattern, (match, text, phone) => {
2060 1669 const safePhone = safeEncodeUrl(phone);
@@ -2348,14 +1957,13 @@
2348 1957 requestAnimationFrame(smoothScroll);
2349 1958 }
2350 1959 }
2351 1960
2352 - function scrollElementToTop(element, botId, topOffset) {
1961 + function scrollElementToTop(element, botId) {
2353 1962 botId = botId || 'default';
2354 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2355 1963 var chatBox = getElement(botId, 'chat-box');
2356 1964 var elementTop = element.position().top + chatBox.scrollTop();
2357 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1965 + chatBox.animate({ scrollTop: elementTop }, 500);
2358 1966 }
2359 1967
2360 1968 function showChatWidget(botId) {
2361 1969 botId = botId || 'default';
@@ -2582,19 +2190,11 @@
2582 2190 if (onComplete) onComplete();
2583 2191 return;
2584 2192 }
2585 2193
2586 - // Use getChatSession which returns null if no session exists (does NOT create one)
2587 2194 var sessionId = getChatSession(botId);
2588 2195 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2589 2196
2590 - // No session yet — nothing to load. History will load after first message via ensureSession.
2591 - if (!sessionId) {
2592 - instance.chatHistoryLoaded = true;
2593 - if (onComplete) onComplete();
2594 - return;
2595 - }
2596 -
2597 2197 if (chatPersistenceEnabled && sessionId) {
2598 2198 $.ajax({
2599 2199 url: mxchatChat.ajax_url,
2600 2200 type: 'POST',
@@ -2605,10 +2205,10 @@
2605 2205 },
2606 2206 success: function(response) {
2607 2207 // Handle session reset (IP changed while user was away)
2608 2208 if (response.success === false && response.data && response.data.action === 'reset_session') {
2609 - // Silent reset — new session but don't clear UI
2610 - MxChatInstances.silentResetSession(botId);
2209 + // Silently reset session - user will start fresh
2210 + resetChatSession(botId);
2611 2211 instance.chatHistoryLoaded = true; // Prevent retry loop
2612 2212 if (onComplete) onComplete();
2613 2213 return;
2614 2214 }
@@ -2666,19 +2266,9 @@
2666 2266 var content = message.content;
2667 2267 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2668 2268 content = decodeHTMLEntities(content);
2669 2269
2670 - // Skip linkify for messages containing structured HTML
2671 - // (forms, product cards, galleries, etc.) to avoid
2672 - // markdown formatting corrupting HTML attributes
2673 - // (e.g. underscores in name="field_name" becoming <em> tags)
2674 - if (content.includes("mxchat-product-card") ||
2675 - content.includes("mxchat-image-gallery") ||
2676 - content.includes("mxchat-featured-products") ||
2677 - content.includes("<form") ||
2678 - content.includes("<input") ||
2679 - content.includes("<select") ||
2680 - content.includes("<textarea")) {
2270 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2681 2271 messageElement.html(content);
2682 2272 } else {
2683 2273 var formattedContent = linkify(content);
2684 2274 messageElement.html(formattedContent);
@@ -2905,20 +2495,14 @@
2905 2495
2906 2496 function checkPreChatDismissal(botId) {
2907 2497 botId = botId || 'default';
2908 2498 try {
2909 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2910 - if (dismissedAt) {
2911 - // Re-show after 24 hours
2912 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2913 - if (elapsed < 86400000) {
2914 - getElement(botId, 'pre-chat-message').hide();
2915 - return;
2916 - }
2917 - // Expired — clear and show again
2918 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2499 + var dismissed = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2500 + if (!dismissed) {
2501 + getElement(botId, 'pre-chat-message').fadeIn(250);
2502 + } else {
2503 + getElement(botId, 'pre-chat-message').hide();
2919 2504 }
2920 - getElement(botId, 'pre-chat-message').fadeIn(250);
2921 2505 } catch (e) {
2922 2506 // localStorage unavailable — show the message
2923 2507 getElement(botId, 'pre-chat-message').fadeIn(250);
2924 2508 }
@@ -2927,9 +2511,9 @@
2927 2511 function handlePreChatDismissal(botId) {
2928 2512 botId = botId || 'default';
2929 2513 getElement(botId, 'pre-chat-message').fadeOut(200);
2930 2514 try {
2931 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2515 + localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, '1');
2932 2516 } catch (e) {
2933 2517 // localStorage unavailable — dismissal won't persist
2934 2518 }
2935 2519 }
@@ -2987,14 +2571,9 @@
2987 2571 collapseQuickQuestions(botId);
2988 2572 });
2989 2573
2990 2574 // Chatbot visibility toggle handlers - use class selector for multi-instance support
2991 - // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
2992 - $(document).on('click keydown', '.floating-chatbot-button', function(e) {
2993 - if (e.type === 'keydown') {
2994 - if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
2995 - e.preventDefault();
2996 - }
2575 + $(document).on('click', '.floating-chatbot-button', function() {
2997 2576 var botId = getBotIdFromElement(this);
2998 2577 var $chatbot = getElement(botId, 'floating-chatbot');
2999 2578 var $badge = getElement(botId, 'chat-notification-badge');
3000 2579 var $preChat = getElement(botId, 'pre-chat-message');
@@ -2999,21 +2578,14 @@
2999 2578 var $badge = getElement(botId, 'chat-notification-badge');
3000 2579 var $preChat = getElement(botId, 'pre-chat-message');
3001 2580
3002 2581 if ($chatbot.hasClass('hidden')) {
3003 - $chatbot.removeClass('hidden').addClass('visible')
3004 - .attr('aria-modal', 'true').attr('role', 'dialog');
3005 - $(this).addClass('hidden').attr('aria-expanded', 'true');
2582 + $chatbot.removeClass('hidden').addClass('visible');
2583 + $(this).addClass('hidden');
3006 2584 $badge.hide(); // Hide notification when opening chat
3007 2585 disableScroll();
3008 2586 $preChat.fadeOut(250);
3009 2587
3010 - // Load chat history for returning visitors (persistence)
3011 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3012 - if (chatPersistenceEnabled) {
3013 - MxChatInstances.ensureSession(botId);
3014 - }
3015 -
3016 2588 // Deferred email check — only on first widget open
3017 2589 var emailBlocker = getElementDOM(botId, 'email-blocker');
3018 2590 var instance = MxChatInstances.get(botId);
3019 2591 if (emailBlocker && !instance.emailCheckDone) {
@@ -3018,64 +2590,31 @@
3018 2590 var instance = MxChatInstances.get(botId);
3019 2591 if (emailBlocker && !instance.emailCheckDone) {
3020 2592 instance.emailCheckDone = true;
3021 2593 resolveEmailState(botId);
3022 - } else if (!emailBlocker) {
3023 - // No email collection — still route through showChatContainerForBot
3024 - // so the loader is shown while chat history loads
3025 - showChatContainerForBot(botId);
3026 2594 }
3027 -
3028 - // Move keyboard focus into the message input after the open transition.
3029 - setTimeout(function() {
3030 - var chatInput = getElementDOM(botId, 'chat-input');
3031 - if (chatInput && !chatInput.disabled) {
3032 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
3033 - }
3034 - }, 300);
3035 2595 } else {
3036 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3037 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2596 + $chatbot.removeClass('visible').addClass('hidden');
2597 + $(this).removeClass('hidden');
3038 2598 enableScroll();
3039 2599 checkPreChatDismissal(botId);
3040 2600 }
3041 2601 });
3042 2602
3043 - // Allow clicking anywhere on the title bar to close the chatbot.
3044 - // Returns keyboard focus to the launcher so keyboard users don't get
3045 - // stranded at <body> (WCAG SC 2.4.3 Focus Order). :focus-visible is
3046 - // heuristic-based so mouse-triggered close won't show a focus ring.
2603 + // Allow clicking anywhere on the title bar to close the chatbot
3047 2604 $(document).on('click', '.chatbot-top-bar', function() {
3048 2605 var botId = getBotIdFromElement(this);
3049 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3050 - var $launcher = getElement(botId, 'floating-chatbot-button');
3051 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
2606 + getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2607 + getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3052 2608 enableScroll();
3053 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3054 2609 });
3055 2610
3056 - // Global Escape-key handler — closes any visible chat widget and
3057 - // returns focus to its launcher. Standard modal-dismissal pattern;
3058 - // pairs with aria-modal="true" set on the widget when it opens.
3059 - $(document).on('keydown', function(e) {
3060 - if (e.key !== 'Escape' && e.key !== 'Esc') return;
3061 - var $visible = $('.floating-chatbot.visible');
3062 - if (!$visible.length) return;
3063 - e.preventDefault();
3064 - $visible.each(function() {
3065 - var botId = getBotIdFromElement(this);
3066 - $(this).addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3067 - var $launcher = getElement(botId, 'floating-chatbot-button');
3068 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
3069 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3070 - });
3071 - enableScroll();
3072 - });
3073 -
3074 2611 $(document).on('click', '.close-pre-chat-message', function(e) {
3075 2612 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3076 2613 var botId = getBotIdFromElement(this);
3077 - handlePreChatDismissal(botId);
2614 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2615 + $(this).remove();
2616 + });
3078 2617 });
3079 2618
3080 2619
3081 2620 // PDF upload button handlers - use class selector
@@ -3116,10 +2655,8 @@
3116 2655 const sendBtn = document.getElementById('send-button');
3117 2656 const originalBtnContent = uploadBtn.innerHTML;
3118 2657
3119 2658 try {
3120 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3121 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3122 2659 const formData = new FormData();
3123 2660 formData.append('action', 'mxchat_upload_pdf');
3124 2661 formData.append('pdf_file', file);
3125 2662 formData.append('session_id', sessionId);
@@ -3183,10 +2720,8 @@
3183 2720 const sendBtn = document.getElementById('send-button');
3184 2721 const originalBtnContent = uploadBtn.innerHTML;
3185 2722
3186 2723 try {
3187 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3188 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3189 2724 const formData = new FormData();
3190 2725 formData.append('action', 'mxchat_upload_word');
3191 2726 formData.append('word_file', file);
3192 2727 formData.append('session_id', sessionId);
@@ -3280,59 +2815,8 @@
3280 2815 });
3281 2816
3282 2817
3283 2818 // ====================================
3284 -// INIT LOADER & CHAT CONTAINER HELPERS
3285 -// ====================================
3286 -// These must be outside the email collection block so they're always available
3287 -// (used by persistence loading even when email collection is off)
3288 -
3289 -function showInitLoader(botId) {
3290 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3291 - if (loader) loader.style.display = 'flex';
3292 -}
3293 -
3294 -function hideInitLoader(botId) {
3295 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3296 - if (loader) loader.style.display = 'none';
3297 -}
3298 -
3299 -function showEmailFormForBot(botId) {
3300 - hideInitLoader(botId);
3301 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3302 - var chatContainer = getElementDOM(botId, 'chat-container');
3303 - if (emailBlocker) emailBlocker.style.display = 'flex';
3304 - if (chatContainer) chatContainer.style.display = 'none';
3305 -}
3306 -
3307 -function showChatContainerForBot(botId) {
3308 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3309 - var chatContainer = getElementDOM(botId, 'chat-container');
3310 - if (emailBlocker) emailBlocker.style.display = 'none';
3311 -
3312 - var instance = MxChatInstances.get(botId);
3313 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3314 -
3315 - // If persistence is on and history hasn't loaded yet, show loader
3316 - // while history loads to prevent flash of empty chat
3317 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3318 - if (chatContainer) chatContainer.style.display = 'none';
3319 - showInitLoader(botId);
3320 - loadChatHistory(botId, function() {
3321 - hideInitLoader(botId);
3322 - if (chatContainer) chatContainer.style.display = 'flex';
3323 - scrollToBottom(botId, true);
3324 - });
3325 - } else {
3326 - hideInitLoader(botId);
3327 - if (chatContainer) chatContainer.style.display = 'flex';
3328 - if (typeof loadChatHistory === 'function') {
3329 - loadChatHistory(botId);
3330 - }
3331 - }
3332 -}
3333 -
3334 -// ====================================
3335 2819 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3336 2820 // ====================================
3337 2821 // Only run email collection setup if it's enabled
3338 2822 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3368,8 +2852,40 @@
3368 2852 `;
3369 2853 document.head.appendChild(style);
3370 2854 }
3371 2855
2856 + // Helper functions for email collection (multi-instance aware)
2857 + function showEmailFormForBot(botId) {
2858 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2859 + var chatContainer = getElementDOM(botId, 'chat-container');
2860 + if (emailBlocker) emailBlocker.style.display = 'flex';
2861 + if (chatContainer) chatContainer.style.display = 'none';
2862 + }
2863 +
2864 + function showChatContainerForBot(botId) {
2865 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2866 + var chatContainer = getElementDOM(botId, 'chat-container');
2867 + if (emailBlocker) emailBlocker.style.display = 'none';
2868 +
2869 + var instance = MxChatInstances.get(botId);
2870 + var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2871 +
2872 + // If persistence is on and history hasn't loaded yet, keep container
2873 + // hidden until history loads to prevent flash of empty chat
2874 + if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
2875 + if (chatContainer) chatContainer.style.display = 'none';
2876 + loadChatHistory(botId, function() {
2877 + if (chatContainer) chatContainer.style.display = 'flex';
2878 + scrollToBottom(botId, true);
2879 + });
2880 + } else {
2881 + if (chatContainer) chatContainer.style.display = 'flex';
2882 + if (typeof loadChatHistory === 'function') {
2883 + loadChatHistory(botId);
2884 + }
2885 + }
2886 + }
2887 +
3372 2888 function isValidEmailAddress(email) {
3373 2889 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3374 2890 return emailRegex.test(email.trim()) && email.length <= 254;
3375 2891 }
@@ -3505,16 +3021,15 @@
3505 3021 }
3506 3022 }
3507 3023
3508 3024 function checkSessionAndEmailForBot(botId) {
3509 - const sessionId = MxChatInstances.ensureSession(botId);
3025 + const sessionId = getChatSession(botId);
3510 3026
3511 - // Hide both panels while we check — show loader instead
3027 + // Hide both panels while we check — prevents flash of wrong state
3512 3028 var emailBlocker = getElementDOM(botId, 'email-blocker');
3513 3029 var chatContainer = getElementDOM(botId, 'chat-container');
3514 3030 if (emailBlocker) emailBlocker.style.display = 'none';
3515 3031 if (chatContainer) chatContainer.style.display = 'none';
3516 - showInitLoader(botId);
3517 3032
3518 3033 fetch(mxchatChat.ajax_url, {
3519 3034 method: 'POST',
3520 3035 headers: {
@@ -3563,9 +3078,9 @@
3563 3078 var emailInput = getElementDOM(botId, 'user-email');
3564 3079 var nameInput = getElementDOM(botId, 'user-name');
3565 3080 var userEmail = emailInput ? emailInput.value.trim() : '';
3566 3081 var userName = nameInput ? nameInput.value.trim() : '';
3567 - var sessionId = MxChatInstances.ensureSession(botId);
3082 + var sessionId = getChatSession(botId);
3568 3083
3569 3084 // Validate email
3570 3085 if (!userEmail) {
3571 3086 showEmailError(botId, 'Please enter your email address.');
@@ -3694,8 +3209,9 @@
3694 3209 $('.mxchat-chatbot-wrapper').each(function() {
3695 3210 var botId = $(this).data('bot-id') || 'default';
3696 3211 var emailBlocker = getElementDOM(botId, 'email-blocker');
3697 3212
3213 + // Only check if email blocker exists for this bot
3698 3214 if (emailBlocker) {
3699 3215 if (isEmbeddedBot(botId)) {
3700 3216 // Embedded bots are always visible — check now
3701 3217 resolveEmailState(botId);
@@ -3700,15 +3216,8 @@
3700 3216 // Embedded bots are always visible — check now
3701 3217 resolveEmailState(botId);
3702 3218 }
3703 3219 // Floating bots: handled in the widget open handler
3704 - } else if (isEmbeddedBot(botId)) {
3705 - // Embedded bot, no email collection — load history with loader
3706 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3707 - if (chatPersistenceEnabled) {
3708 - MxChatInstances.ensureSession(botId);
3709 - showChatContainerForBot(botId);
3710 - }
3711 3220 }
3712 3221 });
3713 3222 }
3714 3223
@@ -3718,17 +3227,11 @@
3718 3227 var $chatbot = getElement(botId, 'floating-chatbot');
3719 3228 if ($chatbot.hasClass('hidden')) {
3720 3229 $chatbot.removeClass('hidden').addClass('visible');
3721 3230 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3722 - handlePreChatDismissal(botId);
3231 + $(this).fadeOut(250); // Hide pre-chat message
3723 3232 disableScroll(); // Disable scroll when chatbot opens
3724 3233
3725 - // Load chat history for returning visitors (persistence)
3726 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3727 - if (chatPersistenceEnabled) {
3728 - MxChatInstances.ensureSession(botId);
3729 - }
3730 -
3731 3234 // Deferred email check — only on first widget open
3732 3235 var emailBlocker = getElementDOM(botId, 'email-blocker');
3733 3236 var instance = MxChatInstances.get(botId);
3734 3237 if (emailBlocker && !instance.emailCheckDone) {
@@ -3733,17 +3236,38 @@
3733 3236 var instance = MxChatInstances.get(botId);
3734 3237 if (emailBlocker && !instance.emailCheckDone) {
3735 3238 instance.emailCheckDone = true;
3736 3239 resolveEmailState(botId);
3737 - } else if (!emailBlocker) {
3738 - showChatContainerForBot(botId);
3739 3240 }
3740 3241 }
3741 3242 });
3742 3243
3743 - // Legacy duplicate close handler removed — handled by single event delegation above
3244 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
3245 + // This is a fallback for legacy support
3246 + $(document).on('click', '.close-pre-chat-message', function() {
3247 + var botId = getBotIdFromElement(this);
3248 + var $preChat = getElement(botId, 'pre-chat-message');
3249 + $preChat.fadeOut(200); // Hide the message
3744 3250
3251 + // Send an AJAX request to set the transient flag for 24 hours
3252 + $.ajax({
3253 + url: mxchatChat.ajax_url,
3254 + type: 'POST',
3255 + data: {
3256 + action: 'mxchat_dismiss_pre_chat_message',
3257 + _ajax_nonce: mxchatChat.nonce
3258 + },
3259 + success: function() {
3260 + // Ensure the message is hidden after dismissal
3261 + $preChat.hide();
3262 + },
3263 + error: function() {
3264 + // Error dismissing pre-chat message - silently continue
3265 + }
3266 + });
3267 + });
3745 3268
3269 +
3746 3270 function hasQuickQuestions(botId) {
3747 3271 botId = botId || 'default';
3748 3272 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3749 3273 if (!questionsContainer) return false;
@@ -3936,265 +3460,6 @@
3936 3460 }, 2000);
3937 3461 });
3938 3462 }
3939 3463 }
3940 -});
3941 -
3942 -// ============================================================================
3943 -// SATISFACTION RATING (v3.2.6)
3944 -// ============================================================================
3945 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
3946 -// inactivity following a bot reply. One prompt per session, deduped via
3947 -// localStorage. Disabled site-wide when mxchatChat.satisfaction_rating_enabled
3948 -// is exactly false (default ON).
3949 -jQuery(function($) {
3950 - if (typeof mxchatChat === 'undefined') return;
3951 - if (mxchatChat.satisfaction_rating_enabled === false || mxchatChat.satisfaction_rating_enabled === 'off') return;
3952 -
3953 - // wp_localize_script stringifies ints, so accept both number and numeric string.
3954 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
3955 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
3956 - if (!isFinite(idleSeconds)) idleSeconds = 60;
3957 - if (idleSeconds < 5) idleSeconds = 5;
3958 - if (idleSeconds > 600) idleSeconds = 600;
3959 - var IDLE_MS = idleSeconds * 1000;
3960 - var MIN_BOT_REPLIES = 2;
3961 - var ratingState = {};
3962 -
3963 - function getState(botId) {
3964 - if (!ratingState[botId]) {
3965 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
3966 - }
3967 - return ratingState[botId];
3968 - }
3969 -
3970 - function getSessionId(botId) {
3971 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
3972 - return MxChatInstances.getChatSession(botId);
3973 - }
3974 - return null;
3975 - }
3976 -
3977 - function isAlreadyRated(sessionId) {
3978 - if (!sessionId) return false;
3979 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
3980 - }
3981 -
3982 - function markRated(sessionId) {
3983 - if (!sessionId) return;
3984 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
3985 - }
3986 -
3987 - function esc(s) {
3988 - return String(s == null ? '' : s)
3989 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
3990 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
3991 - }
3992 -
3993 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
3994 - function ratingSkipInlineColors(botId) {
3995 - if (mxchatChat.skip_inline_colors) return true;
3996 - var botAssignments = mxchatChat.bot_theme_assignments || {};
3997 - return botAssignments.hasOwnProperty(botId);
3998 - }
3999 -
4000 - function botBubbleStyleAttr(botId) {
4001 - if (ratingSkipInlineColors(botId)) return '';
4002 - var bg = mxchatChat.bot_message_bg_color;
4003 - var fg = mxchatChat.bot_message_font_color;
4004 - if (!bg && !fg) return '';
4005 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
4006 - }
4007 -
4008 - function copy(key) {
4009 - var c = mxchatChat.satisfaction_rating_copy || {};
4010 - var d = {
4011 - question: 'Was this helpful?',
4012 - helpful: 'Helpful',
4013 - not_helpful: 'Not helpful',
4014 - dismiss: 'Dismiss',
4015 - thanks: 'Thanks! Anything we should improve? (optional)',
4016 - placeholder: 'Tell us what could be better…',
4017 - send: 'Send',
4018 - skip: 'Skip',
4019 - saved: 'Thanks for the feedback.'
4020 - };
4021 - return c[key] || d[key];
4022 - }
4023 -
4024 - function thumbUpSvg() {
4025 - 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>';
4026 - }
4027 - function thumbDownSvg() {
4028 - 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>';
4029 - }
4030 -
4031 - function buildPromptHtml(botId) {
4032 - var styleAttr = botBubbleStyleAttr(botId);
4033 - return ''
4034 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4035 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
4036 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
4037 - + '<div class="mxchat-rating-actions">'
4038 - + '<span class="mxchat-rating-buttons">'
4039 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
4040 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
4041 - + '</span>'
4042 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
4043 - + '</div>'
4044 - + '</div>'
4045 - + '</div>';
4046 - }
4047 -
4048 - function buildFeedbackHtml(botId, rating) {
4049 - var styleAttr = botBubbleStyleAttr(botId);
4050 - return ''
4051 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4052 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
4053 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
4054 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
4055 - + '<div class="mxchat-rating-feedback-actions">'
4056 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
4057 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
4058 - + '</div>'
4059 - + '</div>'
4060 - + '</div>';
4061 - }
4062 -
4063 - function buildSavedHtml(botId) {
4064 - var styleAttr = botBubbleStyleAttr(botId);
4065 - return ''
4066 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4067 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
4068 - + '</div>';
4069 - }
4070 -
4071 - function getChatBoxByBotId(botId) {
4072 - var $byId = $('#chat-box-' + botId);
4073 - if ($byId.length) return $byId.first();
4074 - return $('.chat-box').first();
4075 - }
4076 -
4077 - function scrollChatBoxToBottom($chatBox) {
4078 - if (!$chatBox || !$chatBox.length) return;
4079 - $chatBox.scrollTop($chatBox[0].scrollHeight);
4080 - }
4081 -
4082 - function showPrompt(botId) {
4083 - var s = getState(botId);
4084 - if (s.promptShown || s.dismissed) return;
4085 - var sessionId = getSessionId(botId);
4086 - if (!sessionId) return;
4087 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4088 - var $chatBox = getChatBoxByBotId(botId);
4089 - if (!$chatBox.length) return;
4090 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
4091 - $chatBox.append(buildPromptHtml(botId));
4092 - s.promptShown = true;
4093 - scrollChatBoxToBottom($chatBox);
4094 - }
4095 -
4096 - function submitRating(botId, rating, feedback) {
4097 - var sessionId = getSessionId(botId);
4098 - if (!sessionId) return;
4099 - $.post(mxchatChat.ajax_url, {
4100 - action: 'mxchat_save_rating',
4101 - session_id: sessionId,
4102 - bot_id: botId,
4103 - rating: rating,
4104 - feedback: feedback || ''
4105 - });
4106 - markRated(sessionId);
4107 - }
4108 -
4109 - function onBotReply(botId) {
4110 - var s = getState(botId);
4111 - s.botReplies += 1;
4112 - if (s.promptShown || s.dismissed) return;
4113 - var sessionId = getSessionId(botId);
4114 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4115 - if (s.botReplies < MIN_BOT_REPLIES) return;
4116 - if (s.idleTimer) clearTimeout(s.idleTimer);
4117 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4118 - }
4119 -
4120 - function onUserMessage(botId) {
4121 - var s = getState(botId);
4122 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4123 - }
4124 -
4125 - function botIdFromChatBox(el) {
4126 - var id = el && el.id ? el.id : '';
4127 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4128 - }
4129 -
4130 - function setupObserver(chatBox) {
4131 - var botId = botIdFromChatBox(chatBox);
4132 - try {
4133 - var observer = new MutationObserver(function(mutations) {
4134 - mutations.forEach(function(m) {
4135 - for (var i = 0; i < m.addedNodes.length; i++) {
4136 - var node = m.addedNodes[i];
4137 - if (!node || node.nodeType !== 1) continue;
4138 - var $n = $(node);
4139 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4140 - 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)
4141 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4142 - }
4143 - });
4144 - });
4145 - observer.observe(chatBox, { childList: true });
4146 - } catch (e) { /* noop */ }
4147 - }
4148 -
4149 - $('.chat-box').each(function() { setupObserver(this); });
4150 -
4151 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4152 - e.preventDefault();
4153 - var $btn = $(this);
4154 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4155 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4156 - var botId = $prompt.data('bot-id') || 'default';
4157 - var rating = parseInt($btn.attr('data-rating'), 10);
4158 - if (rating !== 1 && rating !== -1) return;
4159 - submitRating(botId, rating, '');
4160 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4161 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4162 - });
4163 -
4164 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4165 - e.preventDefault();
4166 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4167 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4168 - var botId = $prompt.data('bot-id') || 'default';
4169 - var s = getState(botId);
4170 - s.dismissed = true;
4171 - markRated(getSessionId(botId));
4172 - ($wrap.length ? $wrap : $prompt).remove();
4173 - });
4174 -
4175 - function closeFeedback($fb) {
4176 - var botId = $fb.data('bot-id') || 'default';
4177 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4178 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4179 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4180 - }
4181 -
4182 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4183 - e.preventDefault();
4184 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4185 - });
4186 -
4187 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4188 - e.preventDefault();
4189 - var $fb = $(this).closest('.mxchat-rating-feedback');
4190 - var botId = $fb.data('bot-id') || 'default';
4191 - var rating = parseInt($fb.attr('data-rating'), 10);
4192 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4193 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4194 - if (text !== '') {
4195 - submitRating(botId, rating, text);
4196 - }
4197 - closeFeedback($fb);
4198 - });
4199 3464 });
4200 3465