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 -1523 3.2.103.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 // ====================================
@@ -603,106 +375,13 @@
603 375 sendButton.disabled = false;
604 376 sendButton.style.opacity = '1';
605 377 sendButton.style.pointerEvents = 'auto';
606 378 }
607 - // Every completion path re-enables input, so this is the single restore
608 - // point for the streaming Stop affordance (no-op when not in stop mode).
609 - mxchatRestoreSendButton(botId);
610 379 }
611 380
612 -// --- Streaming Stop control -------------------------------------------------
613 -// One live stream handle per bot instance, so Stop on one widget never aborts
614 -// another bot on the same page.
615 -var mxchatActiveStreams = {};
616 -// Original send-button markup, captured once per bot the first time the Stop
617 -// state is shown (never captured while already in stop mode, so a rapid
618 -// stop-then-resend can't save the stop glyph as the "original").
619 -var mxchatSendMarkup = {};
620 -
621 -function mxchatShowStopButton(botId) {
622 - var btn = getElementDOM(botId, 'send-button');
623 - if (!btn) return;
624 - if (!btn.classList.contains('mxchat-stop-mode')) {
625 - mxchatSendMarkup[botId] = {
626 - html: btn.innerHTML,
627 - label: btn.getAttribute('aria-label')
628 - };
629 - }
630 -
631 - // Mirror the send icon's rendered size + color so the stop glyph looks
632 - // native, including custom send images/colors and theme overrides.
633 - var child = btn.querySelector('svg, img');
634 - var size = 25;
635 - var color = '';
636 - if (child) {
637 - var rect = child.getBoundingClientRect();
638 - if (rect.width) {
639 - size = Math.round(Math.min(rect.width, rect.height));
640 - }
641 - var cs = window.getComputedStyle(child);
642 - color = (child.tagName.toLowerCase() === 'svg' ? cs.fill : cs.color) || '';
643 - }
644 - var stopLabel = (typeof mxchatChat !== 'undefined' && mxchatChat.stop_button_label) || 'Stop response';
645 - 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>';
646 - // An add-on's DIRECT send-button handler (e.g. mxchat-vision's rebind) can
647 - // start a stream synchronously while the originating click is still
648 - // bubbling up to our delegated handler. Without this guard, that handler
649 - // reads the just-added stop-mode class as a user Stop press and aborts the
650 - // brand-new stream — the user's message renders but no reply ever fires
651 - // (plan-4bba64 silent message loss). The flag only spans the current event
652 - // dispatch: cleared on the next macrotask, long before a real Stop click.
653 - btn.__mxchatStopJustShown = true;
654 - setTimeout(function () { btn.__mxchatStopJustShown = false; }, 0);
655 - btn.classList.add('mxchat-stop-mode');
656 - btn.setAttribute('aria-label', stopLabel);
657 - btn.setAttribute('title', stopLabel);
658 - // disableChatInput() ran when the turn was sent; the Stop control itself
659 - // must stay clickable while the textarea remains disabled.
660 - btn.disabled = false;
661 - btn.style.opacity = '1';
662 - btn.style.pointerEvents = 'auto';
663 -}
664 -
665 -function mxchatRestoreSendButton(botId) {
666 - var btn = getElementDOM(botId, 'send-button');
667 - var saved = mxchatSendMarkup[botId];
668 - if (!btn || !btn.classList.contains('mxchat-stop-mode') || !saved) return;
669 - btn.innerHTML = saved.html;
670 - btn.classList.remove('mxchat-stop-mode');
671 - btn.removeAttribute('title');
672 - if (saved.label) {
673 - btn.setAttribute('aria-label', saved.label);
674 - }
675 -}
676 -
677 -function mxchatStopStreaming(botId) {
678 - var entry = mxchatActiveStreams[botId];
679 - if (!entry || !entry.controller) return;
680 - entry.aborted = true;
681 - try { entry.controller.abort(); } catch (e) {}
682 -}
683 -
684 -// Returns true when a stream rejection came from an intentional Stop click:
685 -// keep the partial text as the turn's answer — no error UI, no fallback resend.
686 -function mxchatHandleStreamAbort(botId, accumulatedContent, callback) {
687 - var entry = mxchatActiveStreams[botId];
688 - if (!entry || !entry.aborted) return false;
689 - delete mxchatActiveStreams[botId];
690 - if (!accumulatedContent) {
691 - // Stopped before the first chunk: drop the thinking bubble, no orphan message.
692 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
693 - }
694 - enableChatInput(botId); // also restores the send icon
695 - if (callback) {
696 - callback(accumulatedContent || '');
697 - }
698 - return true;
699 -}
700 -
701 381 // Update your existing sendMessage function
702 382 function sendMessage(botId) {
703 383 botId = botId || 'default';
704 - MxChatInstances.ensureSession(botId);
705 384 var $chatInput = getElement(botId, 'chat-input');
706 385 var message = $chatInput.val();
707 386
708 387 // ADD PROMPT HOOK HERE
@@ -710,14 +389,10 @@
710 389 message = customMxChatFilter(message, "prompt");
711 390 }
712 391
713 392 if (message) {
714 - // Don't disable input in live agent mode - let users chat freely
715 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
716 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
717 - if (!isAgentMode) {
718 - disableChatInput(botId);
719 - }
393 + // Disable input while waiting for response
394 + disableChatInput(botId);
720 395
721 396 appendMessage("user", message, '', [], false, botId);
722 397 $chatInput.val('');
723 398 $chatInput.css('height', 'auto');
@@ -745,9 +420,8 @@
745 420
746 421 // Update your existing sendMessageToChatbot function
747 422 function sendMessageToChatbot(message, botId) {
748 423 botId = botId || 'default';
749 - MxChatInstances.ensureSession(botId);
750 424
751 425 // ADD PROMPT HOOK HERE
752 426 if (typeof customMxChatFilter === 'function') {
753 427 message = customMxChatFilter(message, "prompt");
@@ -752,14 +426,10 @@
752 426 if (typeof customMxChatFilter === 'function') {
753 427 message = customMxChatFilter(message, "prompt");
754 428 }
755 429
756 - // Don't disable input in live agent mode - let users chat freely
757 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
758 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
759 - if (!isAgentMode) {
760 - disableChatInput(botId);
761 - }
430 + // Disable input while waiting for response
431 + disableChatInput(botId);
762 432
763 433 var sessionId = getChatSession(botId);
764 434
765 435 if (hasQuickQuestions(botId)) {
@@ -838,15 +508,8 @@
838 508
839 509 function callMxChat(message, callback, botId) {
840 510 botId = botId || getMxChatBotId();
841 511
842 - // Streaming fallbacks land here: drop any leftover stream handle and
843 - // return the button to its send state (no-op for plain non-stream turns).
844 - if (mxchatActiveStreams[botId]) {
845 - delete mxchatActiveStreams[botId];
846 - }
847 - mxchatRestoreSendButton(botId);
848 -
849 512 // Store the message in case we need to retry after session reset
850 513 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
851 514
852 515 // Get page context if contextual awareness is enabled
@@ -854,28 +517,13 @@
854 517
855 518 // Get instance for session start timestamp (used when persistence is OFF)
856 519 var instance = MxChatInstances.get(botId);
857 520
858 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
859 - // and returns the guaranteed-present session id from the in-memory instance even when
860 - // cookie/localStorage writes are silently blocked by the browser.
861 - var sessionId = MxChatInstances.ensureSession(botId);
862 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
863 - // Last-resort generation to ensure we never POST a null marker.
864 - sessionId = generateSessionId();
865 - MxChatInstances.setChatSession(botId, sessionId);
866 - }
867 -
868 - // Wait for the page-cache nonce refresh to complete before firing the
869 - // chat-send AJAX. On cached pages the inline mxchatChat.nonce is stale
870 - // until refreshNonceIfNeeded() returns; constructing ajaxData inside the
871 - // callback guarantees we read the fresh value. See plan-c5457f.
872 - refreshNonceIfNeeded(function() {
873 521 // Prepare AJAX data
874 522 const ajaxData = {
875 523 action: 'mxchat_handle_chat_request',
876 524 message: message,
877 - session_id: sessionId,
525 + session_id: getChatSession(botId),
878 526 nonce: mxchatChat.nonce,
879 527 current_page_url: window.location.href,
880 528 current_page_title: document.title,
881 529 bot_id: botId,
@@ -881,14 +529,14 @@
881 529 bot_id: botId,
882 530 // Pass session start timestamp so AI context matches what user sees
883 531 session_start_timestamp: instance.sessionStartTimestamp || 0
884 532 };
885 -
533 +
886 534 // Add page context if available
887 535 if (pageContext) {
888 536 ajaxData.page_context = JSON.stringify(pageContext);
889 537 }
890 -
538 +
891 539 // CHECK FOR VISION FLAGS AND ADD THEM
892 540 if (window.mxchatVisionProcessed) {
893 541 ajaxData.vision_processed = true;
894 542 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -897,9 +545,9 @@
897 545 window.mxchatVisionProcessed = false;
898 546 window.mxchatOriginalMessage = null;
899 547 window.mxchatVisionImagesCount = 0;
900 548 }
901 -
549 +
902 550 $.ajax({
903 551 url: mxchatChat.ajax_url,
904 552 type: 'POST',
905 553 dataType: 'json',
@@ -937,16 +585,23 @@
937 585 errorMessage = "An error occurred. Please try again or contact support.";
938 586 }
939 587
940 588 // Handle session reset action (IP changed, session expired, etc.)
941 - // Silent reset — keep chat UI intact, just get a new session and retry
942 589 if (response.data && response.data.action === 'reset_session') {
943 - MxChatInstances.silentResetSession(botId);
944 - // 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
945 595 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
946 596 if (originalMessage) {
947 597 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
948 - 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';
949 604 if (shouldUseStreaming(currentModel)) {
950 605 callMxChatStream(originalMessage, function(response) {
951 606 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
952 607 }, botId);
@@ -1003,11 +658,9 @@
1003 658 }
1004 659
1005 660 // Check for live agent response
1006 661 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
1007 - removeThinkingDots(botId);
1008 662 updateChatModeIndicator('agent', botId);
1009 - enableChatInput(botId);
1010 663 return;
1011 664 }
1012 665
1013 666 // Handle the message and show notification if chat is hidden
@@ -1040,13 +693,9 @@
1040 693 $badge.show();
1041 694 }
1042 695 }
1043 696 } else {
1044 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
1045 - if (response.vectorstore_error) {
1046 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
1047 - }
1048 - replaceLastMessage("bot", emptyMsg, '', [], botId);
697 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
1049 698 }
1050 699
1051 700 if (response.message_id) {
1052 701 var instance = MxChatInstances.get(botId);
@@ -1088,9 +737,8 @@
1088 737
1089 738 replaceLastMessage("bot", errorMessage, '', [], botId);
1090 739 }
1091 740 });
1092 - }); // refreshNonceIfNeeded
1093 741 }
1094 742
1095 743 function callMxChatStream(message, callback, botId) {
1096 744 botId = botId || getMxChatBotId();
@@ -1109,25 +757,12 @@
1109 757
1110 758 // Get instance for session start timestamp (used when persistence is OFF)
1111 759 var instance = MxChatInstances.get(botId);
1112 760
1113 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
1114 - // non-string value via String(), so passing `null` would POST the literal string "null"
1115 - // and land in the transcripts table as a ghost session. ensureSession() always returns
1116 - // a real string even when cookies/localStorage are blocked.
1117 - var streamSessionId = MxChatInstances.ensureSession(botId);
1118 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
1119 - streamSessionId = generateSessionId();
1120 - MxChatInstances.setChatSession(botId, streamSessionId);
1121 - }
1122 -
1123 - // Wait for the page-cache nonce refresh before constructing formData (which
1124 - // captures mxchatChat.nonce by value). Mirrors callMxChat's wrapping. See plan-c5457f.
1125 - refreshNonceIfNeeded(function() {
1126 761 const formData = new FormData();
1127 762 formData.append('action', 'mxchat_stream_chat');
1128 763 formData.append('message', message);
1129 - formData.append('session_id', streamSessionId);
764 + formData.append('session_id', getChatSession(botId));
1130 765 formData.append('nonce', mxchatChat.nonce);
1131 766 formData.append('current_page_url', window.location.href);
1132 767 formData.append('current_page_title', document.title);
1133 768 formData.append('bot_id', botId);
@@ -1153,20 +788,12 @@
1153 788 let accumulatedContent = '';
1154 789 let testingDataReceived = false;
1155 790 let streamingStarted = false;
1156 791
1157 - // Abortable stream: a fresh controller per turn, keyed by bot instance.
1158 - // The Stop control (send button swapped in place) aborts both the read
1159 - // loop and the underlying request.
1160 - var streamControl = { controller: new AbortController(), aborted: false };
1161 - mxchatActiveStreams[botId] = streamControl;
1162 - mxchatShowStopButton(botId);
1163 -
1164 792 fetch(mxchatChat.ajax_url, {
1165 793 method: 'POST',
1166 794 body: formData,
1167 - credentials: 'same-origin',
1168 - signal: streamControl.controller.signal
795 + credentials: 'same-origin'
1169 796 })
1170 797 .then(response => {
1171 798 // Store the response for potential fallback handling
1172 799 const responseClone = response.clone();
@@ -1235,16 +862,8 @@
1235 862
1236 863 // Re-enable chat input when stream ends with content
1237 864 enableChatInput(botId);
1238 865
1239 - // Scroll the user's last message to the top now that the
1240 - // bot's full reply has rendered (gives max reading room).
1241 - var $chatBoxDone = getElement(botId, 'chat-box');
1242 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
1243 - if ($lastUserMsgDone.length) {
1244 - scrollElementToTop($lastUserMsgDone, botId);
1245 - }
1246 -
1247 866 if (callback) {
1248 867 callback(accumulatedContent);
1249 868 }
1250 869 return;
@@ -1267,16 +886,8 @@
1267 886
1268 887 // Re-enable chat input after streaming completes
1269 888 enableChatInput(botId);
1270 889
1271 - // Scroll the user's last message to the top now
1272 - // that the bot's full reply has rendered.
1273 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1274 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1275 - if ($lastUserMsgStreamDone.length) {
1276 - scrollElementToTop($lastUserMsgStreamDone, botId);
1277 - }
1278 -
1279 890 if (callback) {
1280 891 callback(accumulatedContent);
1281 892 }
1282 893 return;
@@ -1333,9 +944,8 @@
1333 944 }
1334 945
1335 946 processStream();
1336 947 }).catch(streamError => {
1337 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1338 948 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1339 949 callMxChat(message, callback, botId);
1340 950 });
1341 951 }
@@ -1342,9 +952,8 @@
1342 952
1343 953 processStream();
1344 954 })
1345 955 .catch(error => {
1346 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1347 956 // Check if we have server error data with chat mode
1348 957 if (error && error.isServerError && error.data) {
1349 958 // Check for chat mode in error data
1350 959 if (error.data.chat_mode) {
@@ -1357,9 +966,8 @@
1357 966 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1358 967 callMxChat(message, callback, botId);
1359 968 }
1360 969 });
1361 - }); // refreshNonceIfNeeded
1362 970 }
1363 971
1364 972 // Helper function to handle non-streaming responses
1365 973 function handleNonStreamResponse(data, callback, botId) {
@@ -1398,16 +1006,21 @@
1398 1006 errorMessage = "An error occurred. Please try again or contact support.";
1399 1007 }
1400 1008
1401 1009 // Handle session reset action (IP changed, session expired, etc.)
1402 - // Silent reset — keep chat UI intact, just get a new session and retry
1403 1010 if (data.data && data.data.action === 'reset_session') {
1404 - MxChatInstances.silentResetSession(botId);
1405 - // 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
1406 1014 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1407 1015 if (originalMessage) {
1408 1016 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1409 - 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';
1410 1023 if (shouldUseStreaming(currentModel)) {
1411 1024 callMxChatStream(originalMessage, callback, botId);
1412 1025 } else {
1413 1026 callMxChat(originalMessage, callback, botId);
@@ -1429,22 +1042,8 @@
1429 1042 }
1430 1043 return; // Exit early for errors
1431 1044 }
1432 1045
1433 - // Check for live agent response
1434 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1435 - removeThinkingDots(botId);
1436 - // Also remove any leftover bot-message that lost its temporary-message class
1437 - var $chatBox = getElement(botId, 'chat-box');
1438 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1439 - updateChatModeIndicator('agent', botId);
1440 - enableChatInput(botId);
1441 - if (callback) {
1442 - callback('');
1443 - }
1444 - return;
1445 - }
1446 -
1447 1046 // Handle different response formats
1448 1047 if (data.text || data.html || data.message) {
1449 1048
1450 1049 // Apply response hooks
@@ -1489,15 +1088,19 @@
1489 1088 }
1490 1089
1491 1090 // Enhanced updateChatModeIndicator function for immediate DOM updates
1492 1091 function updateChatModeIndicator(mode, botId) {
1092 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1493 1093 botId = botId || 'default';
1494 1094 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1095 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1495 1096 if (indicator) {
1496 1097 const oldText = indicator.textContent;
1098 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1497 1099
1498 1100 if (mode === 'agent') {
1499 1101 indicator.textContent = 'Live Agent';
1102 + console.log('[MxChat] Mode is agent, calling startPolling...');
1500 1103 startPolling(botId);
1501 1104 } else {
1502 1105 // Everything else is AI mode
1503 1106 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1568,21 +1171,9 @@
1568 1171 // Update the event handlers to use the correct function names (using event delegation)
1569 1172 // Use class-based selectors for multi-instance support
1570 1173 $(document).on('click', '.send-button', function() {
1571 1174 var botId = getBotIdFromElement(this);
1572 - // While a response is streaming the button is a Stop control.
1573 - if (this.classList.contains('mxchat-stop-mode')) {
1574 - // Same click that just started this stream (an add-on's direct handler
1575 - // ran before this delegated one) — not a Stop press. See
1576 - // mxchatShowStopButton for the full story (plan-4bba64).
1577 - if (this.__mxchatStopJustShown) return;
1578 - mxchatStopStreaming(botId);
1579 - return;
1580 - }
1581 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1582 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1583 - disableChatInput(botId);
1584 - }
1175 + disableChatInput(botId);
1585 1176 sendMessage(botId);
1586 1177 });
1587 1178
1588 1179 // Override enter key handler (using event delegation)
@@ -1589,313 +1180,14 @@
1589 1180 $(document).on('keypress', '.chat-input', function(e) {
1590 1181 if (e.which == 13 && !e.shiftKey) {
1591 1182 e.preventDefault();
1592 1183 var botId = getBotIdFromElement(this);
1593 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1594 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1595 - disableChatInput(botId);
1596 - }
1184 + disableChatInput(botId);
1597 1185 sendMessage(botId);
1598 1186 }
1599 1187 });
1600 1188
1601 -// Builds the list of overflow-menu items for a given bot.
1602 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1603 -function mxchatGetHeaderMenuItems(botId) {
1604 - var items = [];
1605 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1606 -
1607 - // The `print_button_*` keys still gate this item for back-compat with
1608 - // existing user options. The action is now a transcript download, not print.
1609 - if (settings.print_button_enabled === 'on') {
1610 - items.push({
1611 - id: 'download-transcript',
1612 - label: settings.print_button_label || 'Download Transcript',
1613 - 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>',
1614 - action: function() {
1615 - mxchatDownloadTranscript(botId);
1616 - }
1617 - });
1618 - }
1619 -
1620 - // "Start new chat" — surfaces the EXISTING per-conversation reset
1621 - // (MxChatInstances.resetChatSession) so a visitor can start a fresh thread
1622 - // without the site owner disabling chat persistence globally. Default OFF;
1623 - // gated by the reset_chat_enabled option. plan ac2e81.
1624 - if (settings.reset_chat_enabled === 'on') {
1625 - items.push({
1626 - id: 'reset-chat',
1627 - label: settings.reset_chat_label || 'Start new chat',
1628 - 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>',
1629 - action: function() {
1630 - var confirmMsg = settings.reset_chat_confirm || 'Start a new chat? This clears the current conversation.';
1631 - if (window.confirm(confirmMsg)) {
1632 - MxChatInstances.resetChatSession(botId);
1633 - }
1634 - }
1635 - });
1636 - }
1637 -
1638 - return items;
1639 -}
1640 -
1641 -// Builds a clean markdown transcript of the current conversation and triggers
1642 -// a file download. Used by the "Download Transcript" menu item.
1643 -function mxchatDownloadTranscript(botId) {
1644 - var $chatBox = getElement(botId, 'chat-box');
1645 - if (!$chatBox || !$chatBox.length) return;
1646 -
1647 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1648 - var headerTitle = settings.print_header_title || 'Chat transcript';
1649 - var now = new Date();
1650 - var stamp = now.toLocaleString();
1651 -
1652 - var lines = [];
1653 - lines.push('# ' + headerTitle);
1654 - lines.push('');
1655 - lines.push('Exported: ' + stamp);
1656 - lines.push('');
1657 - lines.push('---');
1658 - lines.push('');
1659 -
1660 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1661 - var $msg = $(this);
1662 - // Skip thinking placeholders and any in-flight temporary messages.
1663 - if ($msg.find('.thinking-dots').length) return;
1664 - if ($msg.hasClass('temporary-message')) return;
1665 -
1666 - var sender;
1667 - if ($msg.hasClass('user-message')) sender = 'User';
1668 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1669 - else sender = 'AI Agent';
1670 -
1671 - // Strip interactive UI from the cloned message so we get the conversation text.
1672 - var $clone = $msg.clone();
1673 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1674 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1675 - if (!text) return;
1676 -
1677 - lines.push('**' + sender + '**');
1678 - lines.push('');
1679 - lines.push(text);
1680 - lines.push('');
1681 - });
1682 -
1683 - var content = lines.join('\n');
1684 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1685 - var fname = 'mxchat-transcript-' + iso + '.md';
1686 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1687 - var url = URL.createObjectURL(blob);
1688 - var a = document.createElement('a');
1689 - a.href = url;
1690 - a.download = fname;
1691 - a.style.display = 'none';
1692 - document.body.appendChild(a);
1693 - a.click();
1694 - setTimeout(function() {
1695 - if (a.parentNode) a.parentNode.removeChild(a);
1696 - URL.revokeObjectURL(url);
1697 - }, 100);
1698 -}
1699 -
1700 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1701 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1702 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1703 -function mxchatSyncMenuColors(botId, $wrap) {
1704 - if (!$wrap || !$wrap.length) return;
1705 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1706 - if (!$bot.length) return;
1707 - var cs = window.getComputedStyle($bot[0]);
1708 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1709 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1710 - }
1711 - // Bot text color usually lives on a child div, not .bot-message itself.
1712 - var $textChild = $bot.find('[style*="color"]').first();
1713 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1714 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1715 -}
1716 -
1717 -// Renders (or re-renders) the item list for one menu wrap. Split out of
1718 -// mxchatInitHeaderMenu so the dynamic-settings merge (plan-32db95) can
1719 -// rebuild items + trigger visibility WITHOUT re-binding the one-time
1720 -// open/close/keyboard wiring. closeMenu is passed in by the init closure;
1721 -// a rebuild before init (never happens, but harmless) just skips it.
1722 -function mxchatRenderHeaderMenuItems(botId, $wrap, closeMenuFn) {
1723 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1724 - var $menu = $wrap.find('.mxchat-header-menu');
1725 - var items = mxchatGetHeaderMenuItems(botId);
1726 -
1727 - $menu.empty();
1728 -
1729 - if (!items.length) {
1730 - $trigger.hide();
1731 - $menu.hide();
1732 - return;
1733 - }
1734 -
1735 - // Clear any inline display:none a previous zero-item render left behind —
1736 - // open/close visibility is governed by the hidden prop + is-open class.
1737 - $trigger.css('display', '');
1738 - $menu.css('display', '');
1739 -
1740 - items.forEach(function(item, idx) {
1741 - var $btn = $('<button>', {
1742 - type: 'button',
1743 - 'class': 'mxchat-menu-item',
1744 - 'role': 'menuitem',
1745 - 'tabindex': '-1',
1746 - 'data-menu-id': item.id,
1747 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1748 - '<span class="mxchat-menu-item-label"></span>'
1749 - });
1750 - $btn.find('.mxchat-menu-item-label').text(item.label);
1751 - $btn.on('click', function(e) {
1752 - e.preventDefault();
1753 - e.stopPropagation();
1754 - if (closeMenuFn) closeMenuFn();
1755 - try { item.action(); } catch (err) { /* no-op */ }
1756 - });
1757 - $menu.append($btn);
1758 - });
1759 -}
1760 -
1761 -// Re-render every menu on the page after a dynamic-settings merge
1762 -// (multi-bot: each wrap re-reads its items). An OPEN menu is left alone —
1763 -// swapping items under the user mid-interaction yanks focus — and the
1764 -// rebuild runs when it closes instead (closeMenu checks the pending flag).
1765 -function mxchatRebuildHeaderMenus() {
1766 - $('.mxchat-header-menu-wrap').each(function() {
1767 - var $wrap = $(this);
1768 - var botId = $wrap.data('bot-id');
1769 - if (!botId) return;
1770 - if (!$wrap.data('mxchatMenuReady')) {
1771 - mxchatInitHeaderMenu(botId);
1772 - return;
1773 - }
1774 - if ($wrap.find('.mxchat-header-menu').hasClass('is-open')) {
1775 - $wrap.data('mxchatMenuRebuildPending', true);
1776 - return;
1777 - }
1778 - mxchatRenderHeaderMenuItems(botId, $wrap, $wrap.data('mxchatMenuClose'));
1779 - });
1780 -}
1781 -
1782 -// One-time per-widget init: renders menu items, wires open/close,
1783 -// outside-click, Escape, and arrow-key navigation. If no items, hides the
1784 -// trigger. Wiring happens even when there are zero items at init, so a
1785 -// later dynamic-settings rebuild that adds items has a working trigger.
1786 -function mxchatInitHeaderMenu(botId) {
1787 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1788 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1789 -
1790 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1791 - var $menu = $wrap.find('.mxchat-header-menu');
1792 -
1793 - // Initial color sync — covers normal page load.
1794 - mxchatSyncMenuColors(botId, $wrap);
1795 -
1796 - function openMenu() {
1797 - // Re-sync each open in case the active theme changed since init.
1798 - mxchatSyncMenuColors(botId, $wrap);
1799 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1800 - $trigger.attr('aria-expanded', 'true');
1801 - // Focus the first item for keyboard users
1802 - setTimeout(function() {
1803 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1804 - }, 0);
1805 - }
1806 - function closeMenu(returnFocus) {
1807 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1808 - $trigger.attr('aria-expanded', 'false');
1809 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1810 - if (returnFocus) $trigger.trigger('focus');
1811 - // A dynamic-settings rebuild that arrived while the menu was open
1812 - // was deferred (mxchatRebuildHeaderMenus) — run it now.
1813 - if ($wrap.data('mxchatMenuRebuildPending')) {
1814 - $wrap.removeData('mxchatMenuRebuildPending');
1815 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1816 - }
1817 - }
1818 -
1819 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1820 - // click-to-collapse handler does not fire.
1821 - $trigger.on('click', function(e) {
1822 - e.preventDefault();
1823 - e.stopPropagation();
1824 - if ($menu.hasClass('is-open')) closeMenu();
1825 - else openMenu();
1826 - });
1827 -
1828 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1829 - $menu.on('click', function(e) {
1830 - e.stopPropagation();
1831 - });
1832 -
1833 - // Outside click closes the menu.
1834 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1835 - if (!$menu.hasClass('is-open')) return;
1836 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1837 - closeMenu();
1838 - });
1839 -
1840 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1841 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1842 - var $items = $menu.find('.mxchat-menu-item');
1843 - var idx = $items.index(this);
1844 - if (e.key === 'Escape') {
1845 - e.preventDefault();
1846 - closeMenu(true);
1847 - } else if (e.key === 'ArrowDown') {
1848 - e.preventDefault();
1849 - var $next = $items.eq((idx + 1) % $items.length);
1850 - $items.attr('tabindex', '-1');
1851 - $next.attr('tabindex', '0').trigger('focus');
1852 - } else if (e.key === 'ArrowUp') {
1853 - e.preventDefault();
1854 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1855 - $items.attr('tabindex', '-1');
1856 - $prev.attr('tabindex', '0').trigger('focus');
1857 - } else if (e.key === 'Enter' || e.key === ' ') {
1858 - e.preventDefault();
1859 - $(this).trigger('click');
1860 - }
1861 - });
1862 - $trigger.on('keydown', function(e) {
1863 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1864 - e.preventDefault();
1865 - closeMenu(true);
1866 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1867 - e.preventDefault();
1868 - openMenu();
1869 - }
1870 - });
1871 -
1872 - // Expose closeMenu for out-of-closure re-renders (mxchatRebuildHeaderMenus),
1873 - // then do the initial item render.
1874 - $wrap.data('mxchatMenuClose', closeMenu);
1875 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1876 -
1877 - $wrap.data('mxchatMenuReady', true);
1878 -}
1879 -
1880 -// Initialize header menus for every rendered widget on DOM ready.
1881 -$(function() {
1882 - $('.mxchat-header-menu-wrap').each(function() {
1883 - var botId = $(this).data('bot-id');
1884 - if (botId) mxchatInitHeaderMenu(botId);
1885 - });
1886 -
1887 - // Embedded (non-floating) widgets are open from the moment the page
1888 - // renders — refresh dynamic settings at init (plan-32db95). Floating
1889 - // widgets refresh on first launcher open instead.
1890 - var hasEmbeddedWidget = $('.mxchat-chatbot-wrapper').filter(function() {
1891 - return !$(this).closest('.floating-chatbot').length;
1892 - }).length > 0;
1893 - if (hasEmbeddedWidget) {
1894 - mxchatRefreshDynamicSettings();
1895 - }
1896 -});
1897 -
1189 +
1898 1190 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1899 1191 try {
1900 1192 // Determine styles based on sender type
1901 1193 let messageClass, bgColor, fontColor;
@@ -1933,12 +1225,17 @@
1933 1225 'margin-bottom': '1em'
1934 1226 });
1935 1227 }
1936 1228
1937 - // Process the message content - always run linkify to convert markdown
1938 - // links and format text. linkify() handles existing HTML safely via
1939 - // negative lookaheads that skip URLs already inside <a> tags.
1940 - 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 + }
1941 1238
1942 1239 // Add images if provided
1943 1240 if (images && images.length > 0) {
1944 1241 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1987,12 +1284,8 @@
1987 1284 if (lastUserMessage.length) {
1988 1285 scrollElementToTop(lastUserMessage, botId);
1989 1286 }
1990 1287 }
1991 -
1992 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1993 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1994 - }
1995 1288 });
1996 1289
1997 1290 if (messageText.id) {
1998 1291 var instance = MxChatInstances.get(botId);
@@ -2077,12 +1370,26 @@
2077 1370 bgColor = botMessageBgColor;
2078 1371 fontColor = botMessageFontColor;
2079 1372 }
2080 1373
2081 - // Always run linkify to convert markdown links and format text.
2082 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
2083 - // to avoid double-processing URLs that are already inside <a> tags).
2084 - 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 + }
2085 1392
2086 1393 if (responseHtml) {
2087 1394 // Only add line breaks if there's actual text content before the HTML
2088 1395 if (fullMessage && fullMessage.trim()) {
@@ -2139,12 +1446,8 @@
2139 1446 }
2140 1447
2141 1448 // Re-enable chat input after response is displayed
2142 1449 enableChatInput(botId);
2143 -
2144 - if (sender === "bot" || sender === "agent") {
2145 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2146 - }
2147 1450 } else {
2148 1451 appendMessage(sender, responseText, responseHtml, images, false, botId);
2149 1452 // Re-enable chat input after response is displayed
2150 1453 enableChatInput(botId);
@@ -2153,15 +1456,8 @@
2153 1456
2154 1457
2155 1458 function appendThinkingMessage(botId) {
2156 1459 botId = botId || 'default';
2157 -
2158 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
2159 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
2160 - if (indicator && indicator.textContent === 'Live Agent') {
2161 - return;
2162 - }
2163 -
2164 1460 var $chatBox = getElement(botId, 'chat-box');
2165 1461
2166 1462 // Remove any existing thinking dots in this bot's chat first
2167 1463 $chatBox.find('.thinking-dots').remove();
@@ -2183,9 +1479,9 @@
2183 1479 '</div>' +
2184 1480 '</div>';
2185 1481
2186 1482 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
2187 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1483 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
2188 1484 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
2189 1485 scrollToBottom(botId);
2190 1486 }
2191 1487
@@ -2191,11 +1487,9 @@
2191 1487
2192 1488 function removeThinkingDots(botId) {
2193 1489 botId = botId || 'default';
2194 1490 var $chatBox = getElement(botId, 'chat-box');
2195 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
2196 1491 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
2197 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
2198 1492 }
2199 1493
2200 1494 // ====================================
2201 1495 // TEXT FORMATTING & PROCESSING
@@ -2229,12 +1523,9 @@
2229 1523 processedText = formatTextStyling(processedText);
2230 1524
2231 1525 // Process code blocks BEFORE processing links
2232 1526 processedText = formatCodeBlocks(processedText);
2233 -
2234 - // Process markdown tables BEFORE converting newlines to paragraphs
2235 - processedText = formatMarkdownTables(processedText);
2236 -
1527 +
2237 1528 // NOW convert to paragraphs
2238 1529 processedText = convertNewlinesToBreaks(processedText);
2239 1530
2240 1531 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -2247,63 +1538,37 @@
2247 1538 // Return as a proper link without the brackets
2248 1539 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2249 1540 });
2250 1541
2251 - // Process markdown links: [text](url) and [](url)
2252 - // Uses balanced parenthesis matching to handle URLs containing parens
2253 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
2254 - processedText = (function(input) {
2255 - var result = '';
2256 - var i = 0;
2257 - while (i < input.length) {
2258 - // Look for [ at current position
2259 - if (input[i] === '[') {
2260 - // Find closing ]
2261 - var closeBracket = input.indexOf(']', i + 1);
2262 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
2263 - result += input[i];
2264 - i++;
2265 - continue;
2266 - }
2267 - var linkText = input.substring(i + 1, closeBracket);
2268 - // Check if URL starts with http
2269 - var urlStart = closeBracket + 2;
2270 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
2271 - result += input[i];
2272 - i++;
2273 - continue;
2274 - }
2275 - // Find balanced closing paren
2276 - var depth = 1;
2277 - var j = urlStart;
2278 - while (j < input.length && depth > 0) {
2279 - if (input[j] === '(') depth++;
2280 - else if (input[j] === ')') depth--;
2281 - if (depth > 0) j++;
2282 - }
2283 - if (depth !== 0) {
2284 - result += input[i];
2285 - i++;
2286 - continue;
2287 - }
2288 - var url = input.substring(urlStart, j);
2289 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
2290 - var encodedUrl = safeEncodeUrl(cleanUrl);
2291 - if (!linkText || !linkText.trim()) {
2292 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
2293 - } else {
2294 - var safeText = sanitizeUserInput(linkText);
2295 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
2296 - }
2297 - i = j + 1; // Skip past the closing )
2298 - } else {
2299 - result += input[i];
2300 - i++;
2301 - }
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>`;
2302 1552 }
2303 - return result;
2304 - })(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 + });
2305 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 +
2306 1571 // Process phone numbers: [text](tel:number)
2307 1572 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
2308 1573 processedText = processedText.replace(phonePattern, (match, text, phone) => {
2309 1574 const safePhone = safeEncodeUrl(phone);
@@ -2455,78 +1720,9 @@
2455 1720 });
2456 1721
2457 1722 return text;
2458 1723 }
2459 -
2460 - function formatMarkdownTables(text) {
2461 - var lines = text.split('\n');
2462 - var result = [];
2463 - var i = 0;
2464 -
2465 - while (i < lines.length) {
2466 - // Check for a table: current line has pipes AND next line is a separator row
2467 - if (i + 1 < lines.length &&
2468 - lines[i].indexOf('|') !== -1 &&
2469 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2470 -
2471 - var tableLines = [];
2472 - var headerLine = lines[i];
2473 - var separatorLine = lines[i + 1];
2474 - tableLines.push(headerLine);
2475 - tableLines.push(separatorLine);
2476 -
2477 - // Collect remaining table rows
2478 - var j = i + 2;
2479 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2480 - tableLines.push(lines[j]);
2481 - j++;
2482 - }
2483 -
2484 - // Parse alignment from separator row
2485 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2486 - var alignments = sepCells.map(function(cell) {
2487 - var trimmed = cell.trim();
2488 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2489 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2490 - return 'left';
2491 - });
2492 -
2493 - // Build HTML table
2494 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2495 -
2496 - // Header row
2497 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2498 - html += '<thead><tr>';
2499 - headerCells.forEach(function(cell, idx) {
2500 - var align = alignments[idx] || 'left';
2501 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2502 - });
2503 - html += '</tr></thead>';
2504 -
2505 - // Body rows
2506 - html += '<tbody>';
2507 - for (var r = 2; r < tableLines.length; r++) {
2508 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2509 - html += '<tr>';
2510 - rowCells.forEach(function(cell, idx) {
2511 - var align = alignments[idx] || 'left';
2512 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2513 - });
2514 - html += '</tr>';
2515 - }
2516 - html += '</tbody></table></div>';
2517 -
2518 - result.push(html);
2519 - i = j;
2520 - } else {
2521 - result.push(lines[i]);
2522 - i++;
2523 - }
2524 - }
2525 -
2526 - return result.join('\n');
2527 - }
2528 -
1724 +
2529 1725 function sanitizeUserInput(text) {
2530 1726 const div = document.createElement('div');
2531 1727 div.textContent = text;
2532 1728 return div.innerHTML;
@@ -2597,14 +1793,13 @@
2597 1793 requestAnimationFrame(smoothScroll);
2598 1794 }
2599 1795 }
2600 1796
2601 - function scrollElementToTop(element, botId, topOffset) {
1797 + function scrollElementToTop(element, botId) {
2602 1798 botId = botId || 'default';
2603 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2604 1799 var chatBox = getElement(botId, 'chat-box');
2605 1800 var elementTop = element.position().top + chatBox.scrollTop();
2606 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1801 + chatBox.animate({ scrollTop: elementTop }, 500);
2607 1802 }
2608 1803
2609 1804 function showChatWidget(botId) {
2610 1805 botId = botId || 'default';
@@ -2748,12 +1943,15 @@
2748 1943 // LIVE AGENT FUNCTIONALITY
2749 1944 // ====================================
2750 1945
2751 1946 function startPolling(botId) {
1947 + console.log('[MxChat] startPolling called for botId:', botId);
2752 1948 botId = botId || 'default';
2753 1949 var instance = MxChatInstances.get(botId);
2754 1950 // Clear any existing interval first
2755 1951 stopPolling(botId);
1952 + // Start new polling interval
1953 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2756 1954 instance.pollingInterval = setInterval(function() {
2757 1955 checkForAgentMessages(botId);
2758 1956 }, 5000);
2759 1957 }
@@ -2758,17 +1956,20 @@
2758 1956 }, 5000);
2759 1957 }
2760 1958
2761 1959 function stopPolling(botId) {
1960 + console.log('[MxChat] stopPolling called for botId:', botId);
2762 1961 botId = botId || 'default';
2763 1962 var instance = MxChatInstances.get(botId);
2764 1963 if (instance.pollingInterval) {
2765 1964 clearInterval(instance.pollingInterval);
2766 1965 instance.pollingInterval = null;
1966 + console.log('[MxChat] Polling stopped for botId:', botId);
2767 1967 }
2768 1968 }
2769 1969
2770 1970 function checkForAgentMessages(botId) {
1971 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2771 1972 botId = botId || 'default';
2772 1973 var instance = MxChatInstances.get(botId);
2773 1974 const sessionId = getChatSession(botId);
2774 1975 $.ajax({
@@ -2794,12 +1995,8 @@
2794 1995 instance.processedMessageIds.add(message.id);
2795 1996 }
2796 1997 });
2797 1998
2798 - if (hasNewMessage) {
2799 - enableChatInput(botId);
2800 - }
2801 -
2802 1999 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2803 2000 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2804 2001 showNotification(botId);
2805 2002 }
@@ -2805,13 +2002,8 @@
2805 2002 }
2806 2003
2807 2004 scrollToBottom(botId, true);
2808 2005 }
2809 -
2810 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2811 - if (response.success && response.data?.chat_mode) {
2812 - updateChatModeIndicator(response.data.chat_mode, botId);
2813 - }
2814 2006 },
2815 2007 error: function (xhr, status, error) {
2816 2008 // Polling error - silently continue
2817 2009 }
@@ -2821,29 +2013,20 @@
2821 2013 // ====================================
2822 2014 // CHAT HISTORY & PERSISTENCE
2823 2015 // ====================================
2824 2016
2825 -function loadChatHistory(botId, onComplete) {
2017 +function loadChatHistory(botId) {
2826 2018 botId = botId || 'default';
2827 2019 var instance = MxChatInstances.get(botId);
2828 2020
2829 2021 // Prevent duplicate loading
2830 2022 if (instance.chatHistoryLoaded) {
2831 - if (onComplete) onComplete();
2832 2023 return;
2833 2024 }
2834 2025
2835 - // Use getChatSession which returns null if no session exists (does NOT create one)
2836 2026 var sessionId = getChatSession(botId);
2837 2027 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2838 2028
2839 - // No session yet — nothing to load. History will load after first message via ensureSession.
2840 - if (!sessionId) {
2841 - instance.chatHistoryLoaded = true;
2842 - if (onComplete) onComplete();
2843 - return;
2844 - }
2845 -
2846 2029 if (chatPersistenceEnabled && sessionId) {
2847 2030 $.ajax({
2848 2031 url: mxchatChat.ajax_url,
2849 2032 type: 'POST',
@@ -2854,12 +2037,11 @@
2854 2037 },
2855 2038 success: function(response) {
2856 2039 // Handle session reset (IP changed while user was away)
2857 2040 if (response.success === false && response.data && response.data.action === 'reset_session') {
2858 - // Silent reset — new session but don't clear UI
2859 - MxChatInstances.silentResetSession(botId);
2041 + // Silently reset session - user will start fresh
2042 + resetChatSession(botId);
2860 2043 instance.chatHistoryLoaded = true; // Prevent retry loop
2861 - if (onComplete) onComplete();
2862 2044 return;
2863 2045 }
2864 2046
2865 2047 // Check if the response indicates success
@@ -2915,19 +2097,9 @@
2915 2097 var content = message.content;
2916 2098 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2917 2099 content = decodeHTMLEntities(content);
2918 2100
2919 - // Skip linkify for messages containing structured HTML
2920 - // (forms, product cards, galleries, etc.) to avoid
2921 - // markdown formatting corrupting HTML attributes
2922 - // (e.g. underscores in name="field_name" becoming <em> tags)
2923 - if (content.includes("mxchat-product-card") ||
2924 - content.includes("mxchat-image-gallery") ||
2925 - content.includes("mxchat-featured-products") ||
2926 - content.includes("<form") ||
2927 - content.includes("<input") ||
2928 - content.includes("<select") ||
2929 - content.includes("<textarea")) {
2101 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2930 2102 messageElement.html(content);
2931 2103 } else {
2932 2104 var formattedContent = linkify(content);
2933 2105 messageElement.html(formattedContent);
@@ -2967,17 +2139,13 @@
2967 2139 instance.chatHistoryLoaded = true;
2968 2140 }
2969 2141 }
2970 2142 }
2971 - if (onComplete) onComplete();
2972 2143 },
2973 2144 error: function(xhr, status, error) {
2974 2145 // Error loading chat history - silently continue
2975 - if (onComplete) onComplete();
2976 2146 }
2977 2147 });
2978 - } else {
2979 - if (onComplete) onComplete();
2980 2148 }
2981 2149 }
2982 2150
2983 2151
@@ -3041,10 +2209,10 @@
3041 2209 .then(data => {
3042 2210 if (data.success) {
3043 2211 container.style.display = 'none';
3044 2212 nameElement.textContent = '';
3045 - instance.activePdfFile = null;
3046 - appendMessage('bot', 'PDF removed.', '', [], false, botId);
2213 + activePdfFile = null;
2214 + appendMessage('bot', 'PDF removed.');
3047 2215 }
3048 2216 })
3049 2217 .catch(error => {
3050 2218 // Error removing PDF - silently continue
@@ -3050,16 +2218,14 @@
3050 2218 // Error removing PDF - silently continue
3051 2219 });
3052 2220 }
3053 2221
3054 - function removeActiveWord(botId) {
3055 - botId = botId || 'default';
3056 - var instance = MxChatInstances.get(botId);
3057 - const container = getElementDOM(botId, 'active-word-container');
3058 - const nameElement = getElementDOM(botId, 'active-word-name');
3059 -
3060 - if (!container || !nameElement || !instance.activeWordFile) return;
3061 -
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 +
3062 2228 fetch(mxchatChat.ajax_url, {
3063 2229 method: 'POST',
3064 2230 headers: {
3065 2231 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3065,9 +2231,9 @@
3065 2231 'Content-Type': 'application/x-www-form-urlencoded',
3066 2232 },
3067 2233 body: new URLSearchParams({
3068 2234 'action': 'mxchat_remove_word',
3069 - 'session_id': getChatSession(botId),
2235 + 'session_id': sessionId,
3070 2236 'nonce': mxchatChat.nonce
3071 2237 })
3072 2238 })
3073 2239 .then(response => response.json())
@@ -3074,10 +2240,10 @@
3074 2240 .then(data => {
3075 2241 if (data.success) {
3076 2242 container.style.display = 'none';
3077 2243 nameElement.textContent = '';
3078 - instance.activeWordFile = null;
3079 - appendMessage('bot', 'Word document removed.', '', [], false, botId);
2244 + activeWordFile = null;
2245 + appendMessage('bot', 'Word document removed.');
3080 2246 }
3081 2247 })
3082 2248 .catch(error => {
3083 2249 // Error removing Word document - silently continue
@@ -3155,35 +2321,45 @@
3155 2321 // ====================================
3156 2322
3157 2323 function checkPreChatDismissal(botId) {
3158 2324 botId = botId || 'default';
3159 - try {
3160 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
3161 - if (dismissedAt) {
3162 - // Re-show after 24 hours
3163 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
3164 - 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 {
3165 2336 getElement(botId, 'pre-chat-message').hide();
3166 - return;
3167 2337 }
3168 - // Expired — clear and show again
3169 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2338 + },
2339 + error: function() {
2340 + // Error checking pre-chat dismissal - silently continue
3170 2341 }
3171 - getElement(botId, 'pre-chat-message').fadeIn(250);
3172 - } catch (e) {
3173 - // localStorage unavailable — show the message
3174 - getElement(botId, 'pre-chat-message').fadeIn(250);
3175 - }
2342 + });
3176 2343 }
3177 2344
3178 2345 function handlePreChatDismissal(botId) {
3179 2346 botId = botId || 'default';
3180 2347 getElement(botId, 'pre-chat-message').fadeOut(200);
3181 - try {
3182 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
3183 - } catch (e) {
3184 - // localStorage unavailable — dismissal won't persist
3185 - }
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 + });
3186 2362 }
3187 2363
3188 2364
3189 2365 // ====================================
@@ -3238,14 +2414,9 @@
3238 2414 collapseQuickQuestions(botId);
3239 2415 });
3240 2416
3241 2417 // Chatbot visibility toggle handlers - use class selector for multi-instance support
3242 - // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
3243 - $(document).on('click keydown', '.floating-chatbot-button', function(e) {
3244 - if (e.type === 'keydown') {
3245 - if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
3246 - e.preventDefault();
3247 - }
2418 + $(document).on('click', '.floating-chatbot-button', function() {
3248 2419 var botId = getBotIdFromElement(this);
3249 2420 var $chatbot = getElement(botId, 'floating-chatbot');
3250 2421 var $badge = getElement(botId, 'chat-notification-badge');
3251 2422 var $preChat = getElement(botId, 'pre-chat-message');
@@ -3250,88 +2421,35 @@
3250 2421 var $badge = getElement(botId, 'chat-notification-badge');
3251 2422 var $preChat = getElement(botId, 'pre-chat-message');
3252 2423
3253 2424 if ($chatbot.hasClass('hidden')) {
3254 - $chatbot.removeClass('hidden').addClass('visible')
3255 - .attr('aria-modal', 'true').attr('role', 'dialog');
3256 - $(this).addClass('hidden').attr('aria-expanded', 'true');
2425 + $chatbot.removeClass('hidden').addClass('visible');
2426 + $(this).addClass('hidden');
3257 2427 $badge.hide(); // Hide notification when opening chat
3258 2428 disableScroll();
3259 2429 $preChat.fadeOut(250);
3260 -
3261 - // First open per page load: re-fetch behavior settings in case
3262 - // this page's inline values came from a stale full-page cache
3263 - // (plan-32db95). Idempotent — later opens are a no-op.
3264 - mxchatRefreshDynamicSettings();
3265 -
3266 - // Load chat history for returning visitors (persistence)
3267 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3268 - if (chatPersistenceEnabled) {
3269 - MxChatInstances.ensureSession(botId);
3270 - }
3271 -
3272 - // Deferred email check — only on first widget open
3273 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3274 - var instance = MxChatInstances.get(botId);
3275 - if (emailBlocker && !instance.emailCheckDone) {
3276 - instance.emailCheckDone = true;
3277 - resolveEmailState(botId);
3278 - } else if (!emailBlocker) {
3279 - // No email collection — still route through showChatContainerForBot
3280 - // so the loader is shown while chat history loads
3281 - showChatContainerForBot(botId);
3282 - }
3283 -
3284 - // Move keyboard focus into the message input after the open transition.
3285 - setTimeout(function() {
3286 - var chatInput = getElementDOM(botId, 'chat-input');
3287 - if (chatInput && !chatInput.disabled) {
3288 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
3289 - }
3290 - }, 300);
3291 2430 } else {
3292 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3293 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2431 + $chatbot.removeClass('visible').addClass('hidden');
2432 + $(this).removeClass('hidden');
3294 2433 enableScroll();
3295 2434 checkPreChatDismissal(botId);
3296 2435 }
3297 2436 });
3298 2437
3299 - // Allow clicking anywhere on the title bar to close the chatbot.
3300 - // Returns keyboard focus to the launcher so keyboard users don't get
3301 - // stranded at <body> (WCAG SC 2.4.3 Focus Order). :focus-visible is
3302 - // heuristic-based so mouse-triggered close won't show a focus ring.
2438 + // Allow clicking anywhere on the title bar to close the chatbot
3303 2439 $(document).on('click', '.chatbot-top-bar', function() {
3304 2440 var botId = getBotIdFromElement(this);
3305 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3306 - var $launcher = getElement(botId, 'floating-chatbot-button');
3307 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
2441 + getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2442 + getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3308 2443 enableScroll();
3309 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3310 2444 });
3311 2445
3312 - // Global Escape-key handler — closes any visible chat widget and
3313 - // returns focus to its launcher. Standard modal-dismissal pattern;
3314 - // pairs with aria-modal="true" set on the widget when it opens.
3315 - $(document).on('keydown', function(e) {
3316 - if (e.key !== 'Escape' && e.key !== 'Esc') return;
3317 - var $visible = $('.floating-chatbot.visible');
3318 - if (!$visible.length) return;
3319 - e.preventDefault();
3320 - $visible.each(function() {
3321 - var botId = getBotIdFromElement(this);
3322 - $(this).addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3323 - var $launcher = getElement(botId, 'floating-chatbot-button');
3324 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
3325 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3326 - });
3327 - enableScroll();
3328 - });
3329 -
3330 2446 $(document).on('click', '.close-pre-chat-message', function(e) {
3331 2447 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3332 2448 var botId = getBotIdFromElement(this);
3333 - handlePreChatDismissal(botId);
2449 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2450 + $(this).remove();
2451 + });
3334 2452 });
3335 2453
3336 2454
3337 2455 // PDF upload button handlers - use class selector
@@ -3347,20 +2465,17 @@
3347 2465 var wordInput = getElementDOM(botId, 'word-upload');
3348 2466 if (wordInput) wordInput.click();
3349 2467 });
3350 2468
3351 - // PDF file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'pdf-upload')
3352 - $(document).on('change', '.pdf-upload', async function(e) {
3353 - var botId = getBotIdFromElement(this);
3354 - var instance = MxChatInstances.get(botId);
3355 - const file = this.files[0];
3356 - const sessionId = MxChatInstances.ensureSession(botId);
3357 -
2469 + // PDF file input change handler
2470 + addSafeEventListener('pdf-upload', 'change', async function(e) {
2471 + const file = e.target.files[0];
2472 +
3358 2473 if (!file || file.type !== 'application/pdf') {
3359 2474 alert('Please select a valid PDF file.');
3360 2475 return;
3361 2476 }
3362 -
2477 +
3363 2478 if (!sessionId) {
3364 2479 alert('Error: No session ID found');
3365 2480 return;
3366 2481 }
@@ -3368,49 +2483,47 @@
3368 2483 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3369 2484 alert('Error: Ajax configuration missing');
3370 2485 return;
3371 2486 }
3372 -
2487 +
3373 2488 // Disable buttons and show loading state
3374 - const uploadBtn = getElementDOM(botId, 'pdf-upload-btn');
3375 - const sendBtn = getElementDOM(botId, 'send-button');
3376 - if (!uploadBtn) return;
2489 + const uploadBtn = document.getElementById('pdf-upload-btn');
2490 + const sendBtn = document.getElementById('send-button');
3377 2491 const originalBtnContent = uploadBtn.innerHTML;
3378 -
2492 +
3379 2493 try {
3380 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3381 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3382 2494 const formData = new FormData();
3383 2495 formData.append('action', 'mxchat_upload_pdf');
3384 2496 formData.append('pdf_file', file);
3385 2497 formData.append('session_id', sessionId);
3386 2498 formData.append('nonce', mxchatChat.nonce);
3387 -
2499 +
3388 2500 uploadBtn.disabled = true;
3389 - if (sendBtn) sendBtn.disabled = true;
2501 + sendBtn.disabled = true;
3390 2502 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3391 2503 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3392 2504 </svg>`;
3393 -
2505 +
3394 2506 const response = await fetch(mxchatChat.ajax_url, {
3395 2507 method: 'POST',
3396 2508 body: formData
3397 2509 });
3398 -
2510 +
3399 2511 const data = await response.json();
3400 -
2512 +
3401 2513 if (data.success) {
3402 2514 // Hide popular questions if they exist
3403 - if (hasQuickQuestions(botId)) {
3404 - collapseQuickQuestions(botId);
2515 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2516 + if (hasQuickQuestions()) {
2517 + collapseQuickQuestions();
3405 2518 }
3406 -
2519 +
3407 2520 // Show the active PDF name
3408 - showActivePdf(data.data.filename, botId);
3409 -
3410 - appendMessage('bot', data.data.message, '', [], false, botId);
3411 - scrollToBottom(botId);
3412 - instance.activePdfFile = data.data.filename;
2521 + showActivePdf(data.data.filename);
2522 +
2523 + appendMessage('bot', data.data.message);
2524 + scrollToBottom();
2525 + activePdfFile = data.data.filename;
3413 2526 } else {
3414 2527 alert('Failed to upload PDF. Please try again.');
3415 2528 }
3416 2529 } catch (error) {
@@ -3416,76 +2529,66 @@
3416 2529 } catch (error) {
3417 2530 alert('Error uploading file. Please try again.');
3418 2531 } finally {
3419 2532 uploadBtn.disabled = false;
3420 - if (sendBtn) sendBtn.disabled = false;
2533 + sendBtn.disabled = false;
3421 2534 uploadBtn.innerHTML = originalBtnContent;
3422 2535 this.value = ''; // Reset file input
3423 2536 }
3424 2537 });
3425 2538
3426 - // Word file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'word-upload')
3427 - $(document).on('change', '.word-upload', async function(e) {
3428 - var botId = getBotIdFromElement(this);
3429 - var instance = MxChatInstances.get(botId);
3430 - const file = this.files[0];
3431 - const sessionId = MxChatInstances.ensureSession(botId);
3432 -
2539 + // Word file input change handler
2540 + addSafeEventListener('word-upload', 'change', async function(e) {
2541 + const file = e.target.files[0];
2542 +
3433 2543 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
3434 2544 alert('Please select a valid Word document (.docx).');
3435 2545 return;
3436 2546 }
3437 -
2547 +
3438 2548 if (!sessionId) {
3439 2549 alert('Error: No session ID found');
3440 2550 return;
3441 2551 }
3442 2552
3443 - if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3444 - alert('Error: Ajax configuration missing');
3445 - return;
3446 - }
3447 -
3448 2553 // Disable buttons and show loading state
3449 - const uploadBtn = getElementDOM(botId, 'word-upload-btn');
3450 - const sendBtn = getElementDOM(botId, 'send-button');
3451 - if (!uploadBtn) return;
2554 + const uploadBtn = document.getElementById('word-upload-btn');
2555 + const sendBtn = document.getElementById('send-button');
3452 2556 const originalBtnContent = uploadBtn.innerHTML;
3453 -
2557 +
3454 2558 try {
3455 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3456 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3457 2559 const formData = new FormData();
3458 2560 formData.append('action', 'mxchat_upload_word');
3459 2561 formData.append('word_file', file);
3460 2562 formData.append('session_id', sessionId);
3461 2563 formData.append('nonce', mxchatChat.nonce);
3462 -
2564 +
3463 2565 uploadBtn.disabled = true;
3464 - if (sendBtn) sendBtn.disabled = true;
2566 + sendBtn.disabled = true;
3465 2567 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3466 2568 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3467 2569 </svg>`;
3468 -
2570 +
3469 2571 const response = await fetch(mxchatChat.ajax_url, {
3470 2572 method: 'POST',
3471 2573 body: formData
3472 2574 });
3473 -
2575 +
3474 2576 const data = await response.json();
3475 -
2577 +
3476 2578 if (data.success) {
3477 2579 // Hide popular questions if they exist
3478 - if (hasQuickQuestions(botId)) {
3479 - collapseQuickQuestions(botId);
2580 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2581 + if (hasQuickQuestions()) {
2582 + collapseQuickQuestions();
3480 2583 }
3481 -
2584 +
3482 2585 // Show the active Word document name
3483 - showActiveWord(data.data.filename, botId);
3484 -
3485 - appendMessage('bot', data.data.message, '', [], false, botId);
3486 - scrollToBottom(botId);
3487 - instance.activeWordFile = data.data.filename;
2586 + showActiveWord(data.data.filename);
2587 +
2588 + appendMessage('bot', data.data.message);
2589 + scrollToBottom();
2590 + activeWordFile = data.data.filename;
3488 2591 } else {
3489 2592 alert('Failed to upload Word document. Please try again.');
3490 2593 }
3491 2594 } catch (error) {
@@ -3491,25 +2594,25 @@
3491 2594 } catch (error) {
3492 2595 alert('Error uploading file. Please try again.');
3493 2596 } finally {
3494 2597 uploadBtn.disabled = false;
3495 - if (sendBtn) sendBtn.disabled = false;
2598 + sendBtn.disabled = false;
3496 2599 uploadBtn.innerHTML = originalBtnContent;
3497 2600 this.value = ''; // Reset file input
3498 2601 }
3499 2602 });
3500 2603
3501 - // Remove button click handlers - delegated, bot-aware (were bound to stale un-suffixed ids)
3502 - $(document).on('click', '.remove-pdf-btn', function(e) {
2604 + // Remove button click handlers
2605 + document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
3503 2606 e.preventDefault();
3504 2607 e.stopPropagation();
3505 - removeActivePdf(getBotIdFromElement(this));
2608 + removeActivePdf();
3506 2609 });
3507 -
3508 - $(document).on('click', '.remove-word-btn', function(e) {
2610 +
2611 + document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
3509 2612 e.preventDefault();
3510 2613 e.stopPropagation();
3511 - removeActiveWord(getBotIdFromElement(this));
2614 + removeActiveWord();
3512 2615 });
3513 2616
3514 2617 // Window resize handlers
3515 2618 $(window).on('resize orientationchange', function() {
@@ -3547,59 +2650,8 @@
3547 2650 });
3548 2651
3549 2652
3550 2653 // ====================================
3551 -// INIT LOADER & CHAT CONTAINER HELPERS
3552 -// ====================================
3553 -// These must be outside the email collection block so they're always available
3554 -// (used by persistence loading even when email collection is off)
3555 -
3556 -function showInitLoader(botId) {
3557 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3558 - if (loader) loader.style.display = 'flex';
3559 -}
3560 -
3561 -function hideInitLoader(botId) {
3562 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3563 - if (loader) loader.style.display = 'none';
3564 -}
3565 -
3566 -function showEmailFormForBot(botId) {
3567 - hideInitLoader(botId);
3568 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3569 - var chatContainer = getElementDOM(botId, 'chat-container');
3570 - if (emailBlocker) emailBlocker.style.display = 'flex';
3571 - if (chatContainer) chatContainer.style.display = 'none';
3572 -}
3573 -
3574 -function showChatContainerForBot(botId) {
3575 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3576 - var chatContainer = getElementDOM(botId, 'chat-container');
3577 - if (emailBlocker) emailBlocker.style.display = 'none';
3578 -
3579 - var instance = MxChatInstances.get(botId);
3580 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3581 -
3582 - // If persistence is on and history hasn't loaded yet, show loader
3583 - // while history loads to prevent flash of empty chat
3584 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3585 - if (chatContainer) chatContainer.style.display = 'none';
3586 - showInitLoader(botId);
3587 - loadChatHistory(botId, function() {
3588 - hideInitLoader(botId);
3589 - if (chatContainer) chatContainer.style.display = 'flex';
3590 - scrollToBottom(botId, true);
3591 - });
3592 - } else {
3593 - hideInitLoader(botId);
3594 - if (chatContainer) chatContainer.style.display = 'flex';
3595 - if (typeof loadChatHistory === 'function') {
3596 - loadChatHistory(botId);
3597 - }
3598 - }
3599 -}
3600 -
3601 -// ====================================
3602 2654 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3603 2655 // ====================================
3604 2656 // Only run email collection setup if it's enabled
3605 2657 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3635,8 +2687,28 @@
3635 2687 `;
3636 2688 document.head.appendChild(style);
3637 2689 }
3638 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 +
3639 2711 function isValidEmailAddress(email) {
3640 2712 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3641 2713 return emailRegex.test(email.trim()) && email.length <= 254;
3642 2714 }
@@ -3758,31 +2830,11 @@
3758 2830 existingErrors.forEach(error => error.remove());
3759 2831 }
3760 2832 }
3761 2833
3762 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3763 - function resolveEmailState(botId) {
3764 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3765 - if (mxchatChat.initial_email_state.show_email_form) {
3766 - showEmailFormForBot(botId);
3767 - } else {
3768 - showChatContainerForBot(botId);
3769 - }
3770 - } else {
3771 - checkSessionAndEmailForBot(botId);
3772 - }
3773 - }
3774 -
3775 2834 function checkSessionAndEmailForBot(botId) {
3776 - const sessionId = MxChatInstances.ensureSession(botId);
2835 + const sessionId = getChatSession(botId);
3777 2836
3778 - // Hide both panels while we check — show loader instead
3779 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3780 - var chatContainer = getElementDOM(botId, 'chat-container');
3781 - if (emailBlocker) emailBlocker.style.display = 'none';
3782 - if (chatContainer) chatContainer.style.display = 'none';
3783 - showInitLoader(botId);
3784 -
3785 2837 fetch(mxchatChat.ajax_url, {
3786 2838 method: 'POST',
3787 2839 headers: {
3788 2840 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3830,9 +2882,9 @@
3830 2882 var emailInput = getElementDOM(botId, 'user-email');
3831 2883 var nameInput = getElementDOM(botId, 'user-name');
3832 2884 var userEmail = emailInput ? emailInput.value.trim() : '';
3833 2885 var userName = nameInput ? nameInput.value.trim() : '';
3834 - var sessionId = MxChatInstances.ensureSession(botId);
2886 + var sessionId = getChatSession(botId);
3835 2887
3836 2888 // Validate email
3837 2889 if (!userEmail) {
3838 2890 showEmailError(botId, 'Please enter your email address.');
@@ -3955,27 +3007,25 @@
3955 3007 }
3956 3008 });
3957 3009
3958 3010 // Initialize email check for all bot instances
3959 - // For floating bots: defer until widget is opened (zero passive AJAX)
3960 - // For embedded bots: check immediately since the form is visible
3961 3011 $('.mxchat-chatbot-wrapper').each(function() {
3962 3012 var botId = $(this).data('bot-id') || 'default';
3963 3013 var emailBlocker = getElementDOM(botId, 'email-blocker');
3964 3014
3015 + // Only check if email blocker exists for this bot
3965 3016 if (emailBlocker) {
3966 - if (isEmbeddedBot(botId)) {
3967 - // Embedded bots are always visible — check now
3968 - 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);
3969 3027 }
3970 - // Floating bots: handled in the widget open handler
3971 - } else if (isEmbeddedBot(botId)) {
3972 - // Embedded bot, no email collection — load history with loader
3973 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3974 - if (chatPersistenceEnabled) {
3975 - MxChatInstances.ensureSession(botId);
3976 - showChatContainerForBot(botId);
3977 - }
3978 3028 }
3979 3029 });
3980 3030 }
3981 3031
@@ -3985,32 +3035,39 @@
3985 3035 var $chatbot = getElement(botId, 'floating-chatbot');
3986 3036 if ($chatbot.hasClass('hidden')) {
3987 3037 $chatbot.removeClass('hidden').addClass('visible');
3988 3038 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3989 - handlePreChatDismissal(botId);
3039 + $(this).fadeOut(250); // Hide pre-chat message
3990 3040 disableScroll(); // Disable scroll when chatbot opens
3041 + }
3042 + });
3991 3043
3992 - // Load chat history for returning visitors (persistence)
3993 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3994 - if (chatPersistenceEnabled) {
3995 - MxChatInstances.ensureSession(botId);
3996 - }
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
3997 3050
3998 - // Deferred email check — only on first widget open
3999 - var emailBlocker = getElementDOM(botId, 'email-blocker');
4000 - var instance = MxChatInstances.get(botId);
4001 - if (emailBlocker && !instance.emailCheckDone) {
4002 - instance.emailCheckDone = true;
4003 - resolveEmailState(botId);
4004 - } else if (!emailBlocker) {
4005 - 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
4006 3065 }
4007 - }
3066 + });
4008 3067 });
4009 3068
4010 - // Legacy duplicate close handler removed — handled by single event delegation above
4011 3069
4012 -
4013 3070 function hasQuickQuestions(botId) {
4014 3071 botId = botId || 'default';
4015 3072 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
4016 3073 if (!questionsContainer) return false;
@@ -4146,11 +3203,18 @@
4146 3203 });
4147 3204
4148 3205 // Initialize when document is ready
4149 3206 setFullHeight();
3207 + trackOriginatingPage();
4150 3208
4151 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
4152 - // 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 + }
4153 3217
4154 3218 // Initialize chat visibility for all instances
4155 3219 $('.mxchat-chatbot-wrapper').each(function() {
4156 3220 var botId = $(this).data('bot-id') || 'default';
@@ -4203,312 +3267,6 @@
4203 3267 }, 2000);
4204 3268 });
4205 3269 }
4206 3270 }
4207 -});
4208 -
4209 -// ============================================================================
4210 -// SATISFACTION RATING (v3.2.6)
4211 -// ============================================================================
4212 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
4213 -// inactivity following a bot reply. One prompt per session, deduped via
4214 -// localStorage. Runs ONLY when the satisfaction_rating_enabled option is on —
4215 -// the option (default off) is authoritative.
4216 -jQuery(function($) {
4217 - if (typeof mxchatChat === 'undefined') return;
4218 - // wp_localize_script stringifies scalars: a PHP boolean false arrives as
4219 - // '' and true as '1', so this must be an explicit-enable allowlist — the
4220 - // old "disabled when exactly false/'off'" check let '' through and the
4221 - // bubble rendered on sites with the option off/unset (plan-4bba64). PHP
4222 - // now emits 'on'/'off' strings; true/'1'/1 keep cached pre-fix HTML
4223 - // (boolean-true localizations) working.
4224 - // NOTE (plan-32db95): this gate reads the INLINE value at DOM ready and is
4225 - // deliberately NOT re-evaluated after the widget's dynamic-settings refresh
4226 - // merges fresh values over mxchatChat (that merge fires on first widget
4227 - // open, after this module has already decided). Re-evaluating would mean
4228 - // restructuring the whole module to late-bind its listeners — not worth it
4229 - // for a prompt that is at worst stale for one page load on a cached page.
4230 - var sre = mxchatChat.satisfaction_rating_enabled;
4231 - if (sre !== 'on' && sre !== true && sre !== '1' && sre !== 1) return;
4232 -
4233 - // wp_localize_script stringifies ints, so accept both number and numeric string.
4234 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
4235 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
4236 - if (!isFinite(idleSeconds)) idleSeconds = 60;
4237 - if (idleSeconds < 5) idleSeconds = 5;
4238 - if (idleSeconds > 600) idleSeconds = 600;
4239 - var IDLE_MS = idleSeconds * 1000;
4240 - var MIN_BOT_REPLIES = 2;
4241 - var ratingState = {};
4242 -
4243 - function getState(botId) {
4244 - if (!ratingState[botId]) {
4245 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
4246 - }
4247 - return ratingState[botId];
4248 - }
4249 -
4250 - function getSessionId(botId) {
4251 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
4252 - return MxChatInstances.getChatSession(botId);
4253 - }
4254 - return null;
4255 - }
4256 -
4257 - function isAlreadyRated(sessionId) {
4258 - if (!sessionId) return false;
4259 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
4260 - }
4261 -
4262 - function markRated(sessionId) {
4263 - if (!sessionId) return;
4264 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
4265 - }
4266 -
4267 - function esc(s) {
4268 - return String(s == null ? '' : s)
4269 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
4270 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
4271 - }
4272 -
4273 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
4274 - function ratingSkipInlineColors(botId) {
4275 - if (mxchatChat.skip_inline_colors) return true;
4276 - var botAssignments = mxchatChat.bot_theme_assignments || {};
4277 - return botAssignments.hasOwnProperty(botId);
4278 - }
4279 -
4280 - function botBubbleStyleAttr(botId) {
4281 - if (ratingSkipInlineColors(botId)) return '';
4282 - var bg = mxchatChat.bot_message_bg_color;
4283 - var fg = mxchatChat.bot_message_font_color;
4284 - if (!bg && !fg) return '';
4285 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
4286 - }
4287 -
4288 - // Reads the rating bubble's actual computed fg+bg (whatever paints it —
4289 - // the inline color pickers OR the mxchat-theme AI customizer's injected CSS)
4290 - // and paints the filled "Send" pill so it fills with the bot font color and
4291 - // labels in the bubble bg. Mirrors mxchatSyncMenuColors(~:1512) for the read.
4292 - // We paint the submit button DIRECTLY (inline longhand) rather than relying
4293 - // on the CSS rule's var()s: Chromium resolves an INHERITED custom property
4294 - // unreliably inside a descendant's `background`, so a bubble-level var would
4295 - // silently fall back to the literal (white-block bug all over again). Inline
4296 - // longhand always wins. Same transparent-guard as the menu so we never paint
4297 - // a see-through value — in that case the CSS literal fallbacks keep it legible.
4298 - function syncRatingBubbleColors(botId) {
4299 - var $chatBox = getChatBoxByBotId(botId);
4300 - if (!$chatBox || !$chatBox.length) return;
4301 - var bubbleEl = $chatBox.find('.mxchat-rating-bot-bubble').last()[0];
4302 - if (!bubbleEl) return;
4303 - var cs = window.getComputedStyle(bubbleEl);
4304 - var fg = cs.color;
4305 - var bg = cs.backgroundColor;
4306 - var hasFg = fg && fg !== 'rgba(0, 0, 0, 0)' && fg !== 'transparent';
4307 - var hasBg = bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent';
4308 - // Expose on the bubble too, for any inheriting styles / future use.
4309 - if (hasFg) bubbleEl.style.setProperty('--mxchat-bot-fg', fg);
4310 - if (hasBg) bubbleEl.style.setProperty('--mxchat-bot-bg', bg);
4311 - // Paint the Send pill directly — the part that actually fixes the bug.
4312 - var submitEl = bubbleEl.querySelector('.mxchat-rating-submit');
4313 - if (submitEl) {
4314 - if (hasFg) submitEl.style.backgroundColor = fg; // fill = bot font color
4315 - if (hasBg) submitEl.style.color = bg; // label = bubble background
4316 - }
4317 - }
4318 -
4319 - function copy(key) {
4320 - var c = mxchatChat.satisfaction_rating_copy || {};
4321 - var d = {
4322 - question: 'Was this helpful?',
4323 - helpful: 'Helpful',
4324 - not_helpful: 'Not helpful',
4325 - dismiss: 'Dismiss',
4326 - thanks: 'Thanks! Anything we should improve? (optional)',
4327 - placeholder: 'Tell us what could be better…',
4328 - send: 'Send',
4329 - skip: 'Skip',
4330 - saved: 'Thanks for the feedback.'
4331 - };
4332 - return c[key] || d[key];
4333 - }
4334 -
4335 - function thumbUpSvg() {
4336 - 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>';
4337 - }
4338 - function thumbDownSvg() {
4339 - 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>';
4340 - }
4341 -
4342 - function buildPromptHtml(botId) {
4343 - var styleAttr = botBubbleStyleAttr(botId);
4344 - return ''
4345 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4346 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
4347 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
4348 - + '<div class="mxchat-rating-actions">'
4349 - + '<span class="mxchat-rating-buttons">'
4350 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
4351 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
4352 - + '</span>'
4353 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
4354 - + '</div>'
4355 - + '</div>'
4356 - + '</div>';
4357 - }
4358 -
4359 - function buildFeedbackHtml(botId, rating) {
4360 - var styleAttr = botBubbleStyleAttr(botId);
4361 - return ''
4362 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4363 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
4364 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
4365 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
4366 - + '<div class="mxchat-rating-feedback-actions">'
4367 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
4368 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
4369 - + '</div>'
4370 - + '</div>'
4371 - + '</div>';
4372 - }
4373 -
4374 - function buildSavedHtml(botId) {
4375 - var styleAttr = botBubbleStyleAttr(botId);
4376 - return ''
4377 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4378 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
4379 - + '</div>';
4380 - }
4381 -
4382 - function getChatBoxByBotId(botId) {
4383 - var $byId = $('#chat-box-' + botId);
4384 - if ($byId.length) return $byId.first();
4385 - return $('.chat-box').first();
4386 - }
4387 -
4388 - function scrollChatBoxToBottom($chatBox) {
4389 - if (!$chatBox || !$chatBox.length) return;
4390 - $chatBox.scrollTop($chatBox[0].scrollHeight);
4391 - }
4392 -
4393 - function showPrompt(botId) {
4394 - var s = getState(botId);
4395 - if (s.promptShown || s.dismissed) return;
4396 - var sessionId = getSessionId(botId);
4397 - if (!sessionId) return;
4398 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4399 - var $chatBox = getChatBoxByBotId(botId);
4400 - if (!$chatBox.length) return;
4401 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
4402 - $chatBox.append(buildPromptHtml(botId));
4403 - syncRatingBubbleColors(botId);
4404 - s.promptShown = true;
4405 - scrollChatBoxToBottom($chatBox);
4406 - }
4407 -
4408 - function submitRating(botId, rating, feedback) {
4409 - var sessionId = getSessionId(botId);
4410 - if (!sessionId) return;
4411 - $.post(mxchatChat.ajax_url, {
4412 - action: 'mxchat_save_rating',
4413 - session_id: sessionId,
4414 - bot_id: botId,
4415 - rating: rating,
4416 - feedback: feedback || ''
4417 - });
4418 - markRated(sessionId);
4419 - }
4420 -
4421 - function onBotReply(botId) {
4422 - var s = getState(botId);
4423 - s.botReplies += 1;
4424 - if (s.promptShown || s.dismissed) return;
4425 - var sessionId = getSessionId(botId);
4426 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4427 - if (s.botReplies < MIN_BOT_REPLIES) return;
4428 - if (s.idleTimer) clearTimeout(s.idleTimer);
4429 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4430 - }
4431 -
4432 - function onUserMessage(botId) {
4433 - var s = getState(botId);
4434 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4435 - }
4436 -
4437 - function botIdFromChatBox(el) {
4438 - var id = el && el.id ? el.id : '';
4439 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4440 - }
4441 -
4442 - function setupObserver(chatBox) {
4443 - var botId = botIdFromChatBox(chatBox);
4444 - try {
4445 - var observer = new MutationObserver(function(mutations) {
4446 - mutations.forEach(function(m) {
4447 - for (var i = 0; i < m.addedNodes.length; i++) {
4448 - var node = m.addedNodes[i];
4449 - if (!node || node.nodeType !== 1) continue;
4450 - var $n = $(node);
4451 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4452 - 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)
4453 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4454 - }
4455 - });
4456 - });
4457 - observer.observe(chatBox, { childList: true });
4458 - } catch (e) { /* noop */ }
4459 - }
4460 -
4461 - $('.chat-box').each(function() { setupObserver(this); });
4462 -
4463 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4464 - e.preventDefault();
4465 - var $btn = $(this);
4466 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4467 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4468 - var botId = $prompt.data('bot-id') || 'default';
4469 - var rating = parseInt($btn.attr('data-rating'), 10);
4470 - if (rating !== 1 && rating !== -1) return;
4471 - submitRating(botId, rating, '');
4472 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4473 - syncRatingBubbleColors(botId);
4474 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4475 - });
4476 -
4477 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4478 - e.preventDefault();
4479 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4480 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4481 - var botId = $prompt.data('bot-id') || 'default';
4482 - var s = getState(botId);
4483 - s.dismissed = true;
4484 - markRated(getSessionId(botId));
4485 - ($wrap.length ? $wrap : $prompt).remove();
4486 - });
4487 -
4488 - function closeFeedback($fb) {
4489 - var botId = $fb.data('bot-id') || 'default';
4490 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4491 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4492 - syncRatingBubbleColors(botId);
4493 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4494 - }
4495 -
4496 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4497 - e.preventDefault();
4498 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4499 - });
4500 -
4501 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4502 - e.preventDefault();
4503 - var $fb = $(this).closest('.mxchat-rating-feedback');
4504 - var botId = $fb.data('bot-id') || 'default';
4505 - var rating = parseInt($fb.attr('data-rating'), 10);
4506 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4507 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4508 - if (text !== '') {
4509 - submitRating(botId, rating, text);
4510 - }
4511 - closeFeedback($fb);
4512 - });
4513 3271 });
4514 3272