PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.2
MxChat – AI Chatbot & Content Generation for WordPress v3.0.2
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/chat-script.js +579 -1826 3.2.93.0.2 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
@@ -170,15 +10,11 @@
170 10
171 11 // Initialize an instance for a bot
172 12 init: function(botId) {
173 13 if (!this.instances[botId]) {
174 - // When persistence is OFF, track when this session started
175 - // so the AI only sees messages from this page load
176 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
177 -
178 14 this.instances[botId] = {
179 15 botId: botId,
180 - sessionId: null,
16 + sessionId: this.getChatSession(botId),
181 17 lastSeenMessageId: '',
182 18 notificationCheckInterval: null,
183 19 pollingInterval: null,
184 20 processedMessageIds: new Set(),
@@ -184,11 +20,9 @@
184 20 processedMessageIds: new Set(),
185 21 activePdfFile: null,
186 22 activeWordFile: null,
187 23 chatHistoryLoaded: false,
188 - isStreaming: false,
189 - // Fresh context timestamp - only used when persistence is OFF
190 - sessionStartTimestamp: chatPersistenceEnabled ? 0 : Date.now()
24 + isStreaming: false
191 25 };
192 26 }
193 27 return this.instances[botId];
194 28 },
@@ -203,77 +37,23 @@
203 37 return Object.keys(this.instances);
204 38 },
205 39
206 40 // 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 41 getChatSession: function(botId) {
210 42 var cookieName = 'mxchat_session_id_' + botId;
211 - var storageKey = 'mxchat_session_id_' + botId;
212 43 var sessionId = getCookie(cookieName);
213 44
214 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
215 45 if (!sessionId) {
216 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
46 + sessionId = generateSessionId();
47 + this.setChatSession(botId, sessionId);
217 48 }
218 49
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;
50 + return sessionId;
238 51 },
239 52
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 53 setChatSession: function(botId, sessionId) {
272 54 var cookieName = 'mxchat_session_id_' + botId;
273 - var storageKey = 'mxchat_session_id_' + botId;
274 55 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
275 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
276 56 if (this.instances[botId]) {
277 57 this.instances[botId].sessionId = sessionId;
278 58 }
279 59 },
@@ -278,10 +58,8 @@
278 58 }
279 59 },
280 60
281 61 resetChatSession: function(botId) {
282 - // Clear old session from localStorage before setting new one
283 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
284 62 var newSessionId = generateSessionId();
285 63 this.setChatSession(botId, newSessionId);
286 64 var $chatBox = getElement(botId, 'chat-box');
287 65 if ($chatBox.length) {
@@ -290,20 +68,8 @@
290 68 if (this.instances[botId]) {
291 69 this.instances[botId].chatHistoryLoaded = false;
292 70 this.instances[botId].processedMessageIds = new Set();
293 71 }
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 72 }
307 73 };
308 74
309 75 // ====================================
@@ -603,106 +369,13 @@
603 369 sendButton.disabled = false;
604 370 sendButton.style.opacity = '1';
605 371 sendButton.style.pointerEvents = 'auto';
606 372 }
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 373 }
611 374
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 375 // Update your existing sendMessage function
702 376 function sendMessage(botId) {
703 377 botId = botId || 'default';
704 - MxChatInstances.ensureSession(botId);
705 378 var $chatInput = getElement(botId, 'chat-input');
706 379 var message = $chatInput.val();
707 380
708 381 // ADD PROMPT HOOK HERE
@@ -710,14 +383,10 @@
710 383 message = customMxChatFilter(message, "prompt");
711 384 }
712 385
713 386 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 - }
387 + // Disable input while waiting for response
388 + disableChatInput(botId);
720 389
721 390 appendMessage("user", message, '', [], false, botId);
722 391 $chatInput.val('');
723 392 $chatInput.css('height', 'auto');
@@ -727,9 +396,9 @@
727 396 }
728 397 appendThinkingMessage(botId);
729 398 scrollToBottom(botId);
730 399
731 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
400 + const currentModel = mxchatChat.model || 'gpt-4o';
732 401
733 402 // Check if streaming is enabled AND supported for this model
734 403 if (shouldUseStreaming(currentModel)) {
735 404 callMxChatStream(message, function(response) {
@@ -745,9 +414,8 @@
745 414
746 415 // Update your existing sendMessageToChatbot function
747 416 function sendMessageToChatbot(message, botId) {
748 417 botId = botId || 'default';
749 - MxChatInstances.ensureSession(botId);
750 418
751 419 // ADD PROMPT HOOK HERE
752 420 if (typeof customMxChatFilter === 'function') {
753 421 message = customMxChatFilter(message, "prompt");
@@ -752,14 +420,10 @@
752 420 if (typeof customMxChatFilter === 'function') {
753 421 message = customMxChatFilter(message, "prompt");
754 422 }
755 423
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 - }
424 + // Disable input while waiting for response
425 + disableChatInput(botId);
762 426
763 427 var sessionId = getChatSession(botId);
764 428
765 429 if (hasQuickQuestions(botId)) {
@@ -767,9 +431,9 @@
767 431 }
768 432 appendThinkingMessage(botId);
769 433 scrollToBottom(botId);
770 434
771 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
435 + const currentModel = mxchatChat.model || 'gpt-4o';
772 436
773 437 // Check if streaming is enabled AND supported for this model
774 438 if (shouldUseStreaming(currentModel)) {
775 439 callMxChatStream(message, function(response) {
@@ -838,15 +502,8 @@
838 502
839 503 function callMxChat(message, callback, botId) {
840 504 botId = botId || getMxChatBotId();
841 505
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 506 // Store the message in case we need to retry after session reset
850 507 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
851 508
852 509 // Get page context if contextual awareness is enabled
@@ -851,44 +508,24 @@
851 508
852 509 // Get page context if contextual awareness is enabled
853 510 const pageContext = getPageContext();
854 511
855 - // Get instance for session start timestamp (used when persistence is OFF)
856 - var instance = MxChatInstances.get(botId);
857 -
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 512 // Prepare AJAX data
874 513 const ajaxData = {
875 514 action: 'mxchat_handle_chat_request',
876 515 message: message,
877 - session_id: sessionId,
516 + session_id: getChatSession(botId),
878 517 nonce: mxchatChat.nonce,
879 518 current_page_url: window.location.href,
880 519 current_page_title: document.title,
881 - bot_id: botId,
882 - // Pass session start timestamp so AI context matches what user sees
883 - session_start_timestamp: instance.sessionStartTimestamp || 0
520 + bot_id: botId
884 521 };
885 -
522 +
886 523 // Add page context if available
887 524 if (pageContext) {
888 525 ajaxData.page_context = JSON.stringify(pageContext);
889 526 }
890 -
527 +
891 528 // CHECK FOR VISION FLAGS AND ADD THEM
892 529 if (window.mxchatVisionProcessed) {
893 530 ajaxData.vision_processed = true;
894 531 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -897,9 +534,9 @@
897 534 window.mxchatVisionProcessed = false;
898 535 window.mxchatOriginalMessage = null;
899 536 window.mxchatVisionImagesCount = 0;
900 537 }
901 -
538 +
902 539 $.ajax({
903 540 url: mxchatChat.ajax_url,
904 541 type: 'POST',
905 542 dataType: 'json',
@@ -937,16 +574,23 @@
937 574 errorMessage = "An error occurred. Please try again or contact support.";
938 575 }
939 576
940 577 // Handle session reset action (IP changed, session expired, etc.)
941 - // Silent reset — keep chat UI intact, just get a new session and retry
942 578 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)
579 + // Clear the old session and generate a new one
580 + resetChatSession(botId);
581 + // Remove the temporary loading message
582 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
583 + // Re-send the original message with the new session
945 584 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
946 585 if (originalMessage) {
947 586 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
948 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
587 + // Re-add the user message and thinking indicator
588 + appendMessage("user", originalMessage, '', [], false, botId);
589 + appendThinkingMessage(botId);
590 + scrollToBottom(botId);
591 + // Determine whether to use streaming
592 + const currentModel = mxchatChat.model || 'gpt-4o';
949 593 if (shouldUseStreaming(currentModel)) {
950 594 callMxChatStream(originalMessage, function(response) {
951 595 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
952 596 }, botId);
@@ -1003,11 +647,9 @@
1003 647 }
1004 648
1005 649 // Check for live agent response
1006 650 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
1007 - removeThinkingDots(botId);
1008 651 updateChatModeIndicator('agent', botId);
1009 - enableChatInput(botId);
1010 652 return;
1011 653 }
1012 654
1013 655 // Handle the message and show notification if chat is hidden
@@ -1040,13 +682,9 @@
1040 682 $badge.show();
1041 683 }
1042 684 }
1043 685 } 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);
686 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
1049 687 }
1050 688
1051 689 if (response.message_id) {
1052 690 var instance = MxChatInstances.get(botId);
@@ -1088,9 +726,8 @@
1088 726
1089 727 replaceLastMessage("bot", errorMessage, '', [], botId);
1090 728 }
1091 729 });
1092 - }); // refreshNonceIfNeeded
1093 730 }
1094 731
1095 732 function callMxChatStream(message, callback, botId) {
1096 733 botId = botId || getMxChatBotId();
@@ -1097,9 +734,9 @@
1097 734
1098 735 // Store the message in case we need to retry after session reset
1099 736 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
1100 737
1101 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
738 + const currentModel = mxchatChat.model || 'gpt-4o';
1102 739 if (!isStreamingSupported(currentModel)) {
1103 740 callMxChat(message, callback, botId);
1104 741 return;
1105 742 }
@@ -1106,35 +743,17 @@
1106 743
1107 744 // Get page context if contextual awareness is enabled
1108 745 const pageContext = getPageContext();
1109 746
1110 - // Get instance for session start timestamp (used when persistence is OFF)
1111 - var instance = MxChatInstances.get(botId);
1112 -
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 747 const formData = new FormData();
1127 748 formData.append('action', 'mxchat_stream_chat');
1128 749 formData.append('message', message);
1129 - formData.append('session_id', streamSessionId);
750 + formData.append('session_id', getChatSession(botId));
1130 751 formData.append('nonce', mxchatChat.nonce);
1131 752 formData.append('current_page_url', window.location.href);
1132 753 formData.append('current_page_title', document.title);
1133 754 formData.append('bot_id', botId);
1134 - // Pass session start timestamp so AI context matches what user sees
1135 - formData.append('session_start_timestamp', instance.sessionStartTimestamp || 0);
1136 -
755 +
1137 756 // Add page context if available
1138 757 if (pageContext) {
1139 758 formData.append('page_context', JSON.stringify(pageContext));
1140 759 }
@@ -1153,20 +772,12 @@
1153 772 let accumulatedContent = '';
1154 773 let testingDataReceived = false;
1155 774 let streamingStarted = false;
1156 775
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 776 fetch(mxchatChat.ajax_url, {
1165 777 method: 'POST',
1166 778 body: formData,
1167 - credentials: 'same-origin',
1168 - signal: streamControl.controller.signal
779 + credentials: 'same-origin'
1169 780 })
1170 781 .then(response => {
1171 782 // Store the response for potential fallback handling
1172 783 const responseClone = response.clone();
@@ -1232,19 +843,8 @@
1232 843 });
1233 844 return;
1234 845 }
1235 846
1236 - // Re-enable chat input when stream ends with content
1237 - enableChatInput(botId);
1238 -
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 847 if (callback) {
1248 848 callback(accumulatedContent);
1249 849 }
1250 850 return;
@@ -1267,16 +867,8 @@
1267 867
1268 868 // Re-enable chat input after streaming completes
1269 869 enableChatInput(botId);
1270 870
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 871 if (callback) {
1280 872 callback(accumulatedContent);
1281 873 }
1282 874 return;
@@ -1333,9 +925,8 @@
1333 925 }
1334 926
1335 927 processStream();
1336 928 }).catch(streamError => {
1337 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1338 929 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1339 930 callMxChat(message, callback, botId);
1340 931 });
1341 932 }
@@ -1342,9 +933,8 @@
1342 933
1343 934 processStream();
1344 935 })
1345 936 .catch(error => {
1346 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1347 937 // Check if we have server error data with chat mode
1348 938 if (error && error.isServerError && error.data) {
1349 939 // Check for chat mode in error data
1350 940 if (error.data.chat_mode) {
@@ -1357,9 +947,8 @@
1357 947 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1358 948 callMxChat(message, callback, botId);
1359 949 }
1360 950 });
1361 - }); // refreshNonceIfNeeded
1362 951 }
1363 952
1364 953 // Helper function to handle non-streaming responses
1365 954 function handleNonStreamResponse(data, callback, botId) {
@@ -1398,16 +987,21 @@
1398 987 errorMessage = "An error occurred. Please try again or contact support.";
1399 988 }
1400 989
1401 990 // Handle session reset action (IP changed, session expired, etc.)
1402 - // Silent reset — keep chat UI intact, just get a new session and retry
1403 991 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)
992 + // Clear the old session and generate a new one
993 + resetChatSession(botId);
994 + // Re-send the original message with the new session
1406 995 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1407 996 if (originalMessage) {
1408 997 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1409 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
998 + // Re-add the user message and thinking indicator
999 + appendMessage("user", originalMessage, '', [], false, botId);
1000 + appendThinkingMessage(botId);
1001 + scrollToBottom(botId);
1002 + // Determine whether to use streaming
1003 + const currentModel = mxchatChat.model || 'gpt-4o';
1410 1004 if (shouldUseStreaming(currentModel)) {
1411 1005 callMxChatStream(originalMessage, callback, botId);
1412 1006 } else {
1413 1007 callMxChat(originalMessage, callback, botId);
@@ -1429,22 +1023,8 @@
1429 1023 }
1430 1024 return; // Exit early for errors
1431 1025 }
1432 1026
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 1027 // Handle different response formats
1448 1028 if (data.text || data.html || data.message) {
1449 1029
1450 1030 // Apply response hooks
@@ -1489,15 +1069,19 @@
1489 1069 }
1490 1070
1491 1071 // Enhanced updateChatModeIndicator function for immediate DOM updates
1492 1072 function updateChatModeIndicator(mode, botId) {
1073 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1493 1074 botId = botId || 'default';
1494 1075 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1076 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1495 1077 if (indicator) {
1496 1078 const oldText = indicator.textContent;
1079 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1497 1080
1498 1081 if (mode === 'agent') {
1499 1082 indicator.textContent = 'Live Agent';
1083 + console.log('[MxChat] Mode is agent, calling startPolling...');
1500 1084 startPolling(botId);
1501 1085 } else {
1502 1086 // Everything else is AI mode
1503 1087 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1568,21 +1152,9 @@
1568 1152 // Update the event handlers to use the correct function names (using event delegation)
1569 1153 // Use class-based selectors for multi-instance support
1570 1154 $(document).on('click', '.send-button', function() {
1571 1155 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 - }
1156 + disableChatInput(botId);
1585 1157 sendMessage(botId);
1586 1158 });
1587 1159
1588 1160 // Override enter key handler (using event delegation)
@@ -1589,295 +1161,14 @@
1589 1161 $(document).on('keypress', '.chat-input', function(e) {
1590 1162 if (e.which == 13 && !e.shiftKey) {
1591 1163 e.preventDefault();
1592 1164 var botId = getBotIdFromElement(this);
1593 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1594 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1595 - disableChatInput(botId);
1596 - }
1165 + disableChatInput(botId);
1597 1166 sendMessage(botId);
1598 1167 }
1599 1168 });
1600 1169
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 - return items;
1621 -}
1622 -
1623 -// Builds a clean markdown transcript of the current conversation and triggers
1624 -// a file download. Used by the "Download Transcript" menu item.
1625 -function mxchatDownloadTranscript(botId) {
1626 - var $chatBox = getElement(botId, 'chat-box');
1627 - if (!$chatBox || !$chatBox.length) return;
1628 -
1629 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1630 - var headerTitle = settings.print_header_title || 'Chat transcript';
1631 - var now = new Date();
1632 - var stamp = now.toLocaleString();
1633 -
1634 - var lines = [];
1635 - lines.push('# ' + headerTitle);
1636 - lines.push('');
1637 - lines.push('Exported: ' + stamp);
1638 - lines.push('');
1639 - lines.push('---');
1640 - lines.push('');
1641 -
1642 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1643 - var $msg = $(this);
1644 - // Skip thinking placeholders and any in-flight temporary messages.
1645 - if ($msg.find('.thinking-dots').length) return;
1646 - if ($msg.hasClass('temporary-message')) return;
1647 -
1648 - var sender;
1649 - if ($msg.hasClass('user-message')) sender = 'User';
1650 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1651 - else sender = 'AI Agent';
1652 -
1653 - // Strip interactive UI from the cloned message so we get the conversation text.
1654 - var $clone = $msg.clone();
1655 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1656 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1657 - if (!text) return;
1658 -
1659 - lines.push('**' + sender + '**');
1660 - lines.push('');
1661 - lines.push(text);
1662 - lines.push('');
1663 - });
1664 -
1665 - var content = lines.join('\n');
1666 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1667 - var fname = 'mxchat-transcript-' + iso + '.md';
1668 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1669 - var url = URL.createObjectURL(blob);
1670 - var a = document.createElement('a');
1671 - a.href = url;
1672 - a.download = fname;
1673 - a.style.display = 'none';
1674 - document.body.appendChild(a);
1675 - a.click();
1676 - setTimeout(function() {
1677 - if (a.parentNode) a.parentNode.removeChild(a);
1678 - URL.revokeObjectURL(url);
1679 - }, 100);
1680 -}
1681 -
1682 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1683 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1684 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1685 -function mxchatSyncMenuColors(botId, $wrap) {
1686 - if (!$wrap || !$wrap.length) return;
1687 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1688 - if (!$bot.length) return;
1689 - var cs = window.getComputedStyle($bot[0]);
1690 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1691 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1692 - }
1693 - // Bot text color usually lives on a child div, not .bot-message itself.
1694 - var $textChild = $bot.find('[style*="color"]').first();
1695 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1696 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1697 -}
1698 -
1699 -// Renders (or re-renders) the item list for one menu wrap. Split out of
1700 -// mxchatInitHeaderMenu so the dynamic-settings merge (plan-32db95) can
1701 -// rebuild items + trigger visibility WITHOUT re-binding the one-time
1702 -// open/close/keyboard wiring. closeMenu is passed in by the init closure;
1703 -// a rebuild before init (never happens, but harmless) just skips it.
1704 -function mxchatRenderHeaderMenuItems(botId, $wrap, closeMenuFn) {
1705 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1706 - var $menu = $wrap.find('.mxchat-header-menu');
1707 - var items = mxchatGetHeaderMenuItems(botId);
1708 -
1709 - $menu.empty();
1710 -
1711 - if (!items.length) {
1712 - $trigger.hide();
1713 - $menu.hide();
1714 - return;
1715 - }
1716 -
1717 - // Clear any inline display:none a previous zero-item render left behind —
1718 - // open/close visibility is governed by the hidden prop + is-open class.
1719 - $trigger.css('display', '');
1720 - $menu.css('display', '');
1721 -
1722 - items.forEach(function(item, idx) {
1723 - var $btn = $('<button>', {
1724 - type: 'button',
1725 - 'class': 'mxchat-menu-item',
1726 - 'role': 'menuitem',
1727 - 'tabindex': '-1',
1728 - 'data-menu-id': item.id,
1729 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1730 - '<span class="mxchat-menu-item-label"></span>'
1731 - });
1732 - $btn.find('.mxchat-menu-item-label').text(item.label);
1733 - $btn.on('click', function(e) {
1734 - e.preventDefault();
1735 - e.stopPropagation();
1736 - if (closeMenuFn) closeMenuFn();
1737 - try { item.action(); } catch (err) { /* no-op */ }
1738 - });
1739 - $menu.append($btn);
1740 - });
1741 -}
1742 -
1743 -// Re-render every menu on the page after a dynamic-settings merge
1744 -// (multi-bot: each wrap re-reads its items). An OPEN menu is left alone —
1745 -// swapping items under the user mid-interaction yanks focus — and the
1746 -// rebuild runs when it closes instead (closeMenu checks the pending flag).
1747 -function mxchatRebuildHeaderMenus() {
1748 - $('.mxchat-header-menu-wrap').each(function() {
1749 - var $wrap = $(this);
1750 - var botId = $wrap.data('bot-id');
1751 - if (!botId) return;
1752 - if (!$wrap.data('mxchatMenuReady')) {
1753 - mxchatInitHeaderMenu(botId);
1754 - return;
1755 - }
1756 - if ($wrap.find('.mxchat-header-menu').hasClass('is-open')) {
1757 - $wrap.data('mxchatMenuRebuildPending', true);
1758 - return;
1759 - }
1760 - mxchatRenderHeaderMenuItems(botId, $wrap, $wrap.data('mxchatMenuClose'));
1761 - });
1762 -}
1763 -
1764 -// One-time per-widget init: renders menu items, wires open/close,
1765 -// outside-click, Escape, and arrow-key navigation. If no items, hides the
1766 -// trigger. Wiring happens even when there are zero items at init, so a
1767 -// later dynamic-settings rebuild that adds items has a working trigger.
1768 -function mxchatInitHeaderMenu(botId) {
1769 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1770 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1771 -
1772 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1773 - var $menu = $wrap.find('.mxchat-header-menu');
1774 -
1775 - // Initial color sync — covers normal page load.
1776 - mxchatSyncMenuColors(botId, $wrap);
1777 -
1778 - function openMenu() {
1779 - // Re-sync each open in case the active theme changed since init.
1780 - mxchatSyncMenuColors(botId, $wrap);
1781 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1782 - $trigger.attr('aria-expanded', 'true');
1783 - // Focus the first item for keyboard users
1784 - setTimeout(function() {
1785 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1786 - }, 0);
1787 - }
1788 - function closeMenu(returnFocus) {
1789 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1790 - $trigger.attr('aria-expanded', 'false');
1791 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1792 - if (returnFocus) $trigger.trigger('focus');
1793 - // A dynamic-settings rebuild that arrived while the menu was open
1794 - // was deferred (mxchatRebuildHeaderMenus) — run it now.
1795 - if ($wrap.data('mxchatMenuRebuildPending')) {
1796 - $wrap.removeData('mxchatMenuRebuildPending');
1797 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1798 - }
1799 - }
1800 -
1801 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1802 - // click-to-collapse handler does not fire.
1803 - $trigger.on('click', function(e) {
1804 - e.preventDefault();
1805 - e.stopPropagation();
1806 - if ($menu.hasClass('is-open')) closeMenu();
1807 - else openMenu();
1808 - });
1809 -
1810 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1811 - $menu.on('click', function(e) {
1812 - e.stopPropagation();
1813 - });
1814 -
1815 - // Outside click closes the menu.
1816 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1817 - if (!$menu.hasClass('is-open')) return;
1818 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1819 - closeMenu();
1820 - });
1821 -
1822 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1823 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1824 - var $items = $menu.find('.mxchat-menu-item');
1825 - var idx = $items.index(this);
1826 - if (e.key === 'Escape') {
1827 - e.preventDefault();
1828 - closeMenu(true);
1829 - } else if (e.key === 'ArrowDown') {
1830 - e.preventDefault();
1831 - var $next = $items.eq((idx + 1) % $items.length);
1832 - $items.attr('tabindex', '-1');
1833 - $next.attr('tabindex', '0').trigger('focus');
1834 - } else if (e.key === 'ArrowUp') {
1835 - e.preventDefault();
1836 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1837 - $items.attr('tabindex', '-1');
1838 - $prev.attr('tabindex', '0').trigger('focus');
1839 - } else if (e.key === 'Enter' || e.key === ' ') {
1840 - e.preventDefault();
1841 - $(this).trigger('click');
1842 - }
1843 - });
1844 - $trigger.on('keydown', function(e) {
1845 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1846 - e.preventDefault();
1847 - closeMenu(true);
1848 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1849 - e.preventDefault();
1850 - openMenu();
1851 - }
1852 - });
1853 -
1854 - // Expose closeMenu for out-of-closure re-renders (mxchatRebuildHeaderMenus),
1855 - // then do the initial item render.
1856 - $wrap.data('mxchatMenuClose', closeMenu);
1857 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1858 -
1859 - $wrap.data('mxchatMenuReady', true);
1860 -}
1861 -
1862 -// Initialize header menus for every rendered widget on DOM ready.
1863 -$(function() {
1864 - $('.mxchat-header-menu-wrap').each(function() {
1865 - var botId = $(this).data('bot-id');
1866 - if (botId) mxchatInitHeaderMenu(botId);
1867 - });
1868 -
1869 - // Embedded (non-floating) widgets are open from the moment the page
1870 - // renders — refresh dynamic settings at init (plan-32db95). Floating
1871 - // widgets refresh on first launcher open instead.
1872 - var hasEmbeddedWidget = $('.mxchat-chatbot-wrapper').filter(function() {
1873 - return !$(this).closest('.floating-chatbot').length;
1874 - }).length > 0;
1875 - if (hasEmbeddedWidget) {
1876 - mxchatRefreshDynamicSettings();
1877 - }
1878 -});
1879 -
1170 +
1880 1171 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1881 1172 try {
1882 1173 // Determine styles based on sender type
1883 1174 let messageClass, bgColor, fontColor;
@@ -1915,12 +1206,17 @@
1915 1206 'margin-bottom': '1em'
1916 1207 });
1917 1208 }
1918 1209
1919 - // Process the message content - always run linkify to convert markdown
1920 - // links and format text. linkify() handles existing HTML safely via
1921 - // negative lookaheads that skip URLs already inside <a> tags.
1922 - let fullMessage = linkify(messageText);
1210 + // Process the message content based on sender
1211 + let fullMessage;
1212 + if (sender === "user") {
1213 + // For user messages, apply linkify after sanitization
1214 + fullMessage = linkify(messageText);
1215 + } else {
1216 + // For bot/agent messages, preserve HTML
1217 + fullMessage = messageText;
1218 + }
1923 1219
1924 1220 // Add images if provided
1925 1221 if (images && images.length > 0) {
1926 1222 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -1969,12 +1265,8 @@
1969 1265 if (lastUserMessage.length) {
1970 1266 scrollElementToTop(lastUserMessage, botId);
1971 1267 }
1972 1268 }
1973 -
1974 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1975 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1976 - }
1977 1269 });
1978 1270
1979 1271 if (messageText.id) {
1980 1272 var instance = MxChatInstances.get(botId);
@@ -2059,12 +1351,26 @@
2059 1351 bgColor = botMessageBgColor;
2060 1352 fontColor = botMessageFontColor;
2061 1353 }
2062 1354
2063 - // Always run linkify to convert markdown links and format text.
2064 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
2065 - // to avoid double-processing URLs that are already inside <a> tags).
2066 - var fullMessage = linkify(responseText);
1355 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1356 + // This prevents double-processing of URLs that are already formatted as HTML
1357 + var fullMessage;
1358 + if (sender === "user") {
1359 + // Always linkify user messages (they're plain text)
1360 + fullMessage = linkify(responseText);
1361 + } else {
1362 + // For bot/agent messages, check if HTML already exists
1363 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1364 + responseText.includes('<img') || responseText.includes('<div') ||
1365 + responseText.includes('<p>') || responseText.includes('<br>')) {
1366 + // Response already has HTML, don't process it
1367 + fullMessage = responseText;
1368 + } else {
1369 + // Plain text response, apply linkify
1370 + fullMessage = linkify(responseText);
1371 + }
1372 + }
2067 1373
2068 1374 if (responseHtml) {
2069 1375 // Only add line breaks if there's actual text content before the HTML
2070 1376 if (fullMessage && fullMessage.trim()) {
@@ -2121,12 +1427,8 @@
2121 1427 }
2122 1428
2123 1429 // Re-enable chat input after response is displayed
2124 1430 enableChatInput(botId);
2125 -
2126 - if (sender === "bot" || sender === "agent") {
2127 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2128 - }
2129 1431 } else {
2130 1432 appendMessage(sender, responseText, responseHtml, images, false, botId);
2131 1433 // Re-enable chat input after response is displayed
2132 1434 enableChatInput(botId);
@@ -2135,15 +1437,8 @@
2135 1437
2136 1438
2137 1439 function appendThinkingMessage(botId) {
2138 1440 botId = botId || 'default';
2139 -
2140 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
2141 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
2142 - if (indicator && indicator.textContent === 'Live Agent') {
2143 - return;
2144 - }
2145 -
2146 1441 var $chatBox = getElement(botId, 'chat-box');
2147 1442
2148 1443 // Remove any existing thinking dots in this bot's chat first
2149 1444 $chatBox.find('.thinking-dots').remove();
@@ -2165,9 +1460,9 @@
2165 1460 '</div>' +
2166 1461 '</div>';
2167 1462
2168 1463 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
2169 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1464 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
2170 1465 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
2171 1466 scrollToBottom(botId);
2172 1467 }
2173 1468
@@ -2173,11 +1468,9 @@
2173 1468
2174 1469 function removeThinkingDots(botId) {
2175 1470 botId = botId || 'default';
2176 1471 var $chatBox = getElement(botId, 'chat-box');
2177 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
2178 1472 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
2179 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
2180 1473 }
2181 1474
2182 1475 // ====================================
2183 1476 // TEXT FORMATTING & PROCESSING
@@ -2211,12 +1504,9 @@
2211 1504 processedText = formatTextStyling(processedText);
2212 1505
2213 1506 // Process code blocks BEFORE processing links
2214 1507 processedText = formatCodeBlocks(processedText);
2215 -
2216 - // Process markdown tables BEFORE converting newlines to paragraphs
2217 - processedText = formatMarkdownTables(processedText);
2218 -
1508 +
2219 1509 // NOW convert to paragraphs
2220 1510 processedText = convertNewlinesToBreaks(processedText);
2221 1511
2222 1512 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -2229,63 +1519,37 @@
2229 1519 // Return as a proper link without the brackets
2230 1520 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2231 1521 });
2232 1522
2233 - // Process markdown links: [text](url) and [](url)
2234 - // Uses balanced parenthesis matching to handle URLs containing parens
2235 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
2236 - processedText = (function(input) {
2237 - var result = '';
2238 - var i = 0;
2239 - while (i < input.length) {
2240 - // Look for [ at current position
2241 - if (input[i] === '[') {
2242 - // Find closing ]
2243 - var closeBracket = input.indexOf(']', i + 1);
2244 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
2245 - result += input[i];
2246 - i++;
2247 - continue;
2248 - }
2249 - var linkText = input.substring(i + 1, closeBracket);
2250 - // Check if URL starts with http
2251 - var urlStart = closeBracket + 2;
2252 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
2253 - result += input[i];
2254 - i++;
2255 - continue;
2256 - }
2257 - // Find balanced closing paren
2258 - var depth = 1;
2259 - var j = urlStart;
2260 - while (j < input.length && depth > 0) {
2261 - if (input[j] === '(') depth++;
2262 - else if (input[j] === ')') depth--;
2263 - if (depth > 0) j++;
2264 - }
2265 - if (depth !== 0) {
2266 - result += input[i];
2267 - i++;
2268 - continue;
2269 - }
2270 - var url = input.substring(urlStart, j);
2271 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
2272 - var encodedUrl = safeEncodeUrl(cleanUrl);
2273 - if (!linkText || !linkText.trim()) {
2274 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
2275 - } else {
2276 - var safeText = sanitizeUserInput(linkText);
2277 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
2278 - }
2279 - i = j + 1; // Skip past the closing )
2280 - } else {
2281 - result += input[i];
2282 - i++;
2283 - }
1523 + // Process proper markdown links with text: [text](url)
1524 + // This MUST have non-empty text in the first brackets
1525 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1526 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1527 + // Make sure we have actual text (not just whitespace)
1528 + if (!text || !text.trim()) {
1529 + // If no text, treat the URL as the text
1530 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1531 + const safeUrl = safeEncodeUrl(cleanUrl);
1532 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2284 1533 }
2285 - return result;
2286 - })(processedText);
1534 +
1535 + // Clean the URL
1536 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1537 + const safeUrl = safeEncodeUrl(cleanUrl);
1538 + const safeText = sanitizeUserInput(text);
1539 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1540 + });
2287 1541
1542 + // Handle empty markdown links: [](url)
1543 + // This is a specific case where there's no text
1544 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1545 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1546 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1547 + const safeUrl = safeEncodeUrl(cleanUrl);
1548 + // Use the URL itself as the link text
1549 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1550 + });
1551 +
2288 1552 // Process phone numbers: [text](tel:number)
2289 1553 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
2290 1554 processedText = processedText.replace(phonePattern, (match, text, phone) => {
2291 1555 const safePhone = safeEncodeUrl(phone);
@@ -2437,78 +1701,9 @@
2437 1701 });
2438 1702
2439 1703 return text;
2440 1704 }
2441 -
2442 - function formatMarkdownTables(text) {
2443 - var lines = text.split('\n');
2444 - var result = [];
2445 - var i = 0;
2446 -
2447 - while (i < lines.length) {
2448 - // Check for a table: current line has pipes AND next line is a separator row
2449 - if (i + 1 < lines.length &&
2450 - lines[i].indexOf('|') !== -1 &&
2451 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2452 -
2453 - var tableLines = [];
2454 - var headerLine = lines[i];
2455 - var separatorLine = lines[i + 1];
2456 - tableLines.push(headerLine);
2457 - tableLines.push(separatorLine);
2458 -
2459 - // Collect remaining table rows
2460 - var j = i + 2;
2461 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2462 - tableLines.push(lines[j]);
2463 - j++;
2464 - }
2465 -
2466 - // Parse alignment from separator row
2467 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2468 - var alignments = sepCells.map(function(cell) {
2469 - var trimmed = cell.trim();
2470 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2471 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2472 - return 'left';
2473 - });
2474 -
2475 - // Build HTML table
2476 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2477 -
2478 - // Header row
2479 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2480 - html += '<thead><tr>';
2481 - headerCells.forEach(function(cell, idx) {
2482 - var align = alignments[idx] || 'left';
2483 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2484 - });
2485 - html += '</tr></thead>';
2486 -
2487 - // Body rows
2488 - html += '<tbody>';
2489 - for (var r = 2; r < tableLines.length; r++) {
2490 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2491 - html += '<tr>';
2492 - rowCells.forEach(function(cell, idx) {
2493 - var align = alignments[idx] || 'left';
2494 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2495 - });
2496 - html += '</tr>';
2497 - }
2498 - html += '</tbody></table></div>';
2499 -
2500 - result.push(html);
2501 - i = j;
2502 - } else {
2503 - result.push(lines[i]);
2504 - i++;
2505 - }
2506 - }
2507 -
2508 - return result.join('\n');
2509 - }
2510 -
1705 +
2511 1706 function sanitizeUserInput(text) {
2512 1707 const div = document.createElement('div');
2513 1708 div.textContent = text;
2514 1709 return div.innerHTML;
@@ -2579,14 +1774,13 @@
2579 1774 requestAnimationFrame(smoothScroll);
2580 1775 }
2581 1776 }
2582 1777
2583 - function scrollElementToTop(element, botId, topOffset) {
1778 + function scrollElementToTop(element, botId) {
2584 1779 botId = botId || 'default';
2585 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2586 1780 var chatBox = getElement(botId, 'chat-box');
2587 1781 var elementTop = element.position().top + chatBox.scrollTop();
2588 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1782 + chatBox.animate({ scrollTop: elementTop }, 500);
2589 1783 }
2590 1784
2591 1785 function showChatWidget(botId) {
2592 1786 botId = botId || 'default';
@@ -2730,12 +1924,15 @@
2730 1924 // LIVE AGENT FUNCTIONALITY
2731 1925 // ====================================
2732 1926
2733 1927 function startPolling(botId) {
1928 + console.log('[MxChat] startPolling called for botId:', botId);
2734 1929 botId = botId || 'default';
2735 1930 var instance = MxChatInstances.get(botId);
2736 1931 // Clear any existing interval first
2737 1932 stopPolling(botId);
1933 + // Start new polling interval
1934 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2738 1935 instance.pollingInterval = setInterval(function() {
2739 1936 checkForAgentMessages(botId);
2740 1937 }, 5000);
2741 1938 }
@@ -2740,17 +1937,20 @@
2740 1937 }, 5000);
2741 1938 }
2742 1939
2743 1940 function stopPolling(botId) {
1941 + console.log('[MxChat] stopPolling called for botId:', botId);
2744 1942 botId = botId || 'default';
2745 1943 var instance = MxChatInstances.get(botId);
2746 1944 if (instance.pollingInterval) {
2747 1945 clearInterval(instance.pollingInterval);
2748 1946 instance.pollingInterval = null;
1947 + console.log('[MxChat] Polling stopped for botId:', botId);
2749 1948 }
2750 1949 }
2751 1950
2752 1951 function checkForAgentMessages(botId) {
1952 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2753 1953 botId = botId || 'default';
2754 1954 var instance = MxChatInstances.get(botId);
2755 1955 const sessionId = getChatSession(botId);
2756 1956 $.ajax({
@@ -2776,12 +1976,8 @@
2776 1976 instance.processedMessageIds.add(message.id);
2777 1977 }
2778 1978 });
2779 1979
2780 - if (hasNewMessage) {
2781 - enableChatInput(botId);
2782 - }
2783 -
2784 1980 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2785 1981 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2786 1982 showNotification(botId);
2787 1983 }
@@ -2787,13 +1983,8 @@
2787 1983 }
2788 1984
2789 1985 scrollToBottom(botId, true);
2790 1986 }
2791 -
2792 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2793 - if (response.success && response.data?.chat_mode) {
2794 - updateChatModeIndicator(response.data.chat_mode, botId);
2795 - }
2796 1987 },
2797 1988 error: function (xhr, status, error) {
2798 1989 // Polling error - silently continue
2799 1990 }
@@ -2803,29 +1994,20 @@
2803 1994 // ====================================
2804 1995 // CHAT HISTORY & PERSISTENCE
2805 1996 // ====================================
2806 1997
2807 -function loadChatHistory(botId, onComplete) {
1998 +function loadChatHistory(botId) {
2808 1999 botId = botId || 'default';
2809 2000 var instance = MxChatInstances.get(botId);
2810 2001
2811 2002 // Prevent duplicate loading
2812 2003 if (instance.chatHistoryLoaded) {
2813 - if (onComplete) onComplete();
2814 2004 return;
2815 2005 }
2816 2006
2817 - // Use getChatSession which returns null if no session exists (does NOT create one)
2818 2007 var sessionId = getChatSession(botId);
2819 2008 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2820 2009
2821 - // No session yet — nothing to load. History will load after first message via ensureSession.
2822 - if (!sessionId) {
2823 - instance.chatHistoryLoaded = true;
2824 - if (onComplete) onComplete();
2825 - return;
2826 - }
2827 -
2828 2010 if (chatPersistenceEnabled && sessionId) {
2829 2011 $.ajax({
2830 2012 url: mxchatChat.ajax_url,
2831 2013 type: 'POST',
@@ -2836,12 +2018,11 @@
2836 2018 },
2837 2019 success: function(response) {
2838 2020 // Handle session reset (IP changed while user was away)
2839 2021 if (response.success === false && response.data && response.data.action === 'reset_session') {
2840 - // Silent reset — new session but don't clear UI
2841 - MxChatInstances.silentResetSession(botId);
2022 + // Silently reset session - user will start fresh
2023 + resetChatSession(botId);
2842 2024 instance.chatHistoryLoaded = true; // Prevent retry loop
2843 - if (onComplete) onComplete();
2844 2025 return;
2845 2026 }
2846 2027
2847 2028 // Check if the response indicates success
@@ -2897,19 +2078,9 @@
2897 2078 var content = message.content;
2898 2079 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2899 2080 content = decodeHTMLEntities(content);
2900 2081
2901 - // Skip linkify for messages containing structured HTML
2902 - // (forms, product cards, galleries, etc.) to avoid
2903 - // markdown formatting corrupting HTML attributes
2904 - // (e.g. underscores in name="field_name" becoming <em> tags)
2905 - if (content.includes("mxchat-product-card") ||
2906 - content.includes("mxchat-image-gallery") ||
2907 - content.includes("mxchat-featured-products") ||
2908 - content.includes("<form") ||
2909 - content.includes("<input") ||
2910 - content.includes("<select") ||
2911 - content.includes("<textarea")) {
2082 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2912 2083 messageElement.html(content);
2913 2084 } else {
2914 2085 var formattedContent = linkify(content);
2915 2086 messageElement.html(formattedContent);
@@ -2949,17 +2120,13 @@
2949 2120 instance.chatHistoryLoaded = true;
2950 2121 }
2951 2122 }
2952 2123 }
2953 - if (onComplete) onComplete();
2954 2124 },
2955 2125 error: function(xhr, status, error) {
2956 2126 // Error loading chat history - silently continue
2957 - if (onComplete) onComplete();
2958 2127 }
2959 2128 });
2960 - } else {
2961 - if (onComplete) onComplete();
2962 2129 }
2963 2130 }
2964 2131
2965 2132
@@ -3023,10 +2190,10 @@
3023 2190 .then(data => {
3024 2191 if (data.success) {
3025 2192 container.style.display = 'none';
3026 2193 nameElement.textContent = '';
3027 - instance.activePdfFile = null;
3028 - appendMessage('bot', 'PDF removed.', '', [], false, botId);
2194 + activePdfFile = null;
2195 + appendMessage('bot', 'PDF removed.');
3029 2196 }
3030 2197 })
3031 2198 .catch(error => {
3032 2199 // Error removing PDF - silently continue
@@ -3032,16 +2199,14 @@
3032 2199 // Error removing PDF - silently continue
3033 2200 });
3034 2201 }
3035 2202
3036 - function removeActiveWord(botId) {
3037 - botId = botId || 'default';
3038 - var instance = MxChatInstances.get(botId);
3039 - const container = getElementDOM(botId, 'active-word-container');
3040 - const nameElement = getElementDOM(botId, 'active-word-name');
3041 -
3042 - if (!container || !nameElement || !instance.activeWordFile) return;
3043 -
2203 + function removeActiveWord() {
2204 + const container = document.getElementById('active-word-container');
2205 + const nameElement = document.getElementById('active-word-name');
2206 +
2207 + if (!container || !nameElement || !activeWordFile) return;
2208 +
3044 2209 fetch(mxchatChat.ajax_url, {
3045 2210 method: 'POST',
3046 2211 headers: {
3047 2212 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3047,9 +2212,9 @@
3047 2212 'Content-Type': 'application/x-www-form-urlencoded',
3048 2213 },
3049 2214 body: new URLSearchParams({
3050 2215 'action': 'mxchat_remove_word',
3051 - 'session_id': getChatSession(botId),
2216 + 'session_id': sessionId,
3052 2217 'nonce': mxchatChat.nonce
3053 2218 })
3054 2219 })
3055 2220 .then(response => response.json())
@@ -3056,10 +2221,10 @@
3056 2221 .then(data => {
3057 2222 if (data.success) {
3058 2223 container.style.display = 'none';
3059 2224 nameElement.textContent = '';
3060 - instance.activeWordFile = null;
3061 - appendMessage('bot', 'Word document removed.', '', [], false, botId);
2225 + activeWordFile = null;
2226 + appendMessage('bot', 'Word document removed.');
3062 2227 }
3063 2228 })
3064 2229 .catch(error => {
3065 2230 // Error removing Word document - silently continue
@@ -3137,35 +2302,45 @@
3137 2302 // ====================================
3138 2303
3139 2304 function checkPreChatDismissal(botId) {
3140 2305 botId = botId || 'default';
3141 - try {
3142 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
3143 - if (dismissedAt) {
3144 - // Re-show after 24 hours
3145 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
3146 - if (elapsed < 86400000) {
2306 + $.ajax({
2307 + url: mxchatChat.ajax_url,
2308 + type: 'POST',
2309 + data: {
2310 + action: 'mxchat_check_pre_chat_message_status',
2311 + _ajax_nonce: mxchatChat.nonce
2312 + },
2313 + success: function(response) {
2314 + if (response.success && !response.data.dismissed) {
2315 + getElement(botId, 'pre-chat-message').fadeIn(250);
2316 + } else {
3147 2317 getElement(botId, 'pre-chat-message').hide();
3148 - return;
3149 2318 }
3150 - // Expired — clear and show again
3151 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2319 + },
2320 + error: function() {
2321 + // Error checking pre-chat dismissal - silently continue
3152 2322 }
3153 - getElement(botId, 'pre-chat-message').fadeIn(250);
3154 - } catch (e) {
3155 - // localStorage unavailable — show the message
3156 - getElement(botId, 'pre-chat-message').fadeIn(250);
3157 - }
2323 + });
3158 2324 }
3159 2325
3160 2326 function handlePreChatDismissal(botId) {
3161 2327 botId = botId || 'default';
3162 2328 getElement(botId, 'pre-chat-message').fadeOut(200);
3163 - try {
3164 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
3165 - } catch (e) {
3166 - // localStorage unavailable — dismissal won't persist
3167 - }
2329 + $.ajax({
2330 + url: mxchatChat.ajax_url,
2331 + type: 'POST',
2332 + data: {
2333 + action: 'mxchat_dismiss_pre_chat_message',
2334 + _ajax_nonce: mxchatChat.nonce
2335 + },
2336 + success: function() {
2337 + $('#pre-chat-message').hide();
2338 + },
2339 + error: function() {
2340 + // Error dismissing pre-chat message - silently continue
2341 + }
2342 + });
3168 2343 }
3169 2344
3170 2345
3171 2346 // ====================================
@@ -3220,14 +2395,9 @@
3220 2395 collapseQuickQuestions(botId);
3221 2396 });
3222 2397
3223 2398 // Chatbot visibility toggle handlers - use class selector for multi-instance support
3224 - // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
3225 - $(document).on('click keydown', '.floating-chatbot-button', function(e) {
3226 - if (e.type === 'keydown') {
3227 - if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
3228 - e.preventDefault();
3229 - }
2399 + $(document).on('click', '.floating-chatbot-button', function() {
3230 2400 var botId = getBotIdFromElement(this);
3231 2401 var $chatbot = getElement(botId, 'floating-chatbot');
3232 2402 var $badge = getElement(botId, 'chat-notification-badge');
3233 2403 var $preChat = getElement(botId, 'pre-chat-message');
@@ -3232,88 +2402,35 @@
3232 2402 var $badge = getElement(botId, 'chat-notification-badge');
3233 2403 var $preChat = getElement(botId, 'pre-chat-message');
3234 2404
3235 2405 if ($chatbot.hasClass('hidden')) {
3236 - $chatbot.removeClass('hidden').addClass('visible')
3237 - .attr('aria-modal', 'true').attr('role', 'dialog');
3238 - $(this).addClass('hidden').attr('aria-expanded', 'true');
2406 + $chatbot.removeClass('hidden').addClass('visible');
2407 + $(this).addClass('hidden');
3239 2408 $badge.hide(); // Hide notification when opening chat
3240 2409 disableScroll();
3241 2410 $preChat.fadeOut(250);
3242 -
3243 - // First open per page load: re-fetch behavior settings in case
3244 - // this page's inline values came from a stale full-page cache
3245 - // (plan-32db95). Idempotent — later opens are a no-op.
3246 - mxchatRefreshDynamicSettings();
3247 -
3248 - // Load chat history for returning visitors (persistence)
3249 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3250 - if (chatPersistenceEnabled) {
3251 - MxChatInstances.ensureSession(botId);
3252 - }
3253 -
3254 - // Deferred email check — only on first widget open
3255 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3256 - var instance = MxChatInstances.get(botId);
3257 - if (emailBlocker && !instance.emailCheckDone) {
3258 - instance.emailCheckDone = true;
3259 - resolveEmailState(botId);
3260 - } else if (!emailBlocker) {
3261 - // No email collection — still route through showChatContainerForBot
3262 - // so the loader is shown while chat history loads
3263 - showChatContainerForBot(botId);
3264 - }
3265 -
3266 - // Move keyboard focus into the message input after the open transition.
3267 - setTimeout(function() {
3268 - var chatInput = getElementDOM(botId, 'chat-input');
3269 - if (chatInput && !chatInput.disabled) {
3270 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
3271 - }
3272 - }, 300);
3273 2411 } else {
3274 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3275 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2412 + $chatbot.removeClass('visible').addClass('hidden');
2413 + $(this).removeClass('hidden');
3276 2414 enableScroll();
3277 2415 checkPreChatDismissal(botId);
3278 2416 }
3279 2417 });
3280 2418
3281 - // Allow clicking anywhere on the title bar to close the chatbot.
3282 - // Returns keyboard focus to the launcher so keyboard users don't get
3283 - // stranded at <body> (WCAG SC 2.4.3 Focus Order). :focus-visible is
3284 - // heuristic-based so mouse-triggered close won't show a focus ring.
2419 + // Allow clicking anywhere on the title bar to close the chatbot
3285 2420 $(document).on('click', '.chatbot-top-bar', function() {
3286 2421 var botId = getBotIdFromElement(this);
3287 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3288 - var $launcher = getElement(botId, 'floating-chatbot-button');
3289 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
2422 + getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2423 + getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3290 2424 enableScroll();
3291 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3292 2425 });
3293 2426
3294 - // Global Escape-key handler — closes any visible chat widget and
3295 - // returns focus to its launcher. Standard modal-dismissal pattern;
3296 - // pairs with aria-modal="true" set on the widget when it opens.
3297 - $(document).on('keydown', function(e) {
3298 - if (e.key !== 'Escape' && e.key !== 'Esc') return;
3299 - var $visible = $('.floating-chatbot.visible');
3300 - if (!$visible.length) return;
3301 - e.preventDefault();
3302 - $visible.each(function() {
3303 - var botId = getBotIdFromElement(this);
3304 - $(this).addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3305 - var $launcher = getElement(botId, 'floating-chatbot-button');
3306 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
3307 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3308 - });
3309 - enableScroll();
3310 - });
3311 -
3312 2427 $(document).on('click', '.close-pre-chat-message', function(e) {
3313 2428 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3314 2429 var botId = getBotIdFromElement(this);
3315 - handlePreChatDismissal(botId);
2430 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2431 + $(this).remove();
2432 + });
3316 2433 });
3317 2434
3318 2435
3319 2436 // PDF upload button handlers - use class selector
@@ -3329,20 +2446,17 @@
3329 2446 var wordInput = getElementDOM(botId, 'word-upload');
3330 2447 if (wordInput) wordInput.click();
3331 2448 });
3332 2449
3333 - // PDF file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'pdf-upload')
3334 - $(document).on('change', '.pdf-upload', async function(e) {
3335 - var botId = getBotIdFromElement(this);
3336 - var instance = MxChatInstances.get(botId);
3337 - const file = this.files[0];
3338 - const sessionId = MxChatInstances.ensureSession(botId);
3339 -
2450 + // PDF file input change handler
2451 + addSafeEventListener('pdf-upload', 'change', async function(e) {
2452 + const file = e.target.files[0];
2453 +
3340 2454 if (!file || file.type !== 'application/pdf') {
3341 2455 alert('Please select a valid PDF file.');
3342 2456 return;
3343 2457 }
3344 -
2458 +
3345 2459 if (!sessionId) {
3346 2460 alert('Error: No session ID found');
3347 2461 return;
3348 2462 }
@@ -3350,49 +2464,47 @@
3350 2464 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3351 2465 alert('Error: Ajax configuration missing');
3352 2466 return;
3353 2467 }
3354 -
2468 +
3355 2469 // Disable buttons and show loading state
3356 - const uploadBtn = getElementDOM(botId, 'pdf-upload-btn');
3357 - const sendBtn = getElementDOM(botId, 'send-button');
3358 - if (!uploadBtn) return;
2470 + const uploadBtn = document.getElementById('pdf-upload-btn');
2471 + const sendBtn = document.getElementById('send-button');
3359 2472 const originalBtnContent = uploadBtn.innerHTML;
3360 -
2473 +
3361 2474 try {
3362 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3363 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3364 2475 const formData = new FormData();
3365 2476 formData.append('action', 'mxchat_upload_pdf');
3366 2477 formData.append('pdf_file', file);
3367 2478 formData.append('session_id', sessionId);
3368 2479 formData.append('nonce', mxchatChat.nonce);
3369 -
2480 +
3370 2481 uploadBtn.disabled = true;
3371 - if (sendBtn) sendBtn.disabled = true;
2482 + sendBtn.disabled = true;
3372 2483 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3373 2484 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3374 2485 </svg>`;
3375 -
2486 +
3376 2487 const response = await fetch(mxchatChat.ajax_url, {
3377 2488 method: 'POST',
3378 2489 body: formData
3379 2490 });
3380 -
2491 +
3381 2492 const data = await response.json();
3382 -
2493 +
3383 2494 if (data.success) {
3384 2495 // Hide popular questions if they exist
3385 - if (hasQuickQuestions(botId)) {
3386 - collapseQuickQuestions(botId);
2496 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2497 + if (hasQuickQuestions()) {
2498 + collapseQuickQuestions();
3387 2499 }
3388 -
2500 +
3389 2501 // Show the active PDF name
3390 - showActivePdf(data.data.filename, botId);
3391 -
3392 - appendMessage('bot', data.data.message, '', [], false, botId);
3393 - scrollToBottom(botId);
3394 - instance.activePdfFile = data.data.filename;
2502 + showActivePdf(data.data.filename);
2503 +
2504 + appendMessage('bot', data.data.message);
2505 + scrollToBottom();
2506 + activePdfFile = data.data.filename;
3395 2507 } else {
3396 2508 alert('Failed to upload PDF. Please try again.');
3397 2509 }
3398 2510 } catch (error) {
@@ -3398,76 +2510,66 @@
3398 2510 } catch (error) {
3399 2511 alert('Error uploading file. Please try again.');
3400 2512 } finally {
3401 2513 uploadBtn.disabled = false;
3402 - if (sendBtn) sendBtn.disabled = false;
2514 + sendBtn.disabled = false;
3403 2515 uploadBtn.innerHTML = originalBtnContent;
3404 2516 this.value = ''; // Reset file input
3405 2517 }
3406 2518 });
3407 2519
3408 - // Word file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'word-upload')
3409 - $(document).on('change', '.word-upload', async function(e) {
3410 - var botId = getBotIdFromElement(this);
3411 - var instance = MxChatInstances.get(botId);
3412 - const file = this.files[0];
3413 - const sessionId = MxChatInstances.ensureSession(botId);
3414 -
2520 + // Word file input change handler
2521 + addSafeEventListener('word-upload', 'change', async function(e) {
2522 + const file = e.target.files[0];
2523 +
3415 2524 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
3416 2525 alert('Please select a valid Word document (.docx).');
3417 2526 return;
3418 2527 }
3419 -
2528 +
3420 2529 if (!sessionId) {
3421 2530 alert('Error: No session ID found');
3422 2531 return;
3423 2532 }
3424 2533
3425 - if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3426 - alert('Error: Ajax configuration missing');
3427 - return;
3428 - }
3429 -
3430 2534 // Disable buttons and show loading state
3431 - const uploadBtn = getElementDOM(botId, 'word-upload-btn');
3432 - const sendBtn = getElementDOM(botId, 'send-button');
3433 - if (!uploadBtn) return;
2535 + const uploadBtn = document.getElementById('word-upload-btn');
2536 + const sendBtn = document.getElementById('send-button');
3434 2537 const originalBtnContent = uploadBtn.innerHTML;
3435 -
2538 +
3436 2539 try {
3437 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3438 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3439 2540 const formData = new FormData();
3440 2541 formData.append('action', 'mxchat_upload_word');
3441 2542 formData.append('word_file', file);
3442 2543 formData.append('session_id', sessionId);
3443 2544 formData.append('nonce', mxchatChat.nonce);
3444 -
2545 +
3445 2546 uploadBtn.disabled = true;
3446 - if (sendBtn) sendBtn.disabled = true;
2547 + sendBtn.disabled = true;
3447 2548 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3448 2549 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3449 2550 </svg>`;
3450 -
2551 +
3451 2552 const response = await fetch(mxchatChat.ajax_url, {
3452 2553 method: 'POST',
3453 2554 body: formData
3454 2555 });
3455 -
2556 +
3456 2557 const data = await response.json();
3457 -
2558 +
3458 2559 if (data.success) {
3459 2560 // Hide popular questions if they exist
3460 - if (hasQuickQuestions(botId)) {
3461 - collapseQuickQuestions(botId);
2561 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2562 + if (hasQuickQuestions()) {
2563 + collapseQuickQuestions();
3462 2564 }
3463 -
2565 +
3464 2566 // Show the active Word document name
3465 - showActiveWord(data.data.filename, botId);
3466 -
3467 - appendMessage('bot', data.data.message, '', [], false, botId);
3468 - scrollToBottom(botId);
3469 - instance.activeWordFile = data.data.filename;
2567 + showActiveWord(data.data.filename);
2568 +
2569 + appendMessage('bot', data.data.message);
2570 + scrollToBottom();
2571 + activeWordFile = data.data.filename;
3470 2572 } else {
3471 2573 alert('Failed to upload Word document. Please try again.');
3472 2574 }
3473 2575 } catch (error) {
@@ -3473,25 +2575,25 @@
3473 2575 } catch (error) {
3474 2576 alert('Error uploading file. Please try again.');
3475 2577 } finally {
3476 2578 uploadBtn.disabled = false;
3477 - if (sendBtn) sendBtn.disabled = false;
2579 + sendBtn.disabled = false;
3478 2580 uploadBtn.innerHTML = originalBtnContent;
3479 2581 this.value = ''; // Reset file input
3480 2582 }
3481 2583 });
3482 2584
3483 - // Remove button click handlers - delegated, bot-aware (were bound to stale un-suffixed ids)
3484 - $(document).on('click', '.remove-pdf-btn', function(e) {
2585 + // Remove button click handlers
2586 + document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
3485 2587 e.preventDefault();
3486 2588 e.stopPropagation();
3487 - removeActivePdf(getBotIdFromElement(this));
2589 + removeActivePdf();
3488 2590 });
3489 -
3490 - $(document).on('click', '.remove-word-btn', function(e) {
2591 +
2592 + document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
3491 2593 e.preventDefault();
3492 2594 e.stopPropagation();
3493 - removeActiveWord(getBotIdFromElement(this));
2595 + removeActiveWord();
3494 2596 });
3495 2597
3496 2598 // Window resize handlers
3497 2599 $(window).on('resize orientationchange', function() {
@@ -3529,437 +2631,380 @@
3529 2631 });
3530 2632
3531 2633
3532 2634 // ====================================
3533 -// INIT LOADER & CHAT CONTAINER HELPERS
2635 +// EMAIL COLLECTION SETUP - FIXED VERSION
3534 2636 // ====================================
3535 -// These must be outside the email collection block so they're always available
3536 -// (used by persistence loading even when email collection is off)
3537 -
3538 -function showInitLoader(botId) {
3539 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3540 - if (loader) loader.style.display = 'flex';
3541 -}
3542 -
3543 -function hideInitLoader(botId) {
3544 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3545 - if (loader) loader.style.display = 'none';
3546 -}
3547 -
3548 -function showEmailFormForBot(botId) {
3549 - hideInitLoader(botId);
3550 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3551 - var chatContainer = getElementDOM(botId, 'chat-container');
3552 - if (emailBlocker) emailBlocker.style.display = 'flex';
3553 - if (chatContainer) chatContainer.style.display = 'none';
3554 -}
3555 -
3556 -function showChatContainerForBot(botId) {
3557 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3558 - var chatContainer = getElementDOM(botId, 'chat-container');
3559 - if (emailBlocker) emailBlocker.style.display = 'none';
3560 -
3561 - var instance = MxChatInstances.get(botId);
3562 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3563 -
3564 - // If persistence is on and history hasn't loaded yet, show loader
3565 - // while history loads to prevent flash of empty chat
3566 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3567 - if (chatContainer) chatContainer.style.display = 'none';
3568 - showInitLoader(botId);
3569 - loadChatHistory(botId, function() {
3570 - hideInitLoader(botId);
3571 - if (chatContainer) chatContainer.style.display = 'flex';
3572 - scrollToBottom(botId, true);
3573 - });
3574 - } else {
3575 - hideInitLoader(botId);
3576 - if (chatContainer) chatContainer.style.display = 'flex';
3577 - if (typeof loadChatHistory === 'function') {
3578 - loadChatHistory(botId);
3579 - }
3580 - }
3581 -}
3582 -
3583 -// ====================================
3584 -// EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3585 -// ====================================
3586 2637 // Only run email collection setup if it's enabled
3587 2638 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2639 + // Email collection form setup and handlers
2640 + const emailForm = document.getElementById('email-collection-form');
2641 + const emailBlocker = document.getElementById('email-blocker');
2642 + const chatbotWrapper = document.getElementById('chat-container');
3588 2643
3589 - // Track submitting state per bot
3590 - const emailSubmittingState = {};
2644 + if (emailForm && emailBlocker && chatbotWrapper) {
2645 +
2646 + // Add loading state management
2647 + let isSubmitting = false;
2648 +
2649 + // Optimized UI transition functions
2650 + function showEmailForm() {
2651 + emailBlocker.style.display = 'flex';
2652 + chatbotWrapper.style.display = 'none';
2653 + }
3591 2654
3592 - // Add CSS animations for email form (once globally)
3593 - if (!document.getElementById('email-error-styles')) {
3594 - const style = document.createElement('style');
3595 - style.id = 'email-error-styles';
3596 - style.textContent = `
3597 - @keyframes fadeInError {
3598 - from { opacity: 0; transform: translateY(-5px); }
3599 - to { opacity: 1; transform: translateY(0); }
2655 + function showChatContainer() {
2656 + // Show chat immediately without delay
2657 + emailBlocker.style.display = 'none';
2658 + chatbotWrapper.style.display = 'flex';
2659 +
2660 + // Load chat history only after showing chat container
2661 + if (typeof loadChatHistory === 'function') {
2662 + loadChatHistory();
3600 2663 }
3601 - .email-input-shake {
3602 - animation: shake 0.5s ease-in-out;
3603 - }
3604 - @keyframes shake {
3605 - 0%, 100% { transform: translateX(0); }
3606 - 25% { transform: translateX(-5px); }
3607 - 75% { transform: translateX(5px); }
3608 - }
3609 - @keyframes spin {
3610 - from { transform: rotate(0deg); }
3611 - to { transform: rotate(360deg); }
3612 - }
3613 - .email-spinner {
3614 - display: inline-block;
3615 - vertical-align: middle;
3616 - }
3617 - `;
3618 - document.head.appendChild(style);
3619 - }
2664 + }
3620 2665
3621 - function isValidEmailAddress(email) {
3622 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3623 - return emailRegex.test(email.trim()) && email.length <= 254;
3624 - }
2666 + // Enhanced email validation
2667 + function isValidEmail(email) {
2668 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2669 + return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2670 + }
3625 2671
3626 - function isValidNameInput(name) {
3627 - return name && name.trim().length >= 2 && name.trim().length <= 100;
3628 - }
2672 + // Enhanced name validation
2673 + function isValidName(name) {
2674 + return name && name.trim().length >= 2 && name.trim().length <= 100;
2675 + }
3629 2676
3630 - /**
3631 - * Replace {visitor_name} placeholder in intro message with actual visitor name
3632 - * @param {string} botId - The bot instance ID
3633 - * @param {string} visitorName - The visitor's name to insert
3634 - */
3635 - function replaceVisitorNamePlaceholder(botId, visitorName) {
3636 - var chatBox = getElementDOM(botId, 'chat-box');
3637 - if (!chatBox) return;
3638 -
3639 - // Find the first bot message (intro message)
3640 - var introMessage = chatBox.querySelector('.bot-message');
3641 - if (!introMessage) return;
3642 -
3643 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3644 - if (!messageContent) return;
3645 -
3646 - var html = messageContent.innerHTML;
3647 -
3648 - // Replace {visitor_name} placeholder (case-insensitive)
3649 - if (visitorName && visitorName.trim()) {
3650 - // Escape HTML to prevent XSS
3651 - var safeName = $('<div>').text(visitorName.trim()).html();
3652 - html = html.replace(/\{visitor_name\}/gi, safeName);
3653 - } else {
3654 - // Remove placeholder and clean up spacing if no name provided
3655 - html = html.replace(/\{visitor_name\}/gi, '');
3656 - // Clean up any double spaces that might result
3657 - html = html.replace(/\s{2,}/g, ' ').trim();
2677 + // Show loading state with spinner
2678 + function setSubmissionState(loading) {
2679 + const submitButton = document.getElementById('email-submit-button');
2680 + const emailInput = document.getElementById('user-email');
2681 + const nameInput = document.getElementById('user-name');
2682 +
2683 + if (loading) {
2684 + isSubmitting = true;
2685 + if (submitButton) submitButton.disabled = true;
2686 + if (emailInput) emailInput.disabled = true;
2687 + if (nameInput) nameInput.disabled = true;
2688 +
2689 + // Store original content and add spinner
2690 + if (submitButton && !submitButton.getAttribute('data-original-html')) {
2691 + submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2692 +
2693 + // Add loading spinner while keeping original text
2694 + const originalText = submitButton.textContent;
2695 + submitButton.innerHTML = `
2696 + <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2697 + <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2698 + <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2699 + <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2700 + </circle>
2701 + </svg>
2702 + ${originalText}
2703 + `;
2704 +
2705 + submitButton.style.opacity = '0.8';
2706 + }
2707 + } else {
2708 + isSubmitting = false;
2709 + if (submitButton) submitButton.disabled = false;
2710 + if (emailInput) emailInput.disabled = false;
2711 + if (nameInput) nameInput.disabled = false;
2712 +
2713 + // Restore original content
2714 + if (submitButton) {
2715 + const originalHtml = submitButton.getAttribute('data-original-html');
2716 + if (originalHtml) {
2717 + submitButton.innerHTML = originalHtml;
2718 + }
2719 + submitButton.style.opacity = '1';
2720 + }
2721 + }
3658 2722 }
3659 2723
3660 - messageContent.innerHTML = html;
3661 - }
3662 -
3663 - function setEmailSubmissionState(botId, loading) {
3664 - var submitButton = getElementDOM(botId, 'email-submit-button');
3665 - var emailInput = getElementDOM(botId, 'user-email');
3666 - var nameInput = getElementDOM(botId, 'user-name');
3667 -
3668 - if (loading) {
3669 - emailSubmittingState[botId] = true;
3670 - if (submitButton) submitButton.disabled = true;
3671 - if (emailInput) emailInput.disabled = true;
3672 - if (nameInput) nameInput.disabled = true;
3673 -
3674 - if (submitButton && !submitButton.getAttribute('data-original-html')) {
3675 - submitButton.setAttribute('data-original-html', submitButton.innerHTML);
3676 - const originalText = submitButton.textContent;
3677 - submitButton.innerHTML = `
3678 - <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
3679 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
3680 - <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
3681 - <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
3682 - </circle>
3683 - </svg>
3684 - ${originalText}
2724 + // Error display functions
2725 + function showEmailError(message) {
2726 + clearEmailError();
2727 +
2728 + const errorDiv = document.createElement('div');
2729 + errorDiv.className = 'email-error';
2730 + errorDiv.style.cssText = `
2731 + color: #e74c3c;
2732 + font-size: 12px;
2733 + margin-top: 8px;
2734 + padding: 4px 0;
2735 + animation: fadeInError 0.3s ease;
2736 + `;
2737 + errorDiv.textContent = message;
2738 +
2739 + // Add CSS animation if not already present
2740 + if (!document.getElementById('email-error-styles')) {
2741 + const style = document.createElement('style');
2742 + style.id = 'email-error-styles';
2743 + style.textContent = `
2744 + @keyframes fadeInError {
2745 + from { opacity: 0; transform: translateY(-5px); }
2746 + to { opacity: 1; transform: translateY(0); }
2747 + }
2748 + .email-input-shake {
2749 + animation: shake 0.5s ease-in-out;
2750 + }
2751 + @keyframes shake {
2752 + 0%, 100% { transform: translateX(0); }
2753 + 25% { transform: translateX(-5px); }
2754 + 75% { transform: translateX(5px); }
2755 + }
2756 + @keyframes spin {
2757 + from { transform: rotate(0deg); }
2758 + to { transform: rotate(360deg); }
2759 + }
2760 + .email-spinner {
2761 + display: inline-block;
2762 + vertical-align: middle;
2763 + }
3685 2764 `;
3686 - submitButton.style.opacity = '0.8';
2765 + document.head.appendChild(style);
3687 2766 }
3688 - } else {
3689 - emailSubmittingState[botId] = false;
3690 - if (submitButton) submitButton.disabled = false;
3691 - if (emailInput) emailInput.disabled = false;
3692 - if (nameInput) nameInput.disabled = false;
3693 -
3694 - if (submitButton) {
3695 - const originalHtml = submitButton.getAttribute('data-original-html');
3696 - if (originalHtml) {
3697 - submitButton.innerHTML = originalHtml;
3698 - }
3699 - submitButton.style.opacity = '1';
2767 +
2768 + emailForm.appendChild(errorDiv);
2769 +
2770 + // Add shake animation to inputs
2771 + const emailInput = document.getElementById('user-email');
2772 + const nameInput = document.getElementById('user-name');
2773 +
2774 + if (emailInput) {
2775 + emailInput.classList.add('email-input-shake');
2776 + setTimeout(() => {
2777 + emailInput.classList.remove('email-input-shake');
2778 + }, 500);
3700 2779 }
2780 +
2781 + if (nameInput) {
2782 + nameInput.classList.add('email-input-shake');
2783 + setTimeout(() => {
2784 + nameInput.classList.remove('email-input-shake');
2785 + }, 500);
2786 + }
3701 2787 }
3702 - }
3703 2788
3704 - function showEmailError(botId, message) {
3705 - clearEmailError(botId);
3706 -
3707 - var emailForm = getElementDOM(botId, 'email-collection-form');
3708 - if (!emailForm) return;
3709 -
3710 - const errorDiv = document.createElement('div');
3711 - errorDiv.className = 'email-error';
3712 - errorDiv.style.cssText = `
3713 - color: #e74c3c;
3714 - font-size: 12px;
3715 - margin-top: 8px;
3716 - padding: 4px 0;
3717 - animation: fadeInError 0.3s ease;
3718 - `;
3719 - errorDiv.textContent = message;
3720 - emailForm.appendChild(errorDiv);
3721 -
3722 - // Add shake animation to inputs
3723 - var emailInput = getElementDOM(botId, 'user-email');
3724 - var nameInput = getElementDOM(botId, 'user-name');
3725 -
3726 - if (emailInput) {
3727 - emailInput.classList.add('email-input-shake');
3728 - setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
3729 - }
3730 - if (nameInput) {
3731 - nameInput.classList.add('email-input-shake');
3732 - setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
3733 - }
3734 - }
3735 -
3736 - function clearEmailError(botId) {
3737 - var emailForm = getElementDOM(botId, 'email-collection-form');
3738 - if (emailForm) {
2789 + function clearEmailError() {
3739 2790 const existingErrors = emailForm.querySelectorAll('.email-error');
3740 2791 existingErrors.forEach(error => error.remove());
3741 2792 }
3742 - }
3743 2793
3744 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3745 - function resolveEmailState(botId) {
3746 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3747 - if (mxchatChat.initial_email_state.show_email_form) {
3748 - showEmailFormForBot(botId);
3749 - } else {
3750 - showChatContainerForBot(botId);
3751 - }
3752 - } else {
3753 - checkSessionAndEmailForBot(botId);
3754 - }
3755 - }
2794 + // MAIN FORM SUBMIT HANDLER
2795 + // Remove any existing event listeners first
2796 + emailForm.removeEventListener('submit', handleFormSubmit);
3756 2797
3757 - function checkSessionAndEmailForBot(botId) {
3758 - const sessionId = MxChatInstances.ensureSession(botId);
2798 + // Add the form submit handler
2799 + emailForm.addEventListener('submit', handleFormSubmit);
3759 2800
3760 - // Hide both panels while we check — show loader instead
3761 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3762 - var chatContainer = getElementDOM(botId, 'chat-container');
3763 - if (emailBlocker) emailBlocker.style.display = 'none';
3764 - if (chatContainer) chatContainer.style.display = 'none';
3765 - showInitLoader(botId);
2801 + function handleFormSubmit(event) {
2802 + event.preventDefault();
2803 + event.stopPropagation();
3766 2804
3767 - fetch(mxchatChat.ajax_url, {
3768 - method: 'POST',
3769 - headers: {
3770 - 'Content-Type': 'application/x-www-form-urlencoded',
3771 - },
3772 - body: new URLSearchParams({
3773 - action: 'mxchat_check_email_provided',
3774 - session_id: sessionId,
3775 - nonce: mxchatChat.nonce,
3776 - })
3777 - })
3778 - .then((response) => {
3779 - if (!response.ok) {
3780 - throw new Error(`HTTP error! status: ${response.status}`);
2805 + // Prevent double submission
2806 + if (isSubmitting) {
2807 + return false;
3781 2808 }
3782 - return response.json();
3783 - })
3784 - .then((data) => {
3785 - if (data.success) {
3786 - if (data.data.logged_in || data.data.email) {
3787 - showChatContainerForBot(botId);
3788 - } else {
3789 - showEmailFormForBot(botId);
3790 - }
3791 - } else {
3792 - showEmailFormForBot(botId);
3793 - }
3794 - })
3795 - .catch((error) => {
3796 - showEmailFormForBot(botId);
3797 - });
3798 - }
3799 2809
3800 - // Event delegation for email form submission
3801 - $(document).on('submit', '.email-collection-form', function(e) {
3802 - e.preventDefault();
3803 - e.stopPropagation();
2810 + const userEmail = document.getElementById('user-email').value.trim();
2811 + const nameInput = document.getElementById('user-name');
2812 + const userName = nameInput ? nameInput.value.trim() : '';
2813 + const sessionId = getChatSession();
3804 2814
3805 - var botId = getBotIdFromElement(this);
2815 + // Validate email before submission
2816 + if (!userEmail) {
2817 + showEmailError('Please enter your email address.');
2818 + return false;
2819 + }
3806 2820
3807 - // Prevent double submission
3808 - if (emailSubmittingState[botId]) {
3809 - return false;
3810 - }
2821 + if (!isValidEmail(userEmail)) {
2822 + showEmailError('Please enter a valid email address.');
2823 + return false;
2824 + }
3811 2825
3812 - var emailInput = getElementDOM(botId, 'user-email');
3813 - var nameInput = getElementDOM(botId, 'user-name');
3814 - var userEmail = emailInput ? emailInput.value.trim() : '';
3815 - var userName = nameInput ? nameInput.value.trim() : '';
3816 - var sessionId = MxChatInstances.ensureSession(botId);
2826 + // Validate name if field exists
2827 + if (nameInput && !isValidName(userName)) {
2828 + showEmailError('Please enter a valid name (2-100 characters).');
2829 + return false;
2830 + }
3817 2831
3818 - // Validate email
3819 - if (!userEmail) {
3820 - showEmailError(botId, 'Please enter your email address.');
3821 - return false;
3822 - }
2832 + // Clear any existing errors
2833 + clearEmailError();
2834 + setSubmissionState(true);
3823 2835
3824 - if (!isValidEmailAddress(userEmail)) {
3825 - showEmailError(botId, 'Please enter a valid email address.');
3826 - return false;
3827 - }
2836 + // Prepare form data with optional name
2837 + const formData = new URLSearchParams({
2838 + action: 'mxchat_handle_save_email_and_response',
2839 + email: userEmail,
2840 + session_id: sessionId,
2841 + nonce: mxchatChat.nonce,
2842 + });
3828 2843
3829 - // Validate name if field exists and has content
3830 - if (nameInput && userName && !isValidNameInput(userName)) {
3831 - showEmailError(botId, 'Please enter a valid name (2-100 characters).');
3832 - return false;
3833 - }
2844 + // Add name to form data if provided
2845 + if (userName) {
2846 + formData.append('name', userName);
2847 + }
3834 2848
3835 - clearEmailError(botId);
3836 - setEmailSubmissionState(botId, true);
2849 + fetch(mxchatChat.ajax_url, {
2850 + method: 'POST',
2851 + headers: {
2852 + 'Content-Type': 'application/x-www-form-urlencoded',
2853 + },
2854 + body: formData
2855 + })
2856 + .then((response) => {
2857 + if (!response.ok) {
2858 + throw new Error(`HTTP error! status: ${response.status}`);
2859 + }
2860 + return response.json();
2861 + })
2862 + .then((data) => {
2863 + setSubmissionState(false);
3837 2864
3838 - // Prepare form data
3839 - const formData = new URLSearchParams({
3840 - action: 'mxchat_handle_save_email_and_response',
3841 - email: userEmail,
3842 - session_id: sessionId,
3843 - nonce: mxchatChat.nonce,
3844 - });
2865 + if (data.success) {
2866 + // Show chat immediately
2867 + showChatContainer();
3845 2868
3846 - if (userName) {
3847 - formData.append('name', userName);
2869 + // Handle bot response if provided
2870 + if (data.message && typeof appendMessage === 'function') {
2871 + setTimeout(() => {
2872 + appendMessage('bot', data.message);
2873 + if (typeof scrollToBottom === 'function') {
2874 + scrollToBottom();
2875 + }
2876 + }, 100);
2877 + }
2878 + } else {
2879 + showEmailError(data.message || 'Failed to save email. Please try again.');
2880 + }
2881 + })
2882 + .catch((error) => {
2883 + setSubmissionState(false);
2884 + showEmailError('An error occurred. Please try again.');
2885 + });
2886 +
2887 + return false; // Extra prevention
3848 2888 }
3849 2889
3850 - fetch(mxchatChat.ajax_url, {
3851 - method: 'POST',
3852 - headers: {
3853 - 'Content-Type': 'application/x-www-form-urlencoded',
3854 - },
3855 - body: formData
3856 - })
3857 - .then((response) => {
3858 - if (!response.ok) {
3859 - throw new Error(`HTTP error! status: ${response.status}`);
3860 - }
3861 - return response.json();
3862 - })
3863 - .then((data) => {
3864 - setEmailSubmissionState(botId, false);
2890 + // Real-time email validation
2891 + const emailInput = document.getElementById('user-email');
2892 + if (emailInput) {
2893 + let validationTimeout;
2894 +
2895 + emailInput.addEventListener('input', function() {
2896 + // Clear previous validation timeout
2897 + if (validationTimeout) {
2898 + clearTimeout(validationTimeout);
2899 + }
2900 +
2901 + // Debounce validation
2902 + validationTimeout = setTimeout(() => {
2903 + const email = this.value.trim();
2904 + clearEmailError();
2905 +
2906 + if (email && !isValidEmail(email)) {
2907 + showEmailError('Please enter a valid email address.');
2908 + }
2909 + }, 500);
2910 + });
3865 2911
3866 - if (data.success) {
3867 - showChatContainerForBot(botId);
2912 + // Handle Enter key
2913 + emailInput.addEventListener('keypress', function(e) {
2914 + if (e.key === 'Enter' && !isSubmitting) {
2915 + e.preventDefault();
2916 + emailForm.dispatchEvent(new Event('submit'));
2917 + }
2918 + });
2919 + }
3868 2920
3869 - // Replace {visitor_name} placeholder in intro message with actual name
3870 - if (userName) {
3871 - replaceVisitorNamePlaceholder(botId, userName);
3872 - } else {
3873 - // Remove placeholder if no name provided
3874 - replaceVisitorNamePlaceholder(botId, '');
2921 + // Real-time name validation
2922 + const nameInput = document.getElementById('user-name');
2923 + if (nameInput) {
2924 + let nameValidationTimeout;
2925 +
2926 + nameInput.addEventListener('input', function() {
2927 + // Clear previous validation timeout
2928 + if (nameValidationTimeout) {
2929 + clearTimeout(nameValidationTimeout);
3875 2930 }
2931 +
2932 + // Debounce validation
2933 + nameValidationTimeout = setTimeout(() => {
2934 + const name = this.value.trim();
2935 + clearEmailError();
2936 +
2937 + if (name && !isValidName(name)) {
2938 + showEmailError('Name must be between 2 and 100 characters.');
2939 + }
2940 + }, 500);
2941 + });
3876 2942
3877 - if (data.message && typeof appendMessage === 'function') {
3878 - setTimeout(() => {
3879 - appendMessage('bot', data.message, '', [], false, botId);
3880 - if (typeof scrollToBottom === 'function') {
3881 - scrollToBottom(botId);
3882 - }
3883 - }, 100);
2943 + // Handle Enter key
2944 + nameInput.addEventListener('keypress', function(e) {
2945 + if (e.key === 'Enter' && !isSubmitting) {
2946 + e.preventDefault();
2947 + emailForm.dispatchEvent(new Event('submit'));
3884 2948 }
2949 + });
2950 + }
2951 +
2952 + // Initial state check
2953 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2954 + const emailState = mxchatChat.initial_email_state;
2955 + if (emailState.show_email_form) {
2956 + showEmailForm();
3885 2957 } else {
3886 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
2958 + showChatContainer();
3887 2959 }
3888 - })
3889 - .catch((error) => {
3890 - setEmailSubmissionState(botId, false);
3891 - showEmailError(botId, 'An error occurred. Please try again.');
3892 - });
3893 -
3894 - return false;
3895 - });
3896 -
3897 - // Real-time email validation using event delegation
3898 - $(document).on('input', '.mxchat-email-input', function() {
3899 - var botId = getBotIdFromElement(this);
3900 - var $input = $(this);
3901 -
3902 - // Clear previous timeout
3903 - clearTimeout($input.data('validationTimeout'));
3904 -
3905 - // Debounce validation
3906 - var timeout = setTimeout(() => {
3907 - var email = this.value.trim();
3908 - clearEmailError(botId);
3909 -
3910 - if (email && !isValidEmailAddress(email)) {
3911 - showEmailError(botId, 'Please enter a valid email address.');
3912 - }
3913 - }, 500);
3914 -
3915 - $input.data('validationTimeout', timeout);
3916 - });
3917 -
3918 - // Handle Enter key in email input
3919 - $(document).on('keypress', '.mxchat-email-input', function(e) {
3920 - if (e.key === 'Enter') {
3921 - e.preventDefault();
3922 - var botId = getBotIdFromElement(this);
3923 - if (!emailSubmittingState[botId]) {
3924 - $(this).closest('.email-collection-form').submit();
3925 - }
2960 + } else {
2961 + // Check email status via AJAX
2962 + setTimeout(checkSessionAndEmail, 100);
3926 2963 }
3927 - });
3928 2964
3929 - // Handle Enter key in name input
3930 - $(document).on('keypress', '.mxchat-name-input', function(e) {
3931 - if (e.key === 'Enter') {
3932 - e.preventDefault();
3933 - var botId = getBotIdFromElement(this);
3934 - if (!emailSubmittingState[botId]) {
3935 - $(this).closest('.email-collection-form').submit();
3936 - }
2965 + // Check if email exists for the current session
2966 + function checkSessionAndEmail() {
2967 + const sessionId = getChatSession();
2968 +
2969 + fetch(mxchatChat.ajax_url, {
2970 + method: 'POST',
2971 + headers: {
2972 + 'Content-Type': 'application/x-www-form-urlencoded',
2973 + },
2974 + body: new URLSearchParams({
2975 + action: 'mxchat_check_email_provided',
2976 + session_id: sessionId,
2977 + nonce: mxchatChat.nonce,
2978 + })
2979 + })
2980 + .then((response) => {
2981 + if (!response.ok) {
2982 + throw new Error(`HTTP error! status: ${response.status}`);
2983 + }
2984 + return response.json();
2985 + })
2986 + .then((data) => {
2987 + if (data.success) {
2988 + if (data.data.logged_in || data.data.email) {
2989 + showChatContainer();
2990 + } else {
2991 + showEmailForm();
2992 + }
2993 + } else {
2994 + // On error, default to showing email form
2995 + showEmailForm();
2996 + }
2997 + })
2998 + .catch((error) => {
2999 + // Email check failed - default to email form
3000 + showEmailForm();
3001 + });
3937 3002 }
3938 - });
3939 3003
3940 - // Initialize email check for all bot instances
3941 - // For floating bots: defer until widget is opened (zero passive AJAX)
3942 - // For embedded bots: check immediately since the form is visible
3943 - $('.mxchat-chatbot-wrapper').each(function() {
3944 - var botId = $(this).data('bot-id') || 'default';
3945 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3946 -
3947 - if (emailBlocker) {
3948 - if (isEmbeddedBot(botId)) {
3949 - // Embedded bots are always visible — check now
3950 - resolveEmailState(botId);
3951 - }
3952 - // Floating bots: handled in the widget open handler
3953 - } else if (isEmbeddedBot(botId)) {
3954 - // Embedded bot, no email collection — load history with loader
3955 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3956 - if (chatPersistenceEnabled) {
3957 - MxChatInstances.ensureSession(botId);
3958 - showChatContainerForBot(botId);
3959 - }
3960 - }
3961 - });
3004 + } else {
3005 + // Email collection is enabled but essential elements are missing - silently continue
3006 + }
3962 3007 }
3963 3008
3964 3009 // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
3965 3010 $(document).on('click', '.pre-chat-message', function() {
@@ -3967,32 +3012,39 @@
3967 3012 var $chatbot = getElement(botId, 'floating-chatbot');
3968 3013 if ($chatbot.hasClass('hidden')) {
3969 3014 $chatbot.removeClass('hidden').addClass('visible');
3970 3015 getElement(botId, 'floating-chatbot-button').addClass('hidden');
3971 - handlePreChatDismissal(botId);
3016 + $(this).fadeOut(250); // Hide pre-chat message
3972 3017 disableScroll(); // Disable scroll when chatbot opens
3018 + }
3019 + });
3973 3020
3974 - // Load chat history for returning visitors (persistence)
3975 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3976 - if (chatPersistenceEnabled) {
3977 - MxChatInstances.ensureSession(botId);
3978 - }
3021 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
3022 + // This is a fallback for legacy support
3023 + $(document).on('click', '.close-pre-chat-message', function() {
3024 + var botId = getBotIdFromElement(this);
3025 + var $preChat = getElement(botId, 'pre-chat-message');
3026 + $preChat.fadeOut(200); // Hide the message
3979 3027
3980 - // Deferred email check — only on first widget open
3981 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3982 - var instance = MxChatInstances.get(botId);
3983 - if (emailBlocker && !instance.emailCheckDone) {
3984 - instance.emailCheckDone = true;
3985 - resolveEmailState(botId);
3986 - } else if (!emailBlocker) {
3987 - showChatContainerForBot(botId);
3028 + // Send an AJAX request to set the transient flag for 24 hours
3029 + $.ajax({
3030 + url: mxchatChat.ajax_url,
3031 + type: 'POST',
3032 + data: {
3033 + action: 'mxchat_dismiss_pre_chat_message',
3034 + _ajax_nonce: mxchatChat.nonce
3035 + },
3036 + success: function() {
3037 + // Ensure the message is hidden after dismissal
3038 + $preChat.hide();
3039 + },
3040 + error: function() {
3041 + // Error dismissing pre-chat message - silently continue
3988 3042 }
3989 - }
3043 + });
3990 3044 });
3991 3045
3992 - // Legacy duplicate close handler removed — handled by single event delegation above
3993 3046
3994 -
3995 3047 function hasQuickQuestions(botId) {
3996 3048 botId = botId || 'default';
3997 3049 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3998 3050 if (!questionsContainer) return false;
@@ -4128,11 +3180,18 @@
4128 3180 });
4129 3181
4130 3182 // Initialize when document is ready
4131 3183 setFullHeight();
3184 + trackOriginatingPage();
4132 3185
4133 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
4134 - // until the user's first interaction via MxChatInstances.ensureSession()
3186 + // Only load chat history if email collection is disabled
3187 + if (mxchatChat.email_collection_enabled !== 'on') {
3188 + // Load history for all instances
3189 + $('.mxchat-chatbot-wrapper').each(function() {
3190 + var botId = $(this).data('bot-id') || 'default';
3191 + loadChatHistory(botId);
3192 + });
3193 + }
4135 3194
4136 3195 // Initialize chat visibility for all instances
4137 3196 $('.mxchat-chatbot-wrapper').each(function() {
4138 3197 var botId = $(this).data('bot-id') || 'default';
@@ -4185,312 +3244,6 @@
4185 3244 }, 2000);
4186 3245 });
4187 3246 }
4188 3247 }
4189 -});
4190 -
4191 -// ============================================================================
4192 -// SATISFACTION RATING (v3.2.6)
4193 -// ============================================================================
4194 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
4195 -// inactivity following a bot reply. One prompt per session, deduped via
4196 -// localStorage. Runs ONLY when the satisfaction_rating_enabled option is on —
4197 -// the option (default off) is authoritative.
4198 -jQuery(function($) {
4199 - if (typeof mxchatChat === 'undefined') return;
4200 - // wp_localize_script stringifies scalars: a PHP boolean false arrives as
4201 - // '' and true as '1', so this must be an explicit-enable allowlist — the
4202 - // old "disabled when exactly false/'off'" check let '' through and the
4203 - // bubble rendered on sites with the option off/unset (plan-4bba64). PHP
4204 - // now emits 'on'/'off' strings; true/'1'/1 keep cached pre-fix HTML
4205 - // (boolean-true localizations) working.
4206 - // NOTE (plan-32db95): this gate reads the INLINE value at DOM ready and is
4207 - // deliberately NOT re-evaluated after the widget's dynamic-settings refresh
4208 - // merges fresh values over mxchatChat (that merge fires on first widget
4209 - // open, after this module has already decided). Re-evaluating would mean
4210 - // restructuring the whole module to late-bind its listeners — not worth it
4211 - // for a prompt that is at worst stale for one page load on a cached page.
4212 - var sre = mxchatChat.satisfaction_rating_enabled;
4213 - if (sre !== 'on' && sre !== true && sre !== '1' && sre !== 1) return;
4214 -
4215 - // wp_localize_script stringifies ints, so accept both number and numeric string.
4216 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
4217 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
4218 - if (!isFinite(idleSeconds)) idleSeconds = 60;
4219 - if (idleSeconds < 5) idleSeconds = 5;
4220 - if (idleSeconds > 600) idleSeconds = 600;
4221 - var IDLE_MS = idleSeconds * 1000;
4222 - var MIN_BOT_REPLIES = 2;
4223 - var ratingState = {};
4224 -
4225 - function getState(botId) {
4226 - if (!ratingState[botId]) {
4227 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
4228 - }
4229 - return ratingState[botId];
4230 - }
4231 -
4232 - function getSessionId(botId) {
4233 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
4234 - return MxChatInstances.getChatSession(botId);
4235 - }
4236 - return null;
4237 - }
4238 -
4239 - function isAlreadyRated(sessionId) {
4240 - if (!sessionId) return false;
4241 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
4242 - }
4243 -
4244 - function markRated(sessionId) {
4245 - if (!sessionId) return;
4246 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
4247 - }
4248 -
4249 - function esc(s) {
4250 - return String(s == null ? '' : s)
4251 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
4252 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
4253 - }
4254 -
4255 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
4256 - function ratingSkipInlineColors(botId) {
4257 - if (mxchatChat.skip_inline_colors) return true;
4258 - var botAssignments = mxchatChat.bot_theme_assignments || {};
4259 - return botAssignments.hasOwnProperty(botId);
4260 - }
4261 -
4262 - function botBubbleStyleAttr(botId) {
4263 - if (ratingSkipInlineColors(botId)) return '';
4264 - var bg = mxchatChat.bot_message_bg_color;
4265 - var fg = mxchatChat.bot_message_font_color;
4266 - if (!bg && !fg) return '';
4267 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
4268 - }
4269 -
4270 - // Reads the rating bubble's actual computed fg+bg (whatever paints it —
4271 - // the inline color pickers OR the mxchat-theme AI customizer's injected CSS)
4272 - // and paints the filled "Send" pill so it fills with the bot font color and
4273 - // labels in the bubble bg. Mirrors mxchatSyncMenuColors(~:1512) for the read.
4274 - // We paint the submit button DIRECTLY (inline longhand) rather than relying
4275 - // on the CSS rule's var()s: Chromium resolves an INHERITED custom property
4276 - // unreliably inside a descendant's `background`, so a bubble-level var would
4277 - // silently fall back to the literal (white-block bug all over again). Inline
4278 - // longhand always wins. Same transparent-guard as the menu so we never paint
4279 - // a see-through value — in that case the CSS literal fallbacks keep it legible.
4280 - function syncRatingBubbleColors(botId) {
4281 - var $chatBox = getChatBoxByBotId(botId);
4282 - if (!$chatBox || !$chatBox.length) return;
4283 - var bubbleEl = $chatBox.find('.mxchat-rating-bot-bubble').last()[0];
4284 - if (!bubbleEl) return;
4285 - var cs = window.getComputedStyle(bubbleEl);
4286 - var fg = cs.color;
4287 - var bg = cs.backgroundColor;
4288 - var hasFg = fg && fg !== 'rgba(0, 0, 0, 0)' && fg !== 'transparent';
4289 - var hasBg = bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent';
4290 - // Expose on the bubble too, for any inheriting styles / future use.
4291 - if (hasFg) bubbleEl.style.setProperty('--mxchat-bot-fg', fg);
4292 - if (hasBg) bubbleEl.style.setProperty('--mxchat-bot-bg', bg);
4293 - // Paint the Send pill directly — the part that actually fixes the bug.
4294 - var submitEl = bubbleEl.querySelector('.mxchat-rating-submit');
4295 - if (submitEl) {
4296 - if (hasFg) submitEl.style.backgroundColor = fg; // fill = bot font color
4297 - if (hasBg) submitEl.style.color = bg; // label = bubble background
4298 - }
4299 - }
4300 -
4301 - function copy(key) {
4302 - var c = mxchatChat.satisfaction_rating_copy || {};
4303 - var d = {
4304 - question: 'Was this helpful?',
4305 - helpful: 'Helpful',
4306 - not_helpful: 'Not helpful',
4307 - dismiss: 'Dismiss',
4308 - thanks: 'Thanks! Anything we should improve? (optional)',
4309 - placeholder: 'Tell us what could be better…',
4310 - send: 'Send',
4311 - skip: 'Skip',
4312 - saved: 'Thanks for the feedback.'
4313 - };
4314 - return c[key] || d[key];
4315 - }
4316 -
4317 - function thumbUpSvg() {
4318 - 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>';
4319 - }
4320 - function thumbDownSvg() {
4321 - 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>';
4322 - }
4323 -
4324 - function buildPromptHtml(botId) {
4325 - var styleAttr = botBubbleStyleAttr(botId);
4326 - return ''
4327 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4328 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
4329 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
4330 - + '<div class="mxchat-rating-actions">'
4331 - + '<span class="mxchat-rating-buttons">'
4332 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
4333 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
4334 - + '</span>'
4335 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
4336 - + '</div>'
4337 - + '</div>'
4338 - + '</div>';
4339 - }
4340 -
4341 - function buildFeedbackHtml(botId, rating) {
4342 - var styleAttr = botBubbleStyleAttr(botId);
4343 - return ''
4344 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4345 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
4346 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
4347 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
4348 - + '<div class="mxchat-rating-feedback-actions">'
4349 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
4350 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
4351 - + '</div>'
4352 - + '</div>'
4353 - + '</div>';
4354 - }
4355 -
4356 - function buildSavedHtml(botId) {
4357 - var styleAttr = botBubbleStyleAttr(botId);
4358 - return ''
4359 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4360 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
4361 - + '</div>';
4362 - }
4363 -
4364 - function getChatBoxByBotId(botId) {
4365 - var $byId = $('#chat-box-' + botId);
4366 - if ($byId.length) return $byId.first();
4367 - return $('.chat-box').first();
4368 - }
4369 -
4370 - function scrollChatBoxToBottom($chatBox) {
4371 - if (!$chatBox || !$chatBox.length) return;
4372 - $chatBox.scrollTop($chatBox[0].scrollHeight);
4373 - }
4374 -
4375 - function showPrompt(botId) {
4376 - var s = getState(botId);
4377 - if (s.promptShown || s.dismissed) return;
4378 - var sessionId = getSessionId(botId);
4379 - if (!sessionId) return;
4380 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4381 - var $chatBox = getChatBoxByBotId(botId);
4382 - if (!$chatBox.length) return;
4383 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
4384 - $chatBox.append(buildPromptHtml(botId));
4385 - syncRatingBubbleColors(botId);
4386 - s.promptShown = true;
4387 - scrollChatBoxToBottom($chatBox);
4388 - }
4389 -
4390 - function submitRating(botId, rating, feedback) {
4391 - var sessionId = getSessionId(botId);
4392 - if (!sessionId) return;
4393 - $.post(mxchatChat.ajax_url, {
4394 - action: 'mxchat_save_rating',
4395 - session_id: sessionId,
4396 - bot_id: botId,
4397 - rating: rating,
4398 - feedback: feedback || ''
4399 - });
4400 - markRated(sessionId);
4401 - }
4402 -
4403 - function onBotReply(botId) {
4404 - var s = getState(botId);
4405 - s.botReplies += 1;
4406 - if (s.promptShown || s.dismissed) return;
4407 - var sessionId = getSessionId(botId);
4408 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4409 - if (s.botReplies < MIN_BOT_REPLIES) return;
4410 - if (s.idleTimer) clearTimeout(s.idleTimer);
4411 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4412 - }
4413 -
4414 - function onUserMessage(botId) {
4415 - var s = getState(botId);
4416 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4417 - }
4418 -
4419 - function botIdFromChatBox(el) {
4420 - var id = el && el.id ? el.id : '';
4421 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4422 - }
4423 -
4424 - function setupObserver(chatBox) {
4425 - var botId = botIdFromChatBox(chatBox);
4426 - try {
4427 - var observer = new MutationObserver(function(mutations) {
4428 - mutations.forEach(function(m) {
4429 - for (var i = 0; i < m.addedNodes.length; i++) {
4430 - var node = m.addedNodes[i];
4431 - if (!node || node.nodeType !== 1) continue;
4432 - var $n = $(node);
4433 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4434 - 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)
4435 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4436 - }
4437 - });
4438 - });
4439 - observer.observe(chatBox, { childList: true });
4440 - } catch (e) { /* noop */ }
4441 - }
4442 -
4443 - $('.chat-box').each(function() { setupObserver(this); });
4444 -
4445 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4446 - e.preventDefault();
4447 - var $btn = $(this);
4448 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4449 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4450 - var botId = $prompt.data('bot-id') || 'default';
4451 - var rating = parseInt($btn.attr('data-rating'), 10);
4452 - if (rating !== 1 && rating !== -1) return;
4453 - submitRating(botId, rating, '');
4454 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4455 - syncRatingBubbleColors(botId);
4456 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4457 - });
4458 -
4459 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4460 - e.preventDefault();
4461 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4462 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4463 - var botId = $prompt.data('bot-id') || 'default';
4464 - var s = getState(botId);
4465 - s.dismissed = true;
4466 - markRated(getSessionId(botId));
4467 - ($wrap.length ? $wrap : $prompt).remove();
4468 - });
4469 -
4470 - function closeFeedback($fb) {
4471 - var botId = $fb.data('bot-id') || 'default';
4472 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4473 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4474 - syncRatingBubbleColors(botId);
4475 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4476 - }
4477 -
4478 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4479 - e.preventDefault();
4480 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4481 - });
4482 -
4483 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4484 - e.preventDefault();
4485 - var $fb = $(this).closest('.mxchat-rating-feedback');
4486 - var botId = $fb.data('bot-id') || 'default';
4487 - var rating = parseInt($fb.attr('data-rating'), 10);
4488 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4489 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4490 - if (text !== '') {
4491 - submitRating(botId, rating, text);
4492 - }
4493 - closeFeedback($fb);
4494 - });
4495 3248 });
4496 3249