PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.7
MxChat – AI Chatbot & Content Generation for WordPress v3.0.7
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 +281 -1601 3.2.133.0.7 View file →
@@ -1,166 +1,6 @@
1 1 jQuery(document).ready(function($) {
2 2
3 - // Nonce refresh — v2 (plan-6a68c9).
4 - //
5 - // The widget no longer relies on a nonce embedded in inline cached HTML.
6 - // Before each chat-send / stream-send / upload, we call the REST endpoint
7 - // GET /wp-json/mxchat/v1/nonce and use the freshly-issued value. The
8 - // endpoint creates the nonce with action `mxchat_chat_send`; the server-side
9 - // verifier ALSO still accepts the legacy `mxchat_chat_nonce` action for a
10 - // 30-day backwards-compat window so cached pages still in users' browsers
11 - // (which carry the legacy inline-localized nonce) keep working.
12 - //
13 - // Cache: a single module-scoped slot. TTL 12h conservatively (WP nonces are
14 - // 24h but we refetch at half-life so a freshly-cached-page user never sees
15 - // a borderline-stale nonce).
16 - var cachedFreshNonce = null;
17 - var cachedFreshNonceFetchedAt = 0;
18 - var NONCE_TTL_MS = 12 * 60 * 60 * 1000;
19 - var nonceRefreshState = 'idle'; // 'idle' | 'pending' | 'done'
20 - var nonceRefreshCallbacks = [];
21 -
22 - function getRestNonceUrl() {
23 - if (typeof mxchatChat !== 'undefined' && mxchatChat.rest_url) {
24 - return mxchatChat.rest_url.replace(/\/+$/, '') + '/nonce';
25 - }
26 - // Fallback: derive from current origin if mxchatChat.rest_url isn't set.
27 - return window.location.origin + '/wp-json/mxchat/v1/nonce';
28 - }
29 -
30 - function fetchFreshNonceFromRest() {
31 - return fetch(getRestNonceUrl(), {
32 - credentials: 'same-origin',
33 - headers: { 'Accept': 'application/json' }
34 - }).then(function (resp) {
35 - if (!resp.ok) {
36 - throw new Error('REST nonce fetch failed: ' + resp.status);
37 - }
38 - return resp.json();
39 - }).then(function (data) {
40 - if (data && data.nonce) {
41 - return data.nonce;
42 - }
43 - throw new Error('REST nonce response had no nonce field.');
44 - });
45 - }
46 -
47 - /**
48 - * withFreshNonce(cb) — invoke cb() after ensuring mxchatChat.nonce is fresh.
49 - * Tries REST endpoint first (cache-bypass design); falls back to the legacy
50 - * admin-ajax refresh path if REST is unavailable. Idempotent — concurrent
51 - * calls share the same in-flight refresh.
52 - */
53 - function withFreshNonce(callback) {
54 - if (typeof mxchatChat === 'undefined') {
55 - if (callback) callback();
56 - return;
57 - }
58 - var now = Date.now();
59 - if (cachedFreshNonce && (now - cachedFreshNonceFetchedAt) < NONCE_TTL_MS) {
60 - mxchatChat.nonce = cachedFreshNonce;
61 - if (callback) callback();
62 - return;
63 - }
64 - if (callback) nonceRefreshCallbacks.push(callback);
65 - if (nonceRefreshState === 'pending') return;
66 - nonceRefreshState = 'pending';
67 -
68 - var resolved = function (nonce) {
69 - if (nonce) {
70 - cachedFreshNonce = nonce;
71 - cachedFreshNonceFetchedAt = Date.now();
72 - mxchatChat.nonce = nonce;
73 - }
74 - nonceRefreshState = 'done';
75 - var pending = nonceRefreshCallbacks;
76 - nonceRefreshCallbacks = [];
77 - pending.forEach(function (cb) { try { cb(); } catch (e) {} });
78 - };
79 -
80 - fetchFreshNonceFromRest()
81 - .then(resolved)
82 - .catch(function () {
83 - // Fallback to the legacy admin-ajax refresh path (issued with the
84 - // old action `mxchat_chat_nonce`; the server still accepts both
85 - // during the compat window).
86 - if (mxchatChat.ajax_url) {
87 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' })
88 - .done(function (res) {
89 - if (res && res.success && res.data && res.data.nonce) {
90 - resolved(res.data.nonce);
91 - return;
92 - }
93 - resolved(null);
94 - })
95 - .fail(function () { resolved(null); });
96 - } else {
97 - resolved(null);
98 - }
99 - });
100 - }
101 -
102 - // Backwards-compat alias — every existing caller in this file (and any
103 - // out-of-tree consumer that hit this internal API) keeps working unchanged.
104 - function refreshNonceIfNeeded(callback) {
105 - return withFreshNonce(callback);
106 - }
107 -
108 - // Dynamic-settings refresh (plan-32db95).
109 - //
110 - // Every widget setting ships inline in cached page HTML, so behind a
111 - // full-page cache the site owner can't purge (host cache, CDN, the
112 - // browser itself) a toggled setting looks broken until the cache turns
113 - // over. Same distrust-cached-HTML reasoning as the per-request nonce:
114 - // on the FIRST widget open per page load we ask the nonce endpoint for
115 - // the current behavior-gate settings (?with_settings=1), merge them over
116 - // mxchatChat, and rebuild the header menu. Colors are NOT refreshed —
117 - // they're server-inline-styled, so a runtime swap would visibly flash.
118 - // On any failure we keep the inline values silently (nonce-fallback
119 - // posture). At most one request per page load, only if a widget opens.
120 - var dynamicSettingsState = 'idle'; // 'idle' | 'pending' | 'done'
121 -
122 - function mxchatRefreshDynamicSettings() {
123 - if (dynamicSettingsState !== 'idle') return;
124 - if (typeof mxchatChat === 'undefined') return;
125 - dynamicSettingsState = 'pending';
126 -
127 - var applied = function (data) {
128 - dynamicSettingsState = 'done';
129 - if (!data) return; // endpoint unavailable — inline values stand.
130 - if (data.nonce) {
131 - // Seed the nonce cache too: saves the first send's REST
132 - // round-trip and keeps us under the endpoint's rate limit.
133 - cachedFreshNonce = data.nonce;
134 - cachedFreshNonceFetchedAt = Date.now();
135 - mxchatChat.nonce = data.nonce;
136 - }
137 - if (data.settings && typeof data.settings === 'object') {
138 - $.extend(mxchatChat, data.settings);
139 - mxchatRebuildHeaderMenus();
140 - }
141 - };
142 -
143 - fetch(getRestNonceUrl() + '?with_settings=1', {
144 - credentials: 'same-origin',
145 - headers: { 'Accept': 'application/json' }
146 - }).then(function (resp) {
147 - if (!resp.ok) throw new Error('settings refresh failed: ' + resp.status);
148 - return resp.json();
149 - }).then(applied).catch(function () {
150 - // Fallback: legacy admin-ajax refresh path, same as withFreshNonce.
151 - if (mxchatChat.ajax_url) {
152 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce', with_settings: 1 })
153 - .done(function (res) {
154 - applied(res && res.success && res.data ? res.data : null);
155 - })
156 - .fail(function () { applied(null); });
157 - } else {
158 - applied(null);
159 - }
160 - });
161 - }
162 -
163 3 // ====================================
164 4 // MULTI-INSTANCE MANAGEMENT SYSTEM
165 5 // ====================================
166 6
@@ -176,9 +16,9 @@
176 16 var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
177 17
178 18 this.instances[botId] = {
179 19 botId: botId,
180 - sessionId: null,
20 + sessionId: this.getChatSession(botId),
181 21 lastSeenMessageId: '',
182 22 notificationCheckInterval: null,
183 23 pollingInterval: null,
184 24 processedMessageIds: new Set(),
@@ -203,77 +43,23 @@
203 43 return Object.keys(this.instances);
204 44 },
205 45
206 46 // Session management per bot
207 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
208 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
209 47 getChatSession: function(botId) {
210 48 var cookieName = 'mxchat_session_id_' + botId;
211 - var storageKey = 'mxchat_session_id_' + botId;
212 49 var sessionId = getCookie(cookieName);
213 50
214 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
215 51 if (!sessionId) {
216 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
52 + sessionId = generateSessionId();
53 + this.setChatSession(botId, sessionId);
217 54 }
218 55
219 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
220 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
221 - // storage). Without this, ensureSession() can generate and store an ID that
222 - // getChatSession() then can't read back, causing null session_ids on send.
223 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
224 - sessionId = this.instances[botId].sessionId;
225 - }
226 -
227 - // Guard against stored sentinel values that indicate earlier broken writes.
228 - if (sessionId === 'null' || sessionId === 'undefined') {
229 - sessionId = null;
230 - }
231 -
232 - // Re-sync cookie from localStorage if cookie was lost
233 - if (sessionId && !getCookie(cookieName)) {
234 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
235 - }
236 -
237 - return sessionId || null;
56 + return sessionId;
238 57 },
239 58
240 - // Lazy session initializer — called on first user interaction
241 - ensureSession: function(botId) {
242 - botId = botId || 'default';
243 - var instance = this.instances[botId] || this.init(botId);
244 -
245 - if (instance.sessionId) {
246 - return instance.sessionId;
247 - }
248 -
249 - // Check for existing session from cookie or localStorage
250 - var existingSession = this.getChatSession(botId);
251 -
252 - if (existingSession) {
253 - instance.sessionId = existingSession;
254 - } else {
255 - // Brand new session
256 - var newId = generateSessionId();
257 - this.setChatSession(botId, newId);
258 - instance.sessionId = newId;
259 - }
260 -
261 - // Now that we have a session, do the deferred work
262 - refreshNonceIfNeeded();
263 - trackOriginatingPage();
264 -
265 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
266 - // so we do NOT call it here to avoid a race condition.
267 -
268 - return instance.sessionId;
269 - },
270 -
271 59 setChatSession: function(botId, sessionId) {
272 60 var cookieName = 'mxchat_session_id_' + botId;
273 - var storageKey = 'mxchat_session_id_' + botId;
274 61 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
275 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
276 62 if (this.instances[botId]) {
277 63 this.instances[botId].sessionId = sessionId;
278 64 }
279 65 },
@@ -278,10 +64,8 @@
278 64 }
279 65 },
280 66
281 67 resetChatSession: function(botId) {
282 - // Clear old session from localStorage before setting new one
283 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
284 68 var newSessionId = generateSessionId();
285 69 this.setChatSession(botId, newSessionId);
286 70 var $chatBox = getElement(botId, 'chat-box');
287 71 if ($chatBox.length) {
@@ -290,20 +74,8 @@
290 74 if (this.instances[botId]) {
291 75 this.instances[botId].chatHistoryLoaded = false;
292 76 this.instances[botId].processedMessageIds = new Set();
293 77 }
294 - },
295 -
296 - // Silent reset — new session ID without clearing the chat UI
297 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
298 - silentResetSession: function(botId) {
299 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
300 - var newSessionId = generateSessionId();
301 - this.setChatSession(botId, newSessionId);
302 - if (this.instances[botId]) {
303 - this.instances[botId].sessionId = newSessionId;
304 - }
305 - return newSessionId;
306 78 }
307 79 };
308 80
309 81 // ====================================
@@ -343,20 +115,8 @@
343 115 var id = $floating.attr('id') || '';
344 116 var match = id.match(/floating-chatbot-(.+)/);
345 117 if (match) return match[1];
346 118 }
347 - // Fallback: the pre-chat teaser bubble (#pre-chat-message-{bot_id}) is a SIBLING
348 - // outside .mxchat-chatbot-wrapper / .floating-chatbot, so its children — e.g. the
349 - // .close-pre-chat-message button, which carries only a class and no id — miss both
350 - // branches above. Walk to the nearest ancestor whose id is pre-chat-message-{bot_id}
351 - // and read the suffix. (closest() includes the element itself, so a click directly on
352 - // #pre-chat-message-{bot_id} resolves here too.)
353 - var $preChat = $(element).closest('[id^="pre-chat-message-"]');
354 - if ($preChat.length) {
355 - var preId = $preChat.attr('id') || '';
356 - var preMatch = preId.match(/^pre-chat-message-(.+)$/);
357 - if (preMatch) return preMatch[1];
358 - }
359 119 // Fallback: check if element itself has an ID with bot suffix (e.g., floating-chatbot-button-{bot_id})
360 120 var elementId = $(element).attr('id') || '';
361 121 if (elementId) {
362 122 // Match patterns like: floating-chatbot-button-{bot_id}, pre-chat-message-{bot_id}
@@ -615,106 +375,13 @@
615 375 sendButton.disabled = false;
616 376 sendButton.style.opacity = '1';
617 377 sendButton.style.pointerEvents = 'auto';
618 378 }
619 - // Every completion path re-enables input, so this is the single restore
620 - // point for the streaming Stop affordance (no-op when not in stop mode).
621 - mxchatRestoreSendButton(botId);
622 379 }
623 380
624 -// --- Streaming Stop control -------------------------------------------------
625 -// One live stream handle per bot instance, so Stop on one widget never aborts
626 -// another bot on the same page.
627 -var mxchatActiveStreams = {};
628 -// Original send-button markup, captured once per bot the first time the Stop
629 -// state is shown (never captured while already in stop mode, so a rapid
630 -// stop-then-resend can't save the stop glyph as the "original").
631 -var mxchatSendMarkup = {};
632 -
633 -function mxchatShowStopButton(botId) {
634 - var btn = getElementDOM(botId, 'send-button');
635 - if (!btn) return;
636 - if (!btn.classList.contains('mxchat-stop-mode')) {
637 - mxchatSendMarkup[botId] = {
638 - html: btn.innerHTML,
639 - label: btn.getAttribute('aria-label')
640 - };
641 - }
642 -
643 - // Mirror the send icon's rendered size + color so the stop glyph looks
644 - // native, including custom send images/colors and theme overrides.
645 - var child = btn.querySelector('svg, img');
646 - var size = 25;
647 - var color = '';
648 - if (child) {
649 - var rect = child.getBoundingClientRect();
650 - if (rect.width) {
651 - size = Math.round(Math.min(rect.width, rect.height));
652 - }
653 - var cs = window.getComputedStyle(child);
654 - color = (child.tagName.toLowerCase() === 'svg' ? cs.fill : cs.color) || '';
655 - }
656 - var stopLabel = (typeof mxchatChat !== 'undefined' && mxchatChat.stop_button_label) || 'Stop response';
657 - btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" style="width:' + size + 'px;height:' + size + 'px;' + (color ? 'fill:' + color + ';' : '') + '"><rect x="5" y="5" width="14" height="14" rx="3"></rect></svg>';
658 - // An add-on's DIRECT send-button handler (e.g. mxchat-vision's rebind) can
659 - // start a stream synchronously while the originating click is still
660 - // bubbling up to our delegated handler. Without this guard, that handler
661 - // reads the just-added stop-mode class as a user Stop press and aborts the
662 - // brand-new stream — the user's message renders but no reply ever fires
663 - // (plan-4bba64 silent message loss). The flag only spans the current event
664 - // dispatch: cleared on the next macrotask, long before a real Stop click.
665 - btn.__mxchatStopJustShown = true;
666 - setTimeout(function () { btn.__mxchatStopJustShown = false; }, 0);
667 - btn.classList.add('mxchat-stop-mode');
668 - btn.setAttribute('aria-label', stopLabel);
669 - btn.setAttribute('title', stopLabel);
670 - // disableChatInput() ran when the turn was sent; the Stop control itself
671 - // must stay clickable while the textarea remains disabled.
672 - btn.disabled = false;
673 - btn.style.opacity = '1';
674 - btn.style.pointerEvents = 'auto';
675 -}
676 -
677 -function mxchatRestoreSendButton(botId) {
678 - var btn = getElementDOM(botId, 'send-button');
679 - var saved = mxchatSendMarkup[botId];
680 - if (!btn || !btn.classList.contains('mxchat-stop-mode') || !saved) return;
681 - btn.innerHTML = saved.html;
682 - btn.classList.remove('mxchat-stop-mode');
683 - btn.removeAttribute('title');
684 - if (saved.label) {
685 - btn.setAttribute('aria-label', saved.label);
686 - }
687 -}
688 -
689 -function mxchatStopStreaming(botId) {
690 - var entry = mxchatActiveStreams[botId];
691 - if (!entry || !entry.controller) return;
692 - entry.aborted = true;
693 - try { entry.controller.abort(); } catch (e) {}
694 -}
695 -
696 -// Returns true when a stream rejection came from an intentional Stop click:
697 -// keep the partial text as the turn's answer — no error UI, no fallback resend.
698 -function mxchatHandleStreamAbort(botId, accumulatedContent, callback) {
699 - var entry = mxchatActiveStreams[botId];
700 - if (!entry || !entry.aborted) return false;
701 - delete mxchatActiveStreams[botId];
702 - if (!accumulatedContent) {
703 - // Stopped before the first chunk: drop the thinking bubble, no orphan message.
704 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
705 - }
706 - enableChatInput(botId); // also restores the send icon
707 - if (callback) {
708 - callback(accumulatedContent || '');
709 - }
710 - return true;
711 -}
712 -
713 381 // Update your existing sendMessage function
714 382 function sendMessage(botId) {
715 383 botId = botId || 'default';
716 - MxChatInstances.ensureSession(botId);
717 384 var $chatInput = getElement(botId, 'chat-input');
718 385 var message = $chatInput.val();
719 386
720 387 // ADD PROMPT HOOK HERE
@@ -722,18 +389,13 @@
722 389 message = customMxChatFilter(message, "prompt");
723 390 }
724 391
725 392 if (message) {
726 - // Don't disable input in live agent mode - let users chat freely
727 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
728 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
729 - if (!isAgentMode) {
730 - disableChatInput(botId);
731 - }
393 + // Disable input while waiting for response
394 + disableChatInput(botId);
732 395
733 396 appendMessage("user", message, '', [], false, botId);
734 397 $chatInput.val('');
735 - mxchatUpdateCharCounter($chatInput[0]); // reset the char counter after send (plan 7091a2)
736 398 $chatInput.css('height', 'auto');
737 399
738 400 if (hasQuickQuestions(botId)) {
739 401 collapseQuickQuestions(botId);
@@ -758,9 +420,8 @@
758 420
759 421 // Update your existing sendMessageToChatbot function
760 422 function sendMessageToChatbot(message, botId) {
761 423 botId = botId || 'default';
762 - MxChatInstances.ensureSession(botId);
763 424
764 425 // ADD PROMPT HOOK HERE
765 426 if (typeof customMxChatFilter === 'function') {
766 427 message = customMxChatFilter(message, "prompt");
@@ -765,14 +426,10 @@
765 426 if (typeof customMxChatFilter === 'function') {
766 427 message = customMxChatFilter(message, "prompt");
767 428 }
768 429
769 - // Don't disable input in live agent mode - let users chat freely
770 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
771 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
772 - if (!isAgentMode) {
773 - disableChatInput(botId);
774 - }
430 + // Disable input while waiting for response
431 + disableChatInput(botId);
775 432
776 433 var sessionId = getChatSession(botId);
777 434
778 435 if (hasQuickQuestions(botId)) {
@@ -851,15 +508,8 @@
851 508
852 509 function callMxChat(message, callback, botId) {
853 510 botId = botId || getMxChatBotId();
854 511
855 - // Streaming fallbacks land here: drop any leftover stream handle and
856 - // return the button to its send state (no-op for plain non-stream turns).
857 - if (mxchatActiveStreams[botId]) {
858 - delete mxchatActiveStreams[botId];
859 - }
860 - mxchatRestoreSendButton(botId);
861 -
862 512 // Store the message in case we need to retry after session reset
863 513 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
864 514
865 515 // Get page context if contextual awareness is enabled
@@ -867,28 +517,13 @@
867 517
868 518 // Get instance for session start timestamp (used when persistence is OFF)
869 519 var instance = MxChatInstances.get(botId);
870 520
871 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
872 - // and returns the guaranteed-present session id from the in-memory instance even when
873 - // cookie/localStorage writes are silently blocked by the browser.
874 - var sessionId = MxChatInstances.ensureSession(botId);
875 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
876 - // Last-resort generation to ensure we never POST a null marker.
877 - sessionId = generateSessionId();
878 - MxChatInstances.setChatSession(botId, sessionId);
879 - }
880 -
881 - // Wait for the page-cache nonce refresh to complete before firing the
882 - // chat-send AJAX. On cached pages the inline mxchatChat.nonce is stale
883 - // until refreshNonceIfNeeded() returns; constructing ajaxData inside the
884 - // callback guarantees we read the fresh value. See plan-c5457f.
885 - refreshNonceIfNeeded(function() {
886 521 // Prepare AJAX data
887 522 const ajaxData = {
888 523 action: 'mxchat_handle_chat_request',
889 524 message: message,
890 - session_id: sessionId,
525 + session_id: getChatSession(botId),
891 526 nonce: mxchatChat.nonce,
892 527 current_page_url: window.location.href,
893 528 current_page_title: document.title,
894 529 bot_id: botId,
@@ -894,14 +529,14 @@
894 529 bot_id: botId,
895 530 // Pass session start timestamp so AI context matches what user sees
896 531 session_start_timestamp: instance.sessionStartTimestamp || 0
897 532 };
898 -
533 +
899 534 // Add page context if available
900 535 if (pageContext) {
901 536 ajaxData.page_context = JSON.stringify(pageContext);
902 537 }
903 -
538 +
904 539 // CHECK FOR VISION FLAGS AND ADD THEM
905 540 if (window.mxchatVisionProcessed) {
906 541 ajaxData.vision_processed = true;
907 542 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -910,9 +545,9 @@
910 545 window.mxchatVisionProcessed = false;
911 546 window.mxchatOriginalMessage = null;
912 547 window.mxchatVisionImagesCount = 0;
913 548 }
914 -
549 +
915 550 $.ajax({
916 551 url: mxchatChat.ajax_url,
917 552 type: 'POST',
918 553 dataType: 'json',
@@ -950,16 +585,23 @@
950 585 errorMessage = "An error occurred. Please try again or contact support.";
951 586 }
952 587
953 588 // Handle session reset action (IP changed, session expired, etc.)
954 - // Silent reset — keep chat UI intact, just get a new session and retry
955 589 if (response.data && response.data.action === 'reset_session') {
956 - MxChatInstances.silentResetSession(botId);
957 - // Re-send the original message with the new session (user message is already displayed)
590 + // Clear the old session and generate a new one
591 + resetChatSession(botId);
592 + // Remove the temporary loading message
593 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
594 + // Re-send the original message with the new session
958 595 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
959 596 if (originalMessage) {
960 597 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
961 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
598 + // Re-add the user message and thinking indicator
599 + appendMessage("user", originalMessage, '', [], false, botId);
600 + appendThinkingMessage(botId);
601 + scrollToBottom(botId);
602 + // Determine whether to use streaming
603 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
962 604 if (shouldUseStreaming(currentModel)) {
963 605 callMxChatStream(originalMessage, function(response) {
964 606 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
965 607 }, botId);
@@ -1016,11 +658,9 @@
1016 658 }
1017 659
1018 660 // Check for live agent response
1019 661 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
1020 - removeThinkingDots(botId);
1021 662 updateChatModeIndicator('agent', botId);
1022 - enableChatInput(botId);
1023 663 return;
1024 664 }
1025 665
1026 666 // Handle the message and show notification if chat is hidden
@@ -1053,13 +693,9 @@
1053 693 $badge.show();
1054 694 }
1055 695 }
1056 696 } else {
1057 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
1058 - if (response.vectorstore_error) {
1059 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
1060 - }
1061 - replaceLastMessage("bot", emptyMsg, '', [], botId);
697 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
1062 698 }
1063 699
1064 700 if (response.message_id) {
1065 701 var instance = MxChatInstances.get(botId);
@@ -1101,9 +737,8 @@
1101 737
1102 738 replaceLastMessage("bot", errorMessage, '', [], botId);
1103 739 }
1104 740 });
1105 - }); // refreshNonceIfNeeded
1106 741 }
1107 742
1108 743 function callMxChatStream(message, callback, botId) {
1109 744 botId = botId || getMxChatBotId();
@@ -1122,25 +757,12 @@
1122 757
1123 758 // Get instance for session start timestamp (used when persistence is OFF)
1124 759 var instance = MxChatInstances.get(botId);
1125 760
1126 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
1127 - // non-string value via String(), so passing `null` would POST the literal string "null"
1128 - // and land in the transcripts table as a ghost session. ensureSession() always returns
1129 - // a real string even when cookies/localStorage are blocked.
1130 - var streamSessionId = MxChatInstances.ensureSession(botId);
1131 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
1132 - streamSessionId = generateSessionId();
1133 - MxChatInstances.setChatSession(botId, streamSessionId);
1134 - }
1135 -
1136 - // Wait for the page-cache nonce refresh before constructing formData (which
1137 - // captures mxchatChat.nonce by value). Mirrors callMxChat's wrapping. See plan-c5457f.
1138 - refreshNonceIfNeeded(function() {
1139 761 const formData = new FormData();
1140 762 formData.append('action', 'mxchat_stream_chat');
1141 763 formData.append('message', message);
1142 - formData.append('session_id', streamSessionId);
764 + formData.append('session_id', getChatSession(botId));
1143 765 formData.append('nonce', mxchatChat.nonce);
1144 766 formData.append('current_page_url', window.location.href);
1145 767 formData.append('current_page_title', document.title);
1146 768 formData.append('bot_id', botId);
@@ -1166,20 +788,12 @@
1166 788 let accumulatedContent = '';
1167 789 let testingDataReceived = false;
1168 790 let streamingStarted = false;
1169 791
1170 - // Abortable stream: a fresh controller per turn, keyed by bot instance.
1171 - // The Stop control (send button swapped in place) aborts both the read
1172 - // loop and the underlying request.
1173 - var streamControl = { controller: new AbortController(), aborted: false };
1174 - mxchatActiveStreams[botId] = streamControl;
1175 - mxchatShowStopButton(botId);
1176 -
1177 792 fetch(mxchatChat.ajax_url, {
1178 793 method: 'POST',
1179 794 body: formData,
1180 - credentials: 'same-origin',
1181 - signal: streamControl.controller.signal
795 + credentials: 'same-origin'
1182 796 })
1183 797 .then(response => {
1184 798 // Store the response for potential fallback handling
1185 799 const responseClone = response.clone();
@@ -1248,16 +862,8 @@
1248 862
1249 863 // Re-enable chat input when stream ends with content
1250 864 enableChatInput(botId);
1251 865
1252 - // Scroll the user's last message to the top now that the
1253 - // bot's full reply has rendered (gives max reading room).
1254 - var $chatBoxDone = getElement(botId, 'chat-box');
1255 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
1256 - if ($lastUserMsgDone.length) {
1257 - scrollElementToTop($lastUserMsgDone, botId);
1258 - }
1259 -
1260 866 if (callback) {
1261 867 callback(accumulatedContent);
1262 868 }
1263 869 return;
@@ -1280,16 +886,8 @@
1280 886
1281 887 // Re-enable chat input after streaming completes
1282 888 enableChatInput(botId);
1283 889
1284 - // Scroll the user's last message to the top now
1285 - // that the bot's full reply has rendered.
1286 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1287 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1288 - if ($lastUserMsgStreamDone.length) {
1289 - scrollElementToTop($lastUserMsgStreamDone, botId);
1290 - }
1291 -
1292 890 if (callback) {
1293 891 callback(accumulatedContent);
1294 892 }
1295 893 return;
@@ -1346,9 +944,8 @@
1346 944 }
1347 945
1348 946 processStream();
1349 947 }).catch(streamError => {
1350 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1351 948 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1352 949 callMxChat(message, callback, botId);
1353 950 });
1354 951 }
@@ -1355,9 +952,8 @@
1355 952
1356 953 processStream();
1357 954 })
1358 955 .catch(error => {
1359 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1360 956 // Check if we have server error data with chat mode
1361 957 if (error && error.isServerError && error.data) {
1362 958 // Check for chat mode in error data
1363 959 if (error.data.chat_mode) {
@@ -1370,9 +966,8 @@
1370 966 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1371 967 callMxChat(message, callback, botId);
1372 968 }
1373 969 });
1374 - }); // refreshNonceIfNeeded
1375 970 }
1376 971
1377 972 // Helper function to handle non-streaming responses
1378 973 function handleNonStreamResponse(data, callback, botId) {
@@ -1411,16 +1006,21 @@
1411 1006 errorMessage = "An error occurred. Please try again or contact support.";
1412 1007 }
1413 1008
1414 1009 // Handle session reset action (IP changed, session expired, etc.)
1415 - // Silent reset — keep chat UI intact, just get a new session and retry
1416 1010 if (data.data && data.data.action === 'reset_session') {
1417 - MxChatInstances.silentResetSession(botId);
1418 - // Re-send the original message with the new session (user message is already displayed)
1011 + // Clear the old session and generate a new one
1012 + resetChatSession(botId);
1013 + // Re-send the original message with the new session
1419 1014 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1420 1015 if (originalMessage) {
1421 1016 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1422 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1017 + // Re-add the user message and thinking indicator
1018 + appendMessage("user", originalMessage, '', [], false, botId);
1019 + appendThinkingMessage(botId);
1020 + scrollToBottom(botId);
1021 + // Determine whether to use streaming
1022 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1423 1023 if (shouldUseStreaming(currentModel)) {
1424 1024 callMxChatStream(originalMessage, callback, botId);
1425 1025 } else {
1426 1026 callMxChat(originalMessage, callback, botId);
@@ -1442,22 +1042,8 @@
1442 1042 }
1443 1043 return; // Exit early for errors
1444 1044 }
1445 1045
1446 - // Check for live agent response
1447 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1448 - removeThinkingDots(botId);
1449 - // Also remove any leftover bot-message that lost its temporary-message class
1450 - var $chatBox = getElement(botId, 'chat-box');
1451 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1452 - updateChatModeIndicator('agent', botId);
1453 - enableChatInput(botId);
1454 - if (callback) {
1455 - callback('');
1456 - }
1457 - return;
1458 - }
1459 -
1460 1046 // Handle different response formats
1461 1047 if (data.text || data.html || data.message) {
1462 1048
1463 1049 // Apply response hooks
@@ -1502,15 +1088,19 @@
1502 1088 }
1503 1089
1504 1090 // Enhanced updateChatModeIndicator function for immediate DOM updates
1505 1091 function updateChatModeIndicator(mode, botId) {
1092 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1506 1093 botId = botId || 'default';
1507 1094 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1095 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1508 1096 if (indicator) {
1509 1097 const oldText = indicator.textContent;
1098 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1510 1099
1511 1100 if (mode === 'agent') {
1512 1101 indicator.textContent = 'Live Agent';
1102 + console.log('[MxChat] Mode is agent, calling startPolling...');
1513 1103 startPolling(botId);
1514 1104 } else {
1515 1105 // Everything else is AI mode
1516 1106 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1581,21 +1171,9 @@
1581 1171 // Update the event handlers to use the correct function names (using event delegation)
1582 1172 // Use class-based selectors for multi-instance support
1583 1173 $(document).on('click', '.send-button', function() {
1584 1174 var botId = getBotIdFromElement(this);
1585 - // While a response is streaming the button is a Stop control.
1586 - if (this.classList.contains('mxchat-stop-mode')) {
1587 - // Same click that just started this stream (an add-on's direct handler
1588 - // ran before this delegated one) — not a Stop press. See
1589 - // mxchatShowStopButton for the full story (plan-4bba64).
1590 - if (this.__mxchatStopJustShown) return;
1591 - mxchatStopStreaming(botId);
1592 - return;
1593 - }
1594 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1595 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1596 - disableChatInput(botId);
1597 - }
1175 + disableChatInput(botId);
1598 1176 sendMessage(botId);
1599 1177 });
1600 1178
1601 1179 // Override enter key handler (using event delegation)
@@ -1602,378 +1180,14 @@
1602 1180 $(document).on('keypress', '.chat-input', function(e) {
1603 1181 if (e.which == 13 && !e.shiftKey) {
1604 1182 e.preventDefault();
1605 1183 var botId = getBotIdFromElement(this);
1606 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1607 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1608 - disableChatInput(botId);
1609 - }
1184 + disableChatInput(botId);
1610 1185 sendMessage(botId);
1611 1186 }
1612 1187 });
1613 1188
1614 -// Chat input character counter + soft limit feedback (plan 7091a2).
1615 -// Language-neutral: numbers + color only, no translatable strings. The counter
1616 -// reveals near the cap and ramps neutral -> amber -> red; an over-limit keystroke
1617 -// or trimmed paste produces a brief border-flash/shake so the maxlength cap (plan
1618 -// a3fae2) is never a silent "input jumps back". Per-bot scoped via .input-container.
1619 -function mxchatUpdateCharCounter(inputEl) {
1620 - if (!inputEl || !inputEl.closest) return;
1621 - var max = parseInt(inputEl.getAttribute('maxlength'), 10);
1622 - var container = inputEl.closest('.input-container');
1623 - if (!container || !max || max <= 0) return;
1624 - var counter = container.querySelector('.mxchat-char-counter');
1625 - if (!counter) return;
1626 - var len = inputEl.value.length;
1627 - var ratio = len / max;
1628 - var nearThreshold = 0.8; // start surfacing the counter at 80% of the cap
1629 - var cur = counter.querySelector('.mxchat-char-counter-current');
1630 - if (cur) cur.textContent = len;
1631 - var warn = ratio >= nearThreshold && len < max;
1632 - var full = len >= max;
1633 - counter.classList.toggle('is-visible', ratio >= nearThreshold);
1634 - counter.classList.toggle('is-warn', warn);
1635 - counter.classList.toggle('is-full', full);
1636 - container.classList.toggle('mxchat-input-near-limit', warn);
1637 - container.classList.toggle('mxchat-input-at-limit', full);
1638 -}
1639 -
1640 -function mxchatBumpInput(inputEl) {
1641 - var container = inputEl && inputEl.closest ? inputEl.closest('.input-container') : null;
1642 - if (!container) return;
1643 - container.classList.remove('mxchat-input-bump');
1644 - void container.offsetWidth; // reflow so a rapid second hit retriggers the animation
1645 - container.classList.add('mxchat-input-bump');
1646 - clearTimeout($(container).data('mxchatBumpTimeout'));
1647 - var t = setTimeout(function() { container.classList.remove('mxchat-input-bump'); }, 220);
1648 - $(container).data('mxchatBumpTimeout', t);
1649 -}
1650 -
1651 -// Live counter update on every input.
1652 -$(document).on('input', '.chat-input', function() {
1653 - mxchatUpdateCharCounter(this);
1654 -});
1655 -
1656 -// Visible "you've hit the edge" feedback when a printable keystroke is about to be
1657 -// rejected at the cap (maxlength silently swallows it otherwise).
1658 -$(document).on('keydown', '.chat-input', function(e) {
1659 - var max = parseInt(this.getAttribute('maxlength'), 10);
1660 - if (!max || max <= 0 || this.value.length < max) return;
1661 - if (e.ctrlKey || e.metaKey || e.altKey) return;
1662 - // A single printable char with no selection to overwrite WILL be rejected.
1663 - if (e.key && e.key.length === 1 && this.selectionStart === this.selectionEnd) {
1664 - mxchatBumpInput(this);
1665 - }
1666 -});
1667 -
1668 -// A paste that gets trimmed to the cap also bumps, so truncation is never silent.
1669 -$(document).on('paste', '.chat-input', function() {
1670 - var el = this;
1671 - var max = parseInt(el.getAttribute('maxlength'), 10);
1672 - if (!max || max <= 0) return;
1673 - setTimeout(function() {
1674 - mxchatUpdateCharCounter(el);
1675 - if (el.value.length >= max) mxchatBumpInput(el);
1676 - }, 0);
1677 -});
1678 -
1679 -// Builds the list of overflow-menu items for a given bot.
1680 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1681 -function mxchatGetHeaderMenuItems(botId) {
1682 - var items = [];
1683 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1684 -
1685 - // The `print_button_*` keys still gate this item for back-compat with
1686 - // existing user options. The action is now a transcript download, not print.
1687 - if (settings.print_button_enabled === 'on') {
1688 - items.push({
1689 - id: 'download-transcript',
1690 - label: settings.print_button_label || 'Download Transcript',
1691 - 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>',
1692 - action: function() {
1693 - mxchatDownloadTranscript(botId);
1694 - }
1695 - });
1696 - }
1697 -
1698 - // "Start new chat" — surfaces the EXISTING per-conversation reset
1699 - // (MxChatInstances.resetChatSession) so a visitor can start a fresh thread
1700 - // without the site owner disabling chat persistence globally. Default OFF;
1701 - // gated by the reset_chat_enabled option. plan ac2e81.
1702 - if (settings.reset_chat_enabled === 'on') {
1703 - items.push({
1704 - id: 'reset-chat',
1705 - label: settings.reset_chat_label || 'Start new chat',
1706 - 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"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>',
1707 - action: function() {
1708 - var confirmMsg = settings.reset_chat_confirm || 'Start a new chat? This clears the current conversation.';
1709 - if (window.confirm(confirmMsg)) {
1710 - MxChatInstances.resetChatSession(botId);
1711 - }
1712 - }
1713 - });
1714 - }
1715 -
1716 - return items;
1717 -}
1718 -
1719 -// Builds a clean markdown transcript of the current conversation and triggers
1720 -// a file download. Used by the "Download Transcript" menu item.
1721 -function mxchatDownloadTranscript(botId) {
1722 - var $chatBox = getElement(botId, 'chat-box');
1723 - if (!$chatBox || !$chatBox.length) return;
1724 -
1725 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1726 - var headerTitle = settings.print_header_title || 'Chat transcript';
1727 - var now = new Date();
1728 - var stamp = now.toLocaleString();
1729 -
1730 - var lines = [];
1731 - lines.push('# ' + headerTitle);
1732 - lines.push('');
1733 - lines.push('Exported: ' + stamp);
1734 - lines.push('');
1735 - lines.push('---');
1736 - lines.push('');
1737 -
1738 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1739 - var $msg = $(this);
1740 - // Skip thinking placeholders and any in-flight temporary messages.
1741 - if ($msg.find('.thinking-dots').length) return;
1742 - if ($msg.hasClass('temporary-message')) return;
1743 -
1744 - var sender;
1745 - if ($msg.hasClass('user-message')) sender = 'User';
1746 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1747 - else sender = 'AI Agent';
1748 -
1749 - // Strip interactive UI from the cloned message so we get the conversation text.
1750 - var $clone = $msg.clone();
1751 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1752 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1753 - if (!text) return;
1754 -
1755 - lines.push('**' + sender + '**');
1756 - lines.push('');
1757 - lines.push(text);
1758 - lines.push('');
1759 - });
1760 -
1761 - var content = lines.join('\n');
1762 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1763 - var fname = 'mxchat-transcript-' + iso + '.md';
1764 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1765 - var url = URL.createObjectURL(blob);
1766 - var a = document.createElement('a');
1767 - a.href = url;
1768 - a.download = fname;
1769 - a.style.display = 'none';
1770 - document.body.appendChild(a);
1771 - a.click();
1772 - setTimeout(function() {
1773 - if (a.parentNode) a.parentNode.removeChild(a);
1774 - URL.revokeObjectURL(url);
1775 - }, 100);
1776 -}
1777 -
1778 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1779 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1780 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1781 -function mxchatSyncMenuColors(botId, $wrap) {
1782 - if (!$wrap || !$wrap.length) return;
1783 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1784 - if (!$bot.length) return;
1785 - var cs = window.getComputedStyle($bot[0]);
1786 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1787 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1788 - }
1789 - // Bot text color usually lives on a child div, not .bot-message itself.
1790 - var $textChild = $bot.find('[style*="color"]').first();
1791 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1792 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1793 -}
1794 -
1795 -// Renders (or re-renders) the item list for one menu wrap. Split out of
1796 -// mxchatInitHeaderMenu so the dynamic-settings merge (plan-32db95) can
1797 -// rebuild items + trigger visibility WITHOUT re-binding the one-time
1798 -// open/close/keyboard wiring. closeMenu is passed in by the init closure;
1799 -// a rebuild before init (never happens, but harmless) just skips it.
1800 -function mxchatRenderHeaderMenuItems(botId, $wrap, closeMenuFn) {
1801 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1802 - var $menu = $wrap.find('.mxchat-header-menu');
1803 - var items = mxchatGetHeaderMenuItems(botId);
1804 -
1805 - $menu.empty();
1806 -
1807 - if (!items.length) {
1808 - $trigger.hide();
1809 - $menu.hide();
1810 - return;
1811 - }
1812 -
1813 - // Clear any inline display:none a previous zero-item render left behind —
1814 - // open/close visibility is governed by the hidden prop + is-open class.
1815 - $trigger.css('display', '');
1816 - $menu.css('display', '');
1817 -
1818 - items.forEach(function(item, idx) {
1819 - var $btn = $('<button>', {
1820 - type: 'button',
1821 - 'class': 'mxchat-menu-item',
1822 - 'role': 'menuitem',
1823 - 'tabindex': '-1',
1824 - 'data-menu-id': item.id,
1825 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1826 - '<span class="mxchat-menu-item-label"></span>'
1827 - });
1828 - $btn.find('.mxchat-menu-item-label').text(item.label);
1829 - $btn.on('click', function(e) {
1830 - e.preventDefault();
1831 - e.stopPropagation();
1832 - if (closeMenuFn) closeMenuFn();
1833 - try { item.action(); } catch (err) { /* no-op */ }
1834 - });
1835 - $menu.append($btn);
1836 - });
1837 -}
1838 -
1839 -// Re-render every menu on the page after a dynamic-settings merge
1840 -// (multi-bot: each wrap re-reads its items). An OPEN menu is left alone —
1841 -// swapping items under the user mid-interaction yanks focus — and the
1842 -// rebuild runs when it closes instead (closeMenu checks the pending flag).
1843 -function mxchatRebuildHeaderMenus() {
1844 - $('.mxchat-header-menu-wrap').each(function() {
1845 - var $wrap = $(this);
1846 - var botId = $wrap.data('bot-id');
1847 - if (!botId) return;
1848 - if (!$wrap.data('mxchatMenuReady')) {
1849 - mxchatInitHeaderMenu(botId);
1850 - return;
1851 - }
1852 - if ($wrap.find('.mxchat-header-menu').hasClass('is-open')) {
1853 - $wrap.data('mxchatMenuRebuildPending', true);
1854 - return;
1855 - }
1856 - mxchatRenderHeaderMenuItems(botId, $wrap, $wrap.data('mxchatMenuClose'));
1857 - });
1858 -}
1859 -
1860 -// One-time per-widget init: renders menu items, wires open/close,
1861 -// outside-click, Escape, and arrow-key navigation. If no items, hides the
1862 -// trigger. Wiring happens even when there are zero items at init, so a
1863 -// later dynamic-settings rebuild that adds items has a working trigger.
1864 -function mxchatInitHeaderMenu(botId) {
1865 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1866 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1867 -
1868 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1869 - var $menu = $wrap.find('.mxchat-header-menu');
1870 -
1871 - // Initial color sync — covers normal page load.
1872 - mxchatSyncMenuColors(botId, $wrap);
1873 -
1874 - function openMenu() {
1875 - // Re-sync each open in case the active theme changed since init.
1876 - mxchatSyncMenuColors(botId, $wrap);
1877 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1878 - $trigger.attr('aria-expanded', 'true');
1879 - // Focus the first item for keyboard users
1880 - setTimeout(function() {
1881 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1882 - }, 0);
1883 - }
1884 - function closeMenu(returnFocus) {
1885 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1886 - $trigger.attr('aria-expanded', 'false');
1887 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1888 - if (returnFocus) $trigger.trigger('focus');
1889 - // A dynamic-settings rebuild that arrived while the menu was open
1890 - // was deferred (mxchatRebuildHeaderMenus) — run it now.
1891 - if ($wrap.data('mxchatMenuRebuildPending')) {
1892 - $wrap.removeData('mxchatMenuRebuildPending');
1893 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1894 - }
1895 - }
1896 -
1897 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1898 - // click-to-collapse handler does not fire.
1899 - $trigger.on('click', function(e) {
1900 - e.preventDefault();
1901 - e.stopPropagation();
1902 - if ($menu.hasClass('is-open')) closeMenu();
1903 - else openMenu();
1904 - });
1905 -
1906 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1907 - $menu.on('click', function(e) {
1908 - e.stopPropagation();
1909 - });
1910 -
1911 - // Outside click closes the menu.
1912 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1913 - if (!$menu.hasClass('is-open')) return;
1914 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1915 - closeMenu();
1916 - });
1917 -
1918 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1919 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1920 - var $items = $menu.find('.mxchat-menu-item');
1921 - var idx = $items.index(this);
1922 - if (e.key === 'Escape') {
1923 - e.preventDefault();
1924 - closeMenu(true);
1925 - } else if (e.key === 'ArrowDown') {
1926 - e.preventDefault();
1927 - var $next = $items.eq((idx + 1) % $items.length);
1928 - $items.attr('tabindex', '-1');
1929 - $next.attr('tabindex', '0').trigger('focus');
1930 - } else if (e.key === 'ArrowUp') {
1931 - e.preventDefault();
1932 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1933 - $items.attr('tabindex', '-1');
1934 - $prev.attr('tabindex', '0').trigger('focus');
1935 - } else if (e.key === 'Enter' || e.key === ' ') {
1936 - e.preventDefault();
1937 - $(this).trigger('click');
1938 - }
1939 - });
1940 - $trigger.on('keydown', function(e) {
1941 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1942 - e.preventDefault();
1943 - closeMenu(true);
1944 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1945 - e.preventDefault();
1946 - openMenu();
1947 - }
1948 - });
1949 -
1950 - // Expose closeMenu for out-of-closure re-renders (mxchatRebuildHeaderMenus),
1951 - // then do the initial item render.
1952 - $wrap.data('mxchatMenuClose', closeMenu);
1953 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1954 -
1955 - $wrap.data('mxchatMenuReady', true);
1956 -}
1957 -
1958 -// Initialize header menus for every rendered widget on DOM ready.
1959 -$(function() {
1960 - $('.mxchat-header-menu-wrap').each(function() {
1961 - var botId = $(this).data('bot-id');
1962 - if (botId) mxchatInitHeaderMenu(botId);
1963 - });
1964 -
1965 - // Embedded (non-floating) widgets are open from the moment the page
1966 - // renders — refresh dynamic settings at init (plan-32db95). Floating
1967 - // widgets refresh on first launcher open instead.
1968 - var hasEmbeddedWidget = $('.mxchat-chatbot-wrapper').filter(function() {
1969 - return !$(this).closest('.floating-chatbot').length;
1970 - }).length > 0;
1971 - if (hasEmbeddedWidget) {
1972 - mxchatRefreshDynamicSettings();
1973 - }
1974 -});
1975 -
1189 +
1976 1190 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1977 1191 try {
1978 1192 // Determine styles based on sender type
1979 1193 let messageClass, bgColor, fontColor;
@@ -2011,12 +1225,17 @@
2011 1225 'margin-bottom': '1em'
2012 1226 });
2013 1227 }
2014 1228
2015 - // Process the message content - always run linkify to convert markdown
2016 - // links and format text. linkify() handles existing HTML safely via
2017 - // negative lookaheads that skip URLs already inside <a> tags.
2018 - let fullMessage = linkify(messageText);
1229 + // Process the message content based on sender
1230 + let fullMessage;
1231 + if (sender === "user") {
1232 + // For user messages, apply linkify after sanitization
1233 + fullMessage = linkify(messageText);
1234 + } else {
1235 + // For bot/agent messages, preserve HTML
1236 + fullMessage = messageText;
1237 + }
2019 1238
2020 1239 // Add images if provided
2021 1240 if (images && images.length > 0) {
2022 1241 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -2065,12 +1284,8 @@
2065 1284 if (lastUserMessage.length) {
2066 1285 scrollElementToTop(lastUserMessage, botId);
2067 1286 }
2068 1287 }
2069 -
2070 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
2071 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2072 - }
2073 1288 });
2074 1289
2075 1290 if (messageText.id) {
2076 1291 var instance = MxChatInstances.get(botId);
@@ -2155,12 +1370,26 @@
2155 1370 bgColor = botMessageBgColor;
2156 1371 fontColor = botMessageFontColor;
2157 1372 }
2158 1373
2159 - // Always run linkify to convert markdown links and format text.
2160 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
2161 - // to avoid double-processing URLs that are already inside <a> tags).
2162 - var fullMessage = linkify(responseText);
1374 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1375 + // This prevents double-processing of URLs that are already formatted as HTML
1376 + var fullMessage;
1377 + if (sender === "user") {
1378 + // Always linkify user messages (they're plain text)
1379 + fullMessage = linkify(responseText);
1380 + } else {
1381 + // For bot/agent messages, check if HTML already exists
1382 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1383 + responseText.includes('<img') || responseText.includes('<div') ||
1384 + responseText.includes('<p>') || responseText.includes('<br>')) {
1385 + // Response already has HTML, don't process it
1386 + fullMessage = responseText;
1387 + } else {
1388 + // Plain text response, apply linkify
1389 + fullMessage = linkify(responseText);
1390 + }
1391 + }
2163 1392
2164 1393 if (responseHtml) {
2165 1394 // Only add line breaks if there's actual text content before the HTML
2166 1395 if (fullMessage && fullMessage.trim()) {
@@ -2217,12 +1446,8 @@
2217 1446 }
2218 1447
2219 1448 // Re-enable chat input after response is displayed
2220 1449 enableChatInput(botId);
2221 -
2222 - if (sender === "bot" || sender === "agent") {
2223 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2224 - }
2225 1450 } else {
2226 1451 appendMessage(sender, responseText, responseHtml, images, false, botId);
2227 1452 // Re-enable chat input after response is displayed
2228 1453 enableChatInput(botId);
@@ -2231,15 +1456,8 @@
2231 1456
2232 1457
2233 1458 function appendThinkingMessage(botId) {
2234 1459 botId = botId || 'default';
2235 -
2236 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
2237 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
2238 - if (indicator && indicator.textContent === 'Live Agent') {
2239 - return;
2240 - }
2241 -
2242 1460 var $chatBox = getElement(botId, 'chat-box');
2243 1461
2244 1462 // Remove any existing thinking dots in this bot's chat first
2245 1463 $chatBox.find('.thinking-dots').remove();
@@ -2261,9 +1479,9 @@
2261 1479 '</div>' +
2262 1480 '</div>';
2263 1481
2264 1482 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
2265 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1483 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
2266 1484 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
2267 1485 scrollToBottom(botId);
2268 1486 }
2269 1487
@@ -2269,11 +1487,9 @@
2269 1487
2270 1488 function removeThinkingDots(botId) {
2271 1489 botId = botId || 'default';
2272 1490 var $chatBox = getElement(botId, 'chat-box');
2273 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
2274 1491 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
2275 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
2276 1492 }
2277 1493
2278 1494 // ====================================
2279 1495 // TEXT FORMATTING & PROCESSING
@@ -2307,12 +1523,9 @@
2307 1523 processedText = formatTextStyling(processedText);
2308 1524
2309 1525 // Process code blocks BEFORE processing links
2310 1526 processedText = formatCodeBlocks(processedText);
2311 -
2312 - // Process markdown tables BEFORE converting newlines to paragraphs
2313 - processedText = formatMarkdownTables(processedText);
2314 -
1527 +
2315 1528 // NOW convert to paragraphs
2316 1529 processedText = convertNewlinesToBreaks(processedText);
2317 1530
2318 1531 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -2325,63 +1538,37 @@
2325 1538 // Return as a proper link without the brackets
2326 1539 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2327 1540 });
2328 1541
2329 - // Process markdown links: [text](url) and [](url)
2330 - // Uses balanced parenthesis matching to handle URLs containing parens
2331 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
2332 - processedText = (function(input) {
2333 - var result = '';
2334 - var i = 0;
2335 - while (i < input.length) {
2336 - // Look for [ at current position
2337 - if (input[i] === '[') {
2338 - // Find closing ]
2339 - var closeBracket = input.indexOf(']', i + 1);
2340 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
2341 - result += input[i];
2342 - i++;
2343 - continue;
2344 - }
2345 - var linkText = input.substring(i + 1, closeBracket);
2346 - // Check if URL starts with http
2347 - var urlStart = closeBracket + 2;
2348 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
2349 - result += input[i];
2350 - i++;
2351 - continue;
2352 - }
2353 - // Find balanced closing paren
2354 - var depth = 1;
2355 - var j = urlStart;
2356 - while (j < input.length && depth > 0) {
2357 - if (input[j] === '(') depth++;
2358 - else if (input[j] === ')') depth--;
2359 - if (depth > 0) j++;
2360 - }
2361 - if (depth !== 0) {
2362 - result += input[i];
2363 - i++;
2364 - continue;
2365 - }
2366 - var url = input.substring(urlStart, j);
2367 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
2368 - var encodedUrl = safeEncodeUrl(cleanUrl);
2369 - if (!linkText || !linkText.trim()) {
2370 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
2371 - } else {
2372 - var safeText = sanitizeUserInput(linkText);
2373 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
2374 - }
2375 - i = j + 1; // Skip past the closing )
2376 - } else {
2377 - result += input[i];
2378 - i++;
2379 - }
1542 + // Process proper markdown links with text: [text](url)
1543 + // This MUST have non-empty text in the first brackets
1544 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1545 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1546 + // Make sure we have actual text (not just whitespace)
1547 + if (!text || !text.trim()) {
1548 + // If no text, treat the URL as the text
1549 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1550 + const safeUrl = safeEncodeUrl(cleanUrl);
1551 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2380 1552 }
2381 - return result;
2382 - })(processedText);
1553 +
1554 + // Clean the URL
1555 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1556 + const safeUrl = safeEncodeUrl(cleanUrl);
1557 + const safeText = sanitizeUserInput(text);
1558 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1559 + });
2383 1560
1561 + // Handle empty markdown links: [](url)
1562 + // This is a specific case where there's no text
1563 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1564 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1565 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1566 + const safeUrl = safeEncodeUrl(cleanUrl);
1567 + // Use the URL itself as the link text
1568 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1569 + });
1570 +
2384 1571 // Process phone numbers: [text](tel:number)
2385 1572 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
2386 1573 processedText = processedText.replace(phonePattern, (match, text, phone) => {
2387 1574 const safePhone = safeEncodeUrl(phone);
@@ -2533,78 +1720,9 @@
2533 1720 });
2534 1721
2535 1722 return text;
2536 1723 }
2537 -
2538 - function formatMarkdownTables(text) {
2539 - var lines = text.split('\n');
2540 - var result = [];
2541 - var i = 0;
2542 -
2543 - while (i < lines.length) {
2544 - // Check for a table: current line has pipes AND next line is a separator row
2545 - if (i + 1 < lines.length &&
2546 - lines[i].indexOf('|') !== -1 &&
2547 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2548 -
2549 - var tableLines = [];
2550 - var headerLine = lines[i];
2551 - var separatorLine = lines[i + 1];
2552 - tableLines.push(headerLine);
2553 - tableLines.push(separatorLine);
2554 -
2555 - // Collect remaining table rows
2556 - var j = i + 2;
2557 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2558 - tableLines.push(lines[j]);
2559 - j++;
2560 - }
2561 -
2562 - // Parse alignment from separator row
2563 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2564 - var alignments = sepCells.map(function(cell) {
2565 - var trimmed = cell.trim();
2566 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2567 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2568 - return 'left';
2569 - });
2570 -
2571 - // Build HTML table
2572 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2573 -
2574 - // Header row
2575 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2576 - html += '<thead><tr>';
2577 - headerCells.forEach(function(cell, idx) {
2578 - var align = alignments[idx] || 'left';
2579 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2580 - });
2581 - html += '</tr></thead>';
2582 -
2583 - // Body rows
2584 - html += '<tbody>';
2585 - for (var r = 2; r < tableLines.length; r++) {
2586 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2587 - html += '<tr>';
2588 - rowCells.forEach(function(cell, idx) {
2589 - var align = alignments[idx] || 'left';
2590 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2591 - });
2592 - html += '</tr>';
2593 - }
2594 - html += '</tbody></table></div>';
2595 -
2596 - result.push(html);
2597 - i = j;
2598 - } else {
2599 - result.push(lines[i]);
2600 - i++;
2601 - }
2602 - }
2603 -
2604 - return result.join('\n');
2605 - }
2606 -
1724 +
2607 1725 function sanitizeUserInput(text) {
2608 1726 const div = document.createElement('div');
2609 1727 div.textContent = text;
2610 1728 return div.innerHTML;
@@ -2675,14 +1793,13 @@
2675 1793 requestAnimationFrame(smoothScroll);
2676 1794 }
2677 1795 }
2678 1796
2679 - function scrollElementToTop(element, botId, topOffset) {
1797 + function scrollElementToTop(element, botId) {
2680 1798 botId = botId || 'default';
2681 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2682 1799 var chatBox = getElement(botId, 'chat-box');
2683 1800 var elementTop = element.position().top + chatBox.scrollTop();
2684 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1801 + chatBox.animate({ scrollTop: elementTop }, 500);
2685 1802 }
2686 1803
2687 1804 function showChatWidget(botId) {
2688 1805 botId = botId || 'default';
@@ -2826,12 +1943,15 @@
2826 1943 // LIVE AGENT FUNCTIONALITY
2827 1944 // ====================================
2828 1945
2829 1946 function startPolling(botId) {
1947 + console.log('[MxChat] startPolling called for botId:', botId);
2830 1948 botId = botId || 'default';
2831 1949 var instance = MxChatInstances.get(botId);
2832 1950 // Clear any existing interval first
2833 1951 stopPolling(botId);
1952 + // Start new polling interval
1953 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2834 1954 instance.pollingInterval = setInterval(function() {
2835 1955 checkForAgentMessages(botId);
2836 1956 }, 5000);
2837 1957 }
@@ -2836,17 +1956,20 @@
2836 1956 }, 5000);
2837 1957 }
2838 1958
2839 1959 function stopPolling(botId) {
1960 + console.log('[MxChat] stopPolling called for botId:', botId);
2840 1961 botId = botId || 'default';
2841 1962 var instance = MxChatInstances.get(botId);
2842 1963 if (instance.pollingInterval) {
2843 1964 clearInterval(instance.pollingInterval);
2844 1965 instance.pollingInterval = null;
1966 + console.log('[MxChat] Polling stopped for botId:', botId);
2845 1967 }
2846 1968 }
2847 1969
2848 1970 function checkForAgentMessages(botId) {
1971 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2849 1972 botId = botId || 'default';
2850 1973 var instance = MxChatInstances.get(botId);
2851 1974 const sessionId = getChatSession(botId);
2852 1975 $.ajax({
@@ -2872,12 +1995,8 @@
2872 1995 instance.processedMessageIds.add(message.id);
2873 1996 }
2874 1997 });
2875 1998
2876 - if (hasNewMessage) {
2877 - enableChatInput(botId);
2878 - }
2879 -
2880 1999 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2881 2000 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2882 2001 showNotification(botId);
2883 2002 }
@@ -2883,13 +2002,8 @@
2883 2002 }
2884 2003
2885 2004 scrollToBottom(botId, true);
2886 2005 }
2887 -
2888 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2889 - if (response.success && response.data?.chat_mode) {
2890 - updateChatModeIndicator(response.data.chat_mode, botId);
2891 - }
2892 2006 },
2893 2007 error: function (xhr, status, error) {
2894 2008 // Polling error - silently continue
2895 2009 }
@@ -2899,29 +2013,20 @@
2899 2013 // ====================================
2900 2014 // CHAT HISTORY & PERSISTENCE
2901 2015 // ====================================
2902 2016
2903 -function loadChatHistory(botId, onComplete) {
2017 +function loadChatHistory(botId) {
2904 2018 botId = botId || 'default';
2905 2019 var instance = MxChatInstances.get(botId);
2906 2020
2907 2021 // Prevent duplicate loading
2908 2022 if (instance.chatHistoryLoaded) {
2909 - if (onComplete) onComplete();
2910 2023 return;
2911 2024 }
2912 2025
2913 - // Use getChatSession which returns null if no session exists (does NOT create one)
2914 2026 var sessionId = getChatSession(botId);
2915 2027 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2916 2028
2917 - // No session yet — nothing to load. History will load after first message via ensureSession.
2918 - if (!sessionId) {
2919 - instance.chatHistoryLoaded = true;
2920 - if (onComplete) onComplete();
2921 - return;
2922 - }
2923 -
2924 2029 if (chatPersistenceEnabled && sessionId) {
2925 2030 $.ajax({
2926 2031 url: mxchatChat.ajax_url,
2927 2032 type: 'POST',
@@ -2932,12 +2037,11 @@
2932 2037 },
2933 2038 success: function(response) {
2934 2039 // Handle session reset (IP changed while user was away)
2935 2040 if (response.success === false && response.data && response.data.action === 'reset_session') {
2936 - // Silent reset — new session but don't clear UI
2937 - MxChatInstances.silentResetSession(botId);
2041 + // Silently reset session - user will start fresh
2042 + resetChatSession(botId);
2938 2043 instance.chatHistoryLoaded = true; // Prevent retry loop
2939 - if (onComplete) onComplete();
2940 2044 return;
2941 2045 }
2942 2046
2943 2047 // Check if the response indicates success
@@ -2993,19 +2097,9 @@
2993 2097 var content = message.content;
2994 2098 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2995 2099 content = decodeHTMLEntities(content);
2996 2100
2997 - // Skip linkify for messages containing structured HTML
2998 - // (forms, product cards, galleries, etc.) to avoid
2999 - // markdown formatting corrupting HTML attributes
3000 - // (e.g. underscores in name="field_name" becoming <em> tags)
3001 - if (content.includes("mxchat-product-card") ||
3002 - content.includes("mxchat-image-gallery") ||
3003 - content.includes("mxchat-featured-products") ||
3004 - content.includes("<form") ||
3005 - content.includes("<input") ||
3006 - content.includes("<select") ||
3007 - content.includes("<textarea")) {
2101 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
3008 2102 messageElement.html(content);
3009 2103 } else {
3010 2104 var formattedContent = linkify(content);
3011 2105 messageElement.html(formattedContent);
@@ -3045,17 +2139,13 @@
3045 2139 instance.chatHistoryLoaded = true;
3046 2140 }
3047 2141 }
3048 2142 }
3049 - if (onComplete) onComplete();
3050 2143 },
3051 2144 error: function(xhr, status, error) {
3052 2145 // Error loading chat history - silently continue
3053 - if (onComplete) onComplete();
3054 2146 }
3055 2147 });
3056 - } else {
3057 - if (onComplete) onComplete();
3058 2148 }
3059 2149 }
3060 2150
3061 2151
@@ -3119,10 +2209,10 @@
3119 2209 .then(data => {
3120 2210 if (data.success) {
3121 2211 container.style.display = 'none';
3122 2212 nameElement.textContent = '';
3123 - instance.activePdfFile = null;
3124 - appendMessage('bot', 'PDF removed.', '', [], false, botId);
2213 + activePdfFile = null;
2214 + appendMessage('bot', 'PDF removed.');
3125 2215 }
3126 2216 })
3127 2217 .catch(error => {
3128 2218 // Error removing PDF - silently continue
@@ -3128,16 +2218,14 @@
3128 2218 // Error removing PDF - silently continue
3129 2219 });
3130 2220 }
3131 2221
3132 - function removeActiveWord(botId) {
3133 - botId = botId || 'default';
3134 - var instance = MxChatInstances.get(botId);
3135 - const container = getElementDOM(botId, 'active-word-container');
3136 - const nameElement = getElementDOM(botId, 'active-word-name');
3137 -
3138 - if (!container || !nameElement || !instance.activeWordFile) return;
3139 -
2222 + function removeActiveWord() {
2223 + const container = document.getElementById('active-word-container');
2224 + const nameElement = document.getElementById('active-word-name');
2225 +
2226 + if (!container || !nameElement || !activeWordFile) return;
2227 +
3140 2228 fetch(mxchatChat.ajax_url, {
3141 2229 method: 'POST',
3142 2230 headers: {
3143 2231 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3143,9 +2231,9 @@
3143 2231 'Content-Type': 'application/x-www-form-urlencoded',
3144 2232 },
3145 2233 body: new URLSearchParams({
3146 2234 'action': 'mxchat_remove_word',
3147 - 'session_id': getChatSession(botId),
2235 + 'session_id': sessionId,
3148 2236 'nonce': mxchatChat.nonce
3149 2237 })
3150 2238 })
3151 2239 .then(response => response.json())
@@ -3152,10 +2240,10 @@
3152 2240 .then(data => {
3153 2241 if (data.success) {
3154 2242 container.style.display = 'none';
3155 2243 nameElement.textContent = '';
3156 - instance.activeWordFile = null;
3157 - appendMessage('bot', 'Word document removed.', '', [], false, botId);
2244 + activeWordFile = null;
2245 + appendMessage('bot', 'Word document removed.');
3158 2246 }
3159 2247 })
3160 2248 .catch(error => {
3161 2249 // Error removing Word document - silently continue
@@ -3233,35 +2321,45 @@
3233 2321 // ====================================
3234 2322
3235 2323 function checkPreChatDismissal(botId) {
3236 2324 botId = botId || 'default';
3237 - try {
3238 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
3239 - if (dismissedAt) {
3240 - // Re-show after 24 hours
3241 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
3242 - if (elapsed < 86400000) {
2325 + $.ajax({
2326 + url: mxchatChat.ajax_url,
2327 + type: 'POST',
2328 + data: {
2329 + action: 'mxchat_check_pre_chat_message_status',
2330 + _ajax_nonce: mxchatChat.nonce
2331 + },
2332 + success: function(response) {
2333 + if (response.success && !response.data.dismissed) {
2334 + getElement(botId, 'pre-chat-message').fadeIn(250);
2335 + } else {
3243 2336 getElement(botId, 'pre-chat-message').hide();
3244 - return;
3245 2337 }
3246 - // Expired — clear and show again
3247 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2338 + },
2339 + error: function() {
2340 + // Error checking pre-chat dismissal - silently continue
3248 2341 }
3249 - getElement(botId, 'pre-chat-message').fadeIn(250);
3250 - } catch (e) {
3251 - // localStorage unavailable — show the message
3252 - getElement(botId, 'pre-chat-message').fadeIn(250);
3253 - }
2342 + });
3254 2343 }
3255 2344
3256 2345 function handlePreChatDismissal(botId) {
3257 2346 botId = botId || 'default';
3258 2347 getElement(botId, 'pre-chat-message').fadeOut(200);
3259 - try {
3260 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
3261 - } catch (e) {
3262 - // localStorage unavailable — dismissal won't persist
3263 - }
2348 + $.ajax({
2349 + url: mxchatChat.ajax_url,
2350 + type: 'POST',
2351 + data: {
2352 + action: 'mxchat_dismiss_pre_chat_message',
2353 + _ajax_nonce: mxchatChat.nonce
2354 + },
2355 + success: function() {
2356 + $('#pre-chat-message').hide();
2357 + },
2358 + error: function() {
2359 + // Error dismissing pre-chat message - silently continue
2360 + }
2361 + });
3264 2362 }
3265 2363
3266 2364
3267 2365 // ====================================
@@ -3316,14 +2414,9 @@
3316 2414 collapseQuickQuestions(botId);
3317 2415 });
3318 2416
3319 2417 // Chatbot visibility toggle handlers - use class selector for multi-instance support
3320 - // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
3321 - $(document).on('click keydown', '.floating-chatbot-button', function(e) {
3322 - if (e.type === 'keydown') {
3323 - if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
3324 - e.preventDefault();
3325 - }
2418 + $(document).on('click', '.floating-chatbot-button', function() {
3326 2419 var botId = getBotIdFromElement(this);
3327 2420 var $chatbot = getElement(botId, 'floating-chatbot');
3328 2421 var $badge = getElement(botId, 'chat-notification-badge');
3329 2422 var $preChat = getElement(botId, 'pre-chat-message');
@@ -3328,88 +2421,35 @@
3328 2421 var $badge = getElement(botId, 'chat-notification-badge');
3329 2422 var $preChat = getElement(botId, 'pre-chat-message');
3330 2423
3331 2424 if ($chatbot.hasClass('hidden')) {
3332 - $chatbot.removeClass('hidden').addClass('visible')
3333 - .attr('aria-modal', 'true').attr('role', 'dialog');
3334 - $(this).addClass('hidden').attr('aria-expanded', 'true');
2425 + $chatbot.removeClass('hidden').addClass('visible');
2426 + $(this).addClass('hidden');
3335 2427 $badge.hide(); // Hide notification when opening chat
3336 2428 disableScroll();
3337 2429 $preChat.fadeOut(250);
3338 -
3339 - // First open per page load: re-fetch behavior settings in case
3340 - // this page's inline values came from a stale full-page cache
3341 - // (plan-32db95). Idempotent — later opens are a no-op.
3342 - mxchatRefreshDynamicSettings();
3343 -
3344 - // Load chat history for returning visitors (persistence)
3345 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3346 - if (chatPersistenceEnabled) {
3347 - MxChatInstances.ensureSession(botId);
3348 - }
3349 -
3350 - // Deferred email check — only on first widget open
3351 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3352 - var instance = MxChatInstances.get(botId);
3353 - if (emailBlocker && !instance.emailCheckDone) {
3354 - instance.emailCheckDone = true;
3355 - resolveEmailState(botId);
3356 - } else if (!emailBlocker) {
3357 - // No email collection — still route through showChatContainerForBot
3358 - // so the loader is shown while chat history loads
3359 - showChatContainerForBot(botId);
3360 - }
3361 -
3362 - // Move keyboard focus into the message input after the open transition.
3363 - setTimeout(function() {
3364 - var chatInput = getElementDOM(botId, 'chat-input');
3365 - if (chatInput && !chatInput.disabled) {
3366 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
3367 - }
3368 - }, 300);
3369 2430 } else {
3370 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3371 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2431 + $chatbot.removeClass('visible').addClass('hidden');
2432 + $(this).removeClass('hidden');
3372 2433 enableScroll();
3373 2434 checkPreChatDismissal(botId);
3374 2435 }
3375 2436 });
3376 2437
3377 - // Allow clicking anywhere on the title bar to close the chatbot.
3378 - // Returns keyboard focus to the launcher so keyboard users don't get
3379 - // stranded at <body> (WCAG SC 2.4.3 Focus Order). :focus-visible is
3380 - // heuristic-based so mouse-triggered close won't show a focus ring.
2438 + // Allow clicking anywhere on the title bar to close the chatbot
3381 2439 $(document).on('click', '.chatbot-top-bar', function() {
3382 2440 var botId = getBotIdFromElement(this);
3383 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3384 - var $launcher = getElement(botId, 'floating-chatbot-button');
3385 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
2441 + getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2442 + getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3386 2443 enableScroll();
3387 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3388 2444 });
3389 2445
3390 - // Global Escape-key handler — closes any visible chat widget and
3391 - // returns focus to its launcher. Standard modal-dismissal pattern;
3392 - // pairs with aria-modal="true" set on the widget when it opens.
3393 - $(document).on('keydown', function(e) {
3394 - if (e.key !== 'Escape' && e.key !== 'Esc') return;
3395 - var $visible = $('.floating-chatbot.visible');
3396 - if (!$visible.length) return;
3397 - e.preventDefault();
3398 - $visible.each(function() {
3399 - var botId = getBotIdFromElement(this);
3400 - $(this).addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3401 - var $launcher = getElement(botId, 'floating-chatbot-button');
3402 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
3403 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3404 - });
3405 - enableScroll();
3406 - });
3407 -
3408 2446 $(document).on('click', '.close-pre-chat-message', function(e) {
3409 2447 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3410 2448 var botId = getBotIdFromElement(this);
3411 - handlePreChatDismissal(botId);
2449 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2450 + $(this).remove();
2451 + });
3412 2452 });
3413 2453
3414 2454
3415 2455 // PDF upload button handlers - use class selector
@@ -3425,20 +2465,17 @@
3425 2465 var wordInput = getElementDOM(botId, 'word-upload');
3426 2466 if (wordInput) wordInput.click();
3427 2467 });
3428 2468
3429 - // PDF file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'pdf-upload')
3430 - $(document).on('change', '.pdf-upload', async function(e) {
3431 - var botId = getBotIdFromElement(this);
3432 - var instance = MxChatInstances.get(botId);
3433 - const file = this.files[0];
3434 - const sessionId = MxChatInstances.ensureSession(botId);
3435 -
2469 + // PDF file input change handler
2470 + addSafeEventListener('pdf-upload', 'change', async function(e) {
2471 + const file = e.target.files[0];
2472 +
3436 2473 if (!file || file.type !== 'application/pdf') {
3437 2474 alert('Please select a valid PDF file.');
3438 2475 return;
3439 2476 }
3440 -
2477 +
3441 2478 if (!sessionId) {
3442 2479 alert('Error: No session ID found');
3443 2480 return;
3444 2481 }
@@ -3446,49 +2483,47 @@
3446 2483 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3447 2484 alert('Error: Ajax configuration missing');
3448 2485 return;
3449 2486 }
3450 -
2487 +
3451 2488 // Disable buttons and show loading state
3452 - const uploadBtn = getElementDOM(botId, 'pdf-upload-btn');
3453 - const sendBtn = getElementDOM(botId, 'send-button');
3454 - if (!uploadBtn) return;
2489 + const uploadBtn = document.getElementById('pdf-upload-btn');
2490 + const sendBtn = document.getElementById('send-button');
3455 2491 const originalBtnContent = uploadBtn.innerHTML;
3456 -
2492 +
3457 2493 try {
3458 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3459 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3460 2494 const formData = new FormData();
3461 2495 formData.append('action', 'mxchat_upload_pdf');
3462 2496 formData.append('pdf_file', file);
3463 2497 formData.append('session_id', sessionId);
3464 2498 formData.append('nonce', mxchatChat.nonce);
3465 -
2499 +
3466 2500 uploadBtn.disabled = true;
3467 - if (sendBtn) sendBtn.disabled = true;
2501 + sendBtn.disabled = true;
3468 2502 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3469 2503 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3470 2504 </svg>`;
3471 -
2505 +
3472 2506 const response = await fetch(mxchatChat.ajax_url, {
3473 2507 method: 'POST',
3474 2508 body: formData
3475 2509 });
3476 -
2510 +
3477 2511 const data = await response.json();
3478 -
2512 +
3479 2513 if (data.success) {
3480 2514 // Hide popular questions if they exist
3481 - if (hasQuickQuestions(botId)) {
3482 - collapseQuickQuestions(botId);
2515 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2516 + if (hasQuickQuestions()) {
2517 + collapseQuickQuestions();
3483 2518 }
3484 -
2519 +
3485 2520 // Show the active PDF name
3486 - showActivePdf(data.data.filename, botId);
3487 -
3488 - appendMessage('bot', data.data.message, '', [], false, botId);
3489 - scrollToBottom(botId);
3490 - instance.activePdfFile = data.data.filename;
2521 + showActivePdf(data.data.filename);
2522 +
2523 + appendMessage('bot', data.data.message);
2524 + scrollToBottom();
2525 + activePdfFile = data.data.filename;
3491 2526 } else {
3492 2527 alert('Failed to upload PDF. Please try again.');
3493 2528 }
3494 2529 } catch (error) {
@@ -3494,76 +2529,66 @@
3494 2529 } catch (error) {
3495 2530 alert('Error uploading file. Please try again.');
3496 2531 } finally {
3497 2532 uploadBtn.disabled = false;
3498 - if (sendBtn) sendBtn.disabled = false;
2533 + sendBtn.disabled = false;
3499 2534 uploadBtn.innerHTML = originalBtnContent;
3500 2535 this.value = ''; // Reset file input
3501 2536 }
3502 2537 });
3503 2538
3504 - // Word file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'word-upload')
3505 - $(document).on('change', '.word-upload', async function(e) {
3506 - var botId = getBotIdFromElement(this);
3507 - var instance = MxChatInstances.get(botId);
3508 - const file = this.files[0];
3509 - const sessionId = MxChatInstances.ensureSession(botId);
3510 -
2539 + // Word file input change handler
2540 + addSafeEventListener('word-upload', 'change', async function(e) {
2541 + const file = e.target.files[0];
2542 +
3511 2543 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
3512 2544 alert('Please select a valid Word document (.docx).');
3513 2545 return;
3514 2546 }
3515 -
2547 +
3516 2548 if (!sessionId) {
3517 2549 alert('Error: No session ID found');
3518 2550 return;
3519 2551 }
3520 2552
3521 - if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3522 - alert('Error: Ajax configuration missing');
3523 - return;
3524 - }
3525 -
3526 2553 // Disable buttons and show loading state
3527 - const uploadBtn = getElementDOM(botId, 'word-upload-btn');
3528 - const sendBtn = getElementDOM(botId, 'send-button');
3529 - if (!uploadBtn) return;
2554 + const uploadBtn = document.getElementById('word-upload-btn');
2555 + const sendBtn = document.getElementById('send-button');
3530 2556 const originalBtnContent = uploadBtn.innerHTML;
3531 -
2557 +
3532 2558 try {
3533 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3534 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3535 2559 const formData = new FormData();
3536 2560 formData.append('action', 'mxchat_upload_word');
3537 2561 formData.append('word_file', file);
3538 2562 formData.append('session_id', sessionId);
3539 2563 formData.append('nonce', mxchatChat.nonce);
3540 -
2564 +
3541 2565 uploadBtn.disabled = true;
3542 - if (sendBtn) sendBtn.disabled = true;
2566 + sendBtn.disabled = true;
3543 2567 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3544 2568 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3545 2569 </svg>`;
3546 -
2570 +
3547 2571 const response = await fetch(mxchatChat.ajax_url, {
3548 2572 method: 'POST',
3549 2573 body: formData
3550 2574 });
3551 -
2575 +
3552 2576 const data = await response.json();
3553 -
2577 +
3554 2578 if (data.success) {
3555 2579 // Hide popular questions if they exist
3556 - if (hasQuickQuestions(botId)) {
3557 - collapseQuickQuestions(botId);
2580 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2581 + if (hasQuickQuestions()) {
2582 + collapseQuickQuestions();
3558 2583 }
3559 -
2584 +
3560 2585 // Show the active Word document name
3561 - showActiveWord(data.data.filename, botId);
3562 -
3563 - appendMessage('bot', data.data.message, '', [], false, botId);
3564 - scrollToBottom(botId);
3565 - instance.activeWordFile = data.data.filename;
2586 + showActiveWord(data.data.filename);
2587 +
2588 + appendMessage('bot', data.data.message);
2589 + scrollToBottom();
2590 + activeWordFile = data.data.filename;
3566 2591 } else {
3567 2592 alert('Failed to upload Word document. Please try again.');
3568 2593 }
3569 2594 } catch (error) {
@@ -3569,25 +2594,25 @@
3569 2594 } catch (error) {
3570 2595 alert('Error uploading file. Please try again.');
3571 2596 } finally {
3572 2597 uploadBtn.disabled = false;
3573 - if (sendBtn) sendBtn.disabled = false;
2598 + sendBtn.disabled = false;
3574 2599 uploadBtn.innerHTML = originalBtnContent;
3575 2600 this.value = ''; // Reset file input
3576 2601 }
3577 2602 });
3578 2603
3579 - // Remove button click handlers - delegated, bot-aware (were bound to stale un-suffixed ids)
3580 - $(document).on('click', '.remove-pdf-btn', function(e) {
2604 + // Remove button click handlers
2605 + document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
3581 2606 e.preventDefault();
3582 2607 e.stopPropagation();
3583 - removeActivePdf(getBotIdFromElement(this));
2608 + removeActivePdf();
3584 2609 });
3585 -
3586 - $(document).on('click', '.remove-word-btn', function(e) {
2610 +
2611 + document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
3587 2612 e.preventDefault();
3588 2613 e.stopPropagation();
3589 - removeActiveWord(getBotIdFromElement(this));
2614 + removeActiveWord();
3590 2615 });
3591 2616
3592 2617 // Window resize handlers
3593 2618 $(window).on('resize orientationchange', function() {
@@ -3625,59 +2650,8 @@
3625 2650 });
3626 2651
3627 2652
3628 2653 // ====================================
3629 -// INIT LOADER & CHAT CONTAINER HELPERS
3630 -// ====================================
3631 -// These must be outside the email collection block so they're always available
3632 -// (used by persistence loading even when email collection is off)
3633 -
3634 -function showInitLoader(botId) {
3635 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3636 - if (loader) loader.style.display = 'flex';
3637 -}
3638 -
3639 -function hideInitLoader(botId) {
3640 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3641 - if (loader) loader.style.display = 'none';
3642 -}
3643 -
3644 -function showEmailFormForBot(botId) {
3645 - hideInitLoader(botId);
3646 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3647 - var chatContainer = getElementDOM(botId, 'chat-container');
3648 - if (emailBlocker) emailBlocker.style.display = 'flex';
3649 - if (chatContainer) chatContainer.style.display = 'none';
3650 -}
3651 -
3652 -function showChatContainerForBot(botId) {
3653 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3654 - var chatContainer = getElementDOM(botId, 'chat-container');
3655 - if (emailBlocker) emailBlocker.style.display = 'none';
3656 -
3657 - var instance = MxChatInstances.get(botId);
3658 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3659 -
3660 - // If persistence is on and history hasn't loaded yet, show loader
3661 - // while history loads to prevent flash of empty chat
3662 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3663 - if (chatContainer) chatContainer.style.display = 'none';
3664 - showInitLoader(botId);
3665 - loadChatHistory(botId, function() {
3666 - hideInitLoader(botId);
3667 - if (chatContainer) chatContainer.style.display = 'flex';
3668 - scrollToBottom(botId, true);
3669 - });
3670 - } else {
3671 - hideInitLoader(botId);
3672 - if (chatContainer) chatContainer.style.display = 'flex';
3673 - if (typeof loadChatHistory === 'function') {
3674 - loadChatHistory(botId);
3675 - }
3676 - }
3677 -}
3678 -
3679 -// ====================================
3680 2654 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3681 2655 // ====================================
3682 2656 // Only run email collection setup if it's enabled
3683 2657 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3713,8 +2687,28 @@
3713 2687 `;
3714 2688 document.head.appendChild(style);
3715 2689 }
3716 2690
2691 + // Helper functions for email collection (multi-instance aware)
2692 + function showEmailFormForBot(botId) {
2693 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2694 + var chatContainer = getElementDOM(botId, 'chat-container');
2695 + if (emailBlocker) emailBlocker.style.display = 'flex';
2696 + if (chatContainer) chatContainer.style.display = 'none';
2697 + }
2698 +
2699 + function showChatContainerForBot(botId) {
2700 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2701 + var chatContainer = getElementDOM(botId, 'chat-container');
2702 + if (emailBlocker) emailBlocker.style.display = 'none';
2703 + if (chatContainer) chatContainer.style.display = 'flex';
2704 +
2705 + // Load chat history for this bot
2706 + if (typeof loadChatHistory === 'function') {
2707 + loadChatHistory(botId);
2708 + }
2709 + }
2710 +
3717 2711 function isValidEmailAddress(email) {
3718 2712 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3719 2713 return emailRegex.test(email.trim()) && email.length <= 254;
3720 2714 }
@@ -3836,31 +2830,11 @@
3836 2830 existingErrors.forEach(error => error.remove());
3837 2831 }
3838 2832 }
3839 2833
3840 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3841 - function resolveEmailState(botId) {
3842 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3843 - if (mxchatChat.initial_email_state.show_email_form) {
3844 - showEmailFormForBot(botId);
3845 - } else {
3846 - showChatContainerForBot(botId);
3847 - }
3848 - } else {
3849 - checkSessionAndEmailForBot(botId);
3850 - }
3851 - }
3852 -
3853 2834 function checkSessionAndEmailForBot(botId) {
3854 - const sessionId = MxChatInstances.ensureSession(botId);
2835 + const sessionId = getChatSession(botId);
3855 2836
3856 - // Hide both panels while we check — show loader instead
3857 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3858 - var chatContainer = getElementDOM(botId, 'chat-container');
3859 - if (emailBlocker) emailBlocker.style.display = 'none';
3860 - if (chatContainer) chatContainer.style.display = 'none';
3861 - showInitLoader(botId);
3862 -
3863 2837 fetch(mxchatChat.ajax_url, {
3864 2838 method: 'POST',
3865 2839 headers: {
3866 2840 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3908,9 +2882,9 @@
3908 2882 var emailInput = getElementDOM(botId, 'user-email');
3909 2883 var nameInput = getElementDOM(botId, 'user-name');
3910 2884 var userEmail = emailInput ? emailInput.value.trim() : '';
3911 2885 var userName = nameInput ? nameInput.value.trim() : '';
3912 - var sessionId = MxChatInstances.ensureSession(botId);
2886 + var sessionId = getChatSession(botId);
3913 2887
3914 2888 // Validate email
3915 2889 if (!userEmail) {
3916 2890 showEmailError(botId, 'Please enter your email address.');
@@ -4033,27 +3007,25 @@
4033 3007 }
4034 3008 });
4035 3009
4036 3010 // Initialize email check for all bot instances
4037 - // For floating bots: defer until widget is opened (zero passive AJAX)
4038 - // For embedded bots: check immediately since the form is visible
4039 3011 $('.mxchat-chatbot-wrapper').each(function() {
4040 3012 var botId = $(this).data('bot-id') || 'default';
4041 3013 var emailBlocker = getElementDOM(botId, 'email-blocker');
4042 3014
3015 + // Only check if email blocker exists for this bot
4043 3016 if (emailBlocker) {
4044 - if (isEmbeddedBot(botId)) {
4045 - // Embedded bots are always visible — check now
4046 - resolveEmailState(botId);
3017 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3018 + if (mxchatChat.initial_email_state.show_email_form) {
3019 + showEmailFormForBot(botId);
3020 + } else {
3021 + showChatContainerForBot(botId);
3022 + }
3023 + } else {
3024 + setTimeout(function() {
3025 + checkSessionAndEmailForBot(botId);
3026 + }, 100);
4047 3027 }
4048 - // Floating bots: handled in the widget open handler
4049 - } else if (isEmbeddedBot(botId)) {
4050 - // Embedded bot, no email collection — load history with loader
4051 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
4052 - if (chatPersistenceEnabled) {
4053 - MxChatInstances.ensureSession(botId);
4054 - showChatContainerForBot(botId);
4055 - }
4056 3028 }
4057 3029 });
4058 3030 }
4059 3031
@@ -4063,32 +3035,39 @@
4063 3035 var $chatbot = getElement(botId, 'floating-chatbot');
4064 3036 if ($chatbot.hasClass('hidden')) {
4065 3037 $chatbot.removeClass('hidden').addClass('visible');
4066 3038 getElement(botId, 'floating-chatbot-button').addClass('hidden');
4067 - handlePreChatDismissal(botId);
3039 + $(this).fadeOut(250); // Hide pre-chat message
4068 3040 disableScroll(); // Disable scroll when chatbot opens
3041 + }
3042 + });
4069 3043
4070 - // Load chat history for returning visitors (persistence)
4071 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
4072 - if (chatPersistenceEnabled) {
4073 - MxChatInstances.ensureSession(botId);
4074 - }
3044 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
3045 + // This is a fallback for legacy support
3046 + $(document).on('click', '.close-pre-chat-message', function() {
3047 + var botId = getBotIdFromElement(this);
3048 + var $preChat = getElement(botId, 'pre-chat-message');
3049 + $preChat.fadeOut(200); // Hide the message
4075 3050
4076 - // Deferred email check — only on first widget open
4077 - var emailBlocker = getElementDOM(botId, 'email-blocker');
4078 - var instance = MxChatInstances.get(botId);
4079 - if (emailBlocker && !instance.emailCheckDone) {
4080 - instance.emailCheckDone = true;
4081 - resolveEmailState(botId);
4082 - } else if (!emailBlocker) {
4083 - showChatContainerForBot(botId);
3051 + // Send an AJAX request to set the transient flag for 24 hours
3052 + $.ajax({
3053 + url: mxchatChat.ajax_url,
3054 + type: 'POST',
3055 + data: {
3056 + action: 'mxchat_dismiss_pre_chat_message',
3057 + _ajax_nonce: mxchatChat.nonce
3058 + },
3059 + success: function() {
3060 + // Ensure the message is hidden after dismissal
3061 + $preChat.hide();
3062 + },
3063 + error: function() {
3064 + // Error dismissing pre-chat message - silently continue
4084 3065 }
4085 - }
3066 + });
4086 3067 });
4087 3068
4088 - // Legacy duplicate close handler removed — handled by single event delegation above
4089 3069
4090 -
4091 3070 function hasQuickQuestions(botId) {
4092 3071 botId = botId || 'default';
4093 3072 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
4094 3073 if (!questionsContainer) return false;
@@ -4224,11 +3203,18 @@
4224 3203 });
4225 3204
4226 3205 // Initialize when document is ready
4227 3206 setFullHeight();
3207 + trackOriginatingPage();
4228 3208
4229 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
4230 - // until the user's first interaction via MxChatInstances.ensureSession()
3209 + // Only load chat history if email collection is disabled
3210 + if (mxchatChat.email_collection_enabled !== 'on') {
3211 + // Load history for all instances
3212 + $('.mxchat-chatbot-wrapper').each(function() {
3213 + var botId = $(this).data('bot-id') || 'default';
3214 + loadChatHistory(botId);
3215 + });
3216 + }
4231 3217
4232 3218 // Initialize chat visibility for all instances
4233 3219 $('.mxchat-chatbot-wrapper').each(function() {
4234 3220 var botId = $(this).data('bot-id') || 'default';
@@ -4281,312 +3267,6 @@
4281 3267 }, 2000);
4282 3268 });
4283 3269 }
4284 3270 }
4285 -});
4286 -
4287 -// ============================================================================
4288 -// SATISFACTION RATING (v3.2.6)
4289 -// ============================================================================
4290 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
4291 -// inactivity following a bot reply. One prompt per session, deduped via
4292 -// localStorage. Runs ONLY when the satisfaction_rating_enabled option is on —
4293 -// the option (default off) is authoritative.
4294 -jQuery(function($) {
4295 - if (typeof mxchatChat === 'undefined') return;
4296 - // wp_localize_script stringifies scalars: a PHP boolean false arrives as
4297 - // '' and true as '1', so this must be an explicit-enable allowlist — the
4298 - // old "disabled when exactly false/'off'" check let '' through and the
4299 - // bubble rendered on sites with the option off/unset (plan-4bba64). PHP
4300 - // now emits 'on'/'off' strings; true/'1'/1 keep cached pre-fix HTML
4301 - // (boolean-true localizations) working.
4302 - // NOTE (plan-32db95): this gate reads the INLINE value at DOM ready and is
4303 - // deliberately NOT re-evaluated after the widget's dynamic-settings refresh
4304 - // merges fresh values over mxchatChat (that merge fires on first widget
4305 - // open, after this module has already decided). Re-evaluating would mean
4306 - // restructuring the whole module to late-bind its listeners — not worth it
4307 - // for a prompt that is at worst stale for one page load on a cached page.
4308 - var sre = mxchatChat.satisfaction_rating_enabled;
4309 - if (sre !== 'on' && sre !== true && sre !== '1' && sre !== 1) return;
4310 -
4311 - // wp_localize_script stringifies ints, so accept both number and numeric string.
4312 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
4313 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
4314 - if (!isFinite(idleSeconds)) idleSeconds = 60;
4315 - if (idleSeconds < 5) idleSeconds = 5;
4316 - if (idleSeconds > 600) idleSeconds = 600;
4317 - var IDLE_MS = idleSeconds * 1000;
4318 - var MIN_BOT_REPLIES = 2;
4319 - var ratingState = {};
4320 -
4321 - function getState(botId) {
4322 - if (!ratingState[botId]) {
4323 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
4324 - }
4325 - return ratingState[botId];
4326 - }
4327 -
4328 - function getSessionId(botId) {
4329 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
4330 - return MxChatInstances.getChatSession(botId);
4331 - }
4332 - return null;
4333 - }
4334 -
4335 - function isAlreadyRated(sessionId) {
4336 - if (!sessionId) return false;
4337 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
4338 - }
4339 -
4340 - function markRated(sessionId) {
4341 - if (!sessionId) return;
4342 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
4343 - }
4344 -
4345 - function esc(s) {
4346 - return String(s == null ? '' : s)
4347 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
4348 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
4349 - }
4350 -
4351 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
4352 - function ratingSkipInlineColors(botId) {
4353 - if (mxchatChat.skip_inline_colors) return true;
4354 - var botAssignments = mxchatChat.bot_theme_assignments || {};
4355 - return botAssignments.hasOwnProperty(botId);
4356 - }
4357 -
4358 - function botBubbleStyleAttr(botId) {
4359 - if (ratingSkipInlineColors(botId)) return '';
4360 - var bg = mxchatChat.bot_message_bg_color;
4361 - var fg = mxchatChat.bot_message_font_color;
4362 - if (!bg && !fg) return '';
4363 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
4364 - }
4365 -
4366 - // Reads the rating bubble's actual computed fg+bg (whatever paints it —
4367 - // the inline color pickers OR the mxchat-theme AI customizer's injected CSS)
4368 - // and paints the filled "Send" pill so it fills with the bot font color and
4369 - // labels in the bubble bg. Mirrors mxchatSyncMenuColors(~:1512) for the read.
4370 - // We paint the submit button DIRECTLY (inline longhand) rather than relying
4371 - // on the CSS rule's var()s: Chromium resolves an INHERITED custom property
4372 - // unreliably inside a descendant's `background`, so a bubble-level var would
4373 - // silently fall back to the literal (white-block bug all over again). Inline
4374 - // longhand always wins. Same transparent-guard as the menu so we never paint
4375 - // a see-through value — in that case the CSS literal fallbacks keep it legible.
4376 - function syncRatingBubbleColors(botId) {
4377 - var $chatBox = getChatBoxByBotId(botId);
4378 - if (!$chatBox || !$chatBox.length) return;
4379 - var bubbleEl = $chatBox.find('.mxchat-rating-bot-bubble').last()[0];
4380 - if (!bubbleEl) return;
4381 - var cs = window.getComputedStyle(bubbleEl);
4382 - var fg = cs.color;
4383 - var bg = cs.backgroundColor;
4384 - var hasFg = fg && fg !== 'rgba(0, 0, 0, 0)' && fg !== 'transparent';
4385 - var hasBg = bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent';
4386 - // Expose on the bubble too, for any inheriting styles / future use.
4387 - if (hasFg) bubbleEl.style.setProperty('--mxchat-bot-fg', fg);
4388 - if (hasBg) bubbleEl.style.setProperty('--mxchat-bot-bg', bg);
4389 - // Paint the Send pill directly — the part that actually fixes the bug.
4390 - var submitEl = bubbleEl.querySelector('.mxchat-rating-submit');
4391 - if (submitEl) {
4392 - if (hasFg) submitEl.style.backgroundColor = fg; // fill = bot font color
4393 - if (hasBg) submitEl.style.color = bg; // label = bubble background
4394 - }
4395 - }
4396 -
4397 - function copy(key) {
4398 - var c = mxchatChat.satisfaction_rating_copy || {};
4399 - var d = {
4400 - question: 'Was this helpful?',
4401 - helpful: 'Helpful',
4402 - not_helpful: 'Not helpful',
4403 - dismiss: 'Dismiss',
4404 - thanks: 'Thanks! Anything we should improve? (optional)',
4405 - placeholder: 'Tell us what could be better…',
4406 - send: 'Send',
4407 - skip: 'Skip',
4408 - saved: 'Thanks for the feedback.'
4409 - };
4410 - return c[key] || d[key];
4411 - }
4412 -
4413 - function thumbUpSvg() {
4414 - 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>';
4415 - }
4416 - function thumbDownSvg() {
4417 - 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>';
4418 - }
4419 -
4420 - function buildPromptHtml(botId) {
4421 - var styleAttr = botBubbleStyleAttr(botId);
4422 - return ''
4423 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4424 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
4425 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
4426 - + '<div class="mxchat-rating-actions">'
4427 - + '<span class="mxchat-rating-buttons">'
4428 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
4429 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
4430 - + '</span>'
4431 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
4432 - + '</div>'
4433 - + '</div>'
4434 - + '</div>';
4435 - }
4436 -
4437 - function buildFeedbackHtml(botId, rating) {
4438 - var styleAttr = botBubbleStyleAttr(botId);
4439 - return ''
4440 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4441 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
4442 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
4443 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
4444 - + '<div class="mxchat-rating-feedback-actions">'
4445 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
4446 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
4447 - + '</div>'
4448 - + '</div>'
4449 - + '</div>';
4450 - }
4451 -
4452 - function buildSavedHtml(botId) {
4453 - var styleAttr = botBubbleStyleAttr(botId);
4454 - return ''
4455 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4456 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
4457 - + '</div>';
4458 - }
4459 -
4460 - function getChatBoxByBotId(botId) {
4461 - var $byId = $('#chat-box-' + botId);
4462 - if ($byId.length) return $byId.first();
4463 - return $('.chat-box').first();
4464 - }
4465 -
4466 - function scrollChatBoxToBottom($chatBox) {
4467 - if (!$chatBox || !$chatBox.length) return;
4468 - $chatBox.scrollTop($chatBox[0].scrollHeight);
4469 - }
4470 -
4471 - function showPrompt(botId) {
4472 - var s = getState(botId);
4473 - if (s.promptShown || s.dismissed) return;
4474 - var sessionId = getSessionId(botId);
4475 - if (!sessionId) return;
4476 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4477 - var $chatBox = getChatBoxByBotId(botId);
4478 - if (!$chatBox.length) return;
4479 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
4480 - $chatBox.append(buildPromptHtml(botId));
4481 - syncRatingBubbleColors(botId);
4482 - s.promptShown = true;
4483 - scrollChatBoxToBottom($chatBox);
4484 - }
4485 -
4486 - function submitRating(botId, rating, feedback) {
4487 - var sessionId = getSessionId(botId);
4488 - if (!sessionId) return;
4489 - $.post(mxchatChat.ajax_url, {
4490 - action: 'mxchat_save_rating',
4491 - session_id: sessionId,
4492 - bot_id: botId,
4493 - rating: rating,
4494 - feedback: feedback || ''
4495 - });
4496 - markRated(sessionId);
4497 - }
4498 -
4499 - function onBotReply(botId) {
4500 - var s = getState(botId);
4501 - s.botReplies += 1;
4502 - if (s.promptShown || s.dismissed) return;
4503 - var sessionId = getSessionId(botId);
4504 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4505 - if (s.botReplies < MIN_BOT_REPLIES) return;
4506 - if (s.idleTimer) clearTimeout(s.idleTimer);
4507 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4508 - }
4509 -
4510 - function onUserMessage(botId) {
4511 - var s = getState(botId);
4512 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4513 - }
4514 -
4515 - function botIdFromChatBox(el) {
4516 - var id = el && el.id ? el.id : '';
4517 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4518 - }
4519 -
4520 - function setupObserver(chatBox) {
4521 - var botId = botIdFromChatBox(chatBox);
4522 - try {
4523 - var observer = new MutationObserver(function(mutations) {
4524 - mutations.forEach(function(m) {
4525 - for (var i = 0; i < m.addedNodes.length; i++) {
4526 - var node = m.addedNodes[i];
4527 - if (!node || node.nodeType !== 1) continue;
4528 - var $n = $(node);
4529 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4530 - 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)
4531 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4532 - }
4533 - });
4534 - });
4535 - observer.observe(chatBox, { childList: true });
4536 - } catch (e) { /* noop */ }
4537 - }
4538 -
4539 - $('.chat-box').each(function() { setupObserver(this); });
4540 -
4541 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4542 - e.preventDefault();
4543 - var $btn = $(this);
4544 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4545 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4546 - var botId = $prompt.data('bot-id') || 'default';
4547 - var rating = parseInt($btn.attr('data-rating'), 10);
4548 - if (rating !== 1 && rating !== -1) return;
4549 - submitRating(botId, rating, '');
4550 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4551 - syncRatingBubbleColors(botId);
4552 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4553 - });
4554 -
4555 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4556 - e.preventDefault();
4557 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4558 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4559 - var botId = $prompt.data('bot-id') || 'default';
4560 - var s = getState(botId);
4561 - s.dismissed = true;
4562 - markRated(getSessionId(botId));
4563 - ($wrap.length ? $wrap : $prompt).remove();
4564 - });
4565 -
4566 - function closeFeedback($fb) {
4567 - var botId = $fb.data('bot-id') || 'default';
4568 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4569 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4570 - syncRatingBubbleColors(botId);
4571 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4572 - }
4573 -
4574 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4575 - e.preventDefault();
4576 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4577 - });
4578 -
4579 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4580 - e.preventDefault();
4581 - var $fb = $(this).closest('.mxchat-rating-feedback');
4582 - var botId = $fb.data('bot-id') || 'default';
4583 - var rating = parseInt($fb.attr('data-rating'), 10);
4584 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4585 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4586 - if (text !== '') {
4587 - submitRating(botId, rating, text);
4588 - }
4589 - closeFeedback($fb);
4590 - });
4591 3271 });
4592 3272