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 -1910 3.2.113.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,18 +383,13 @@
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 - mxchatUpdateCharCounter($chatInput[0]); // reset the char counter after send (plan 7091a2)
724 392 $chatInput.css('height', 'auto');
725 393
726 394 if (hasQuickQuestions(botId)) {
727 395 collapseQuickQuestions(botId);
@@ -728,9 +396,9 @@
728 396 }
729 397 appendThinkingMessage(botId);
730 398 scrollToBottom(botId);
731 399
732 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
400 + const currentModel = mxchatChat.model || 'gpt-4o';
733 401
734 402 // Check if streaming is enabled AND supported for this model
735 403 if (shouldUseStreaming(currentModel)) {
736 404 callMxChatStream(message, function(response) {
@@ -746,9 +414,8 @@
746 414
747 415 // Update your existing sendMessageToChatbot function
748 416 function sendMessageToChatbot(message, botId) {
749 417 botId = botId || 'default';
750 - MxChatInstances.ensureSession(botId);
751 418
752 419 // ADD PROMPT HOOK HERE
753 420 if (typeof customMxChatFilter === 'function') {
754 421 message = customMxChatFilter(message, "prompt");
@@ -753,14 +420,10 @@
753 420 if (typeof customMxChatFilter === 'function') {
754 421 message = customMxChatFilter(message, "prompt");
755 422 }
756 423
757 - // Don't disable input in live agent mode - let users chat freely
758 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
759 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
760 - if (!isAgentMode) {
761 - disableChatInput(botId);
762 - }
424 + // Disable input while waiting for response
425 + disableChatInput(botId);
763 426
764 427 var sessionId = getChatSession(botId);
765 428
766 429 if (hasQuickQuestions(botId)) {
@@ -768,9 +431,9 @@
768 431 }
769 432 appendThinkingMessage(botId);
770 433 scrollToBottom(botId);
771 434
772 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
435 + const currentModel = mxchatChat.model || 'gpt-4o';
773 436
774 437 // Check if streaming is enabled AND supported for this model
775 438 if (shouldUseStreaming(currentModel)) {
776 439 callMxChatStream(message, function(response) {
@@ -839,15 +502,8 @@
839 502
840 503 function callMxChat(message, callback, botId) {
841 504 botId = botId || getMxChatBotId();
842 505
843 - // Streaming fallbacks land here: drop any leftover stream handle and
844 - // return the button to its send state (no-op for plain non-stream turns).
845 - if (mxchatActiveStreams[botId]) {
846 - delete mxchatActiveStreams[botId];
847 - }
848 - mxchatRestoreSendButton(botId);
849 -
850 506 // Store the message in case we need to retry after session reset
851 507 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
852 508
853 509 // Get page context if contextual awareness is enabled
@@ -852,44 +508,24 @@
852 508
853 509 // Get page context if contextual awareness is enabled
854 510 const pageContext = getPageContext();
855 511
856 - // Get instance for session start timestamp (used when persistence is OFF)
857 - var instance = MxChatInstances.get(botId);
858 -
859 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
860 - // and returns the guaranteed-present session id from the in-memory instance even when
861 - // cookie/localStorage writes are silently blocked by the browser.
862 - var sessionId = MxChatInstances.ensureSession(botId);
863 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
864 - // Last-resort generation to ensure we never POST a null marker.
865 - sessionId = generateSessionId();
866 - MxChatInstances.setChatSession(botId, sessionId);
867 - }
868 -
869 - // Wait for the page-cache nonce refresh to complete before firing the
870 - // chat-send AJAX. On cached pages the inline mxchatChat.nonce is stale
871 - // until refreshNonceIfNeeded() returns; constructing ajaxData inside the
872 - // callback guarantees we read the fresh value. See plan-c5457f.
873 - refreshNonceIfNeeded(function() {
874 512 // Prepare AJAX data
875 513 const ajaxData = {
876 514 action: 'mxchat_handle_chat_request',
877 515 message: message,
878 - session_id: sessionId,
516 + session_id: getChatSession(botId),
879 517 nonce: mxchatChat.nonce,
880 518 current_page_url: window.location.href,
881 519 current_page_title: document.title,
882 - bot_id: botId,
883 - // Pass session start timestamp so AI context matches what user sees
884 - session_start_timestamp: instance.sessionStartTimestamp || 0
520 + bot_id: botId
885 521 };
886 -
522 +
887 523 // Add page context if available
888 524 if (pageContext) {
889 525 ajaxData.page_context = JSON.stringify(pageContext);
890 526 }
891 -
527 +
892 528 // CHECK FOR VISION FLAGS AND ADD THEM
893 529 if (window.mxchatVisionProcessed) {
894 530 ajaxData.vision_processed = true;
895 531 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -898,9 +534,9 @@
898 534 window.mxchatVisionProcessed = false;
899 535 window.mxchatOriginalMessage = null;
900 536 window.mxchatVisionImagesCount = 0;
901 537 }
902 -
538 +
903 539 $.ajax({
904 540 url: mxchatChat.ajax_url,
905 541 type: 'POST',
906 542 dataType: 'json',
@@ -938,16 +574,23 @@
938 574 errorMessage = "An error occurred. Please try again or contact support.";
939 575 }
940 576
941 577 // Handle session reset action (IP changed, session expired, etc.)
942 - // Silent reset — keep chat UI intact, just get a new session and retry
943 578 if (response.data && response.data.action === 'reset_session') {
944 - MxChatInstances.silentResetSession(botId);
945 - // 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
946 584 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
947 585 if (originalMessage) {
948 586 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
949 - 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';
950 593 if (shouldUseStreaming(currentModel)) {
951 594 callMxChatStream(originalMessage, function(response) {
952 595 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
953 596 }, botId);
@@ -1004,11 +647,9 @@
1004 647 }
1005 648
1006 649 // Check for live agent response
1007 650 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
1008 - removeThinkingDots(botId);
1009 651 updateChatModeIndicator('agent', botId);
1010 - enableChatInput(botId);
1011 652 return;
1012 653 }
1013 654
1014 655 // Handle the message and show notification if chat is hidden
@@ -1041,13 +682,9 @@
1041 682 $badge.show();
1042 683 }
1043 684 }
1044 685 } else {
1045 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
1046 - if (response.vectorstore_error) {
1047 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
1048 - }
1049 - replaceLastMessage("bot", emptyMsg, '', [], botId);
686 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
1050 687 }
1051 688
1052 689 if (response.message_id) {
1053 690 var instance = MxChatInstances.get(botId);
@@ -1089,9 +726,8 @@
1089 726
1090 727 replaceLastMessage("bot", errorMessage, '', [], botId);
1091 728 }
1092 729 });
1093 - }); // refreshNonceIfNeeded
1094 730 }
1095 731
1096 732 function callMxChatStream(message, callback, botId) {
1097 733 botId = botId || getMxChatBotId();
@@ -1098,9 +734,9 @@
1098 734
1099 735 // Store the message in case we need to retry after session reset
1100 736 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
1101 737
1102 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
738 + const currentModel = mxchatChat.model || 'gpt-4o';
1103 739 if (!isStreamingSupported(currentModel)) {
1104 740 callMxChat(message, callback, botId);
1105 741 return;
1106 742 }
@@ -1107,35 +743,17 @@
1107 743
1108 744 // Get page context if contextual awareness is enabled
1109 745 const pageContext = getPageContext();
1110 746
1111 - // Get instance for session start timestamp (used when persistence is OFF)
1112 - var instance = MxChatInstances.get(botId);
1113 -
1114 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
1115 - // non-string value via String(), so passing `null` would POST the literal string "null"
1116 - // and land in the transcripts table as a ghost session. ensureSession() always returns
1117 - // a real string even when cookies/localStorage are blocked.
1118 - var streamSessionId = MxChatInstances.ensureSession(botId);
1119 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
1120 - streamSessionId = generateSessionId();
1121 - MxChatInstances.setChatSession(botId, streamSessionId);
1122 - }
1123 -
1124 - // Wait for the page-cache nonce refresh before constructing formData (which
1125 - // captures mxchatChat.nonce by value). Mirrors callMxChat's wrapping. See plan-c5457f.
1126 - refreshNonceIfNeeded(function() {
1127 747 const formData = new FormData();
1128 748 formData.append('action', 'mxchat_stream_chat');
1129 749 formData.append('message', message);
1130 - formData.append('session_id', streamSessionId);
750 + formData.append('session_id', getChatSession(botId));
1131 751 formData.append('nonce', mxchatChat.nonce);
1132 752 formData.append('current_page_url', window.location.href);
1133 753 formData.append('current_page_title', document.title);
1134 754 formData.append('bot_id', botId);
1135 - // Pass session start timestamp so AI context matches what user sees
1136 - formData.append('session_start_timestamp', instance.sessionStartTimestamp || 0);
1137 -
755 +
1138 756 // Add page context if available
1139 757 if (pageContext) {
1140 758 formData.append('page_context', JSON.stringify(pageContext));
1141 759 }
@@ -1154,20 +772,12 @@
1154 772 let accumulatedContent = '';
1155 773 let testingDataReceived = false;
1156 774 let streamingStarted = false;
1157 775
1158 - // Abortable stream: a fresh controller per turn, keyed by bot instance.
1159 - // The Stop control (send button swapped in place) aborts both the read
1160 - // loop and the underlying request.
1161 - var streamControl = { controller: new AbortController(), aborted: false };
1162 - mxchatActiveStreams[botId] = streamControl;
1163 - mxchatShowStopButton(botId);
1164 -
1165 776 fetch(mxchatChat.ajax_url, {
1166 777 method: 'POST',
1167 778 body: formData,
1168 - credentials: 'same-origin',
1169 - signal: streamControl.controller.signal
779 + credentials: 'same-origin'
1170 780 })
1171 781 .then(response => {
1172 782 // Store the response for potential fallback handling
1173 783 const responseClone = response.clone();
@@ -1233,19 +843,8 @@
1233 843 });
1234 844 return;
1235 845 }
1236 846
1237 - // Re-enable chat input when stream ends with content
1238 - enableChatInput(botId);
1239 -
1240 - // Scroll the user's last message to the top now that the
1241 - // bot's full reply has rendered (gives max reading room).
1242 - var $chatBoxDone = getElement(botId, 'chat-box');
1243 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
1244 - if ($lastUserMsgDone.length) {
1245 - scrollElementToTop($lastUserMsgDone, botId);
1246 - }
1247 -
1248 847 if (callback) {
1249 848 callback(accumulatedContent);
1250 849 }
1251 850 return;
@@ -1268,16 +867,8 @@
1268 867
1269 868 // Re-enable chat input after streaming completes
1270 869 enableChatInput(botId);
1271 870
1272 - // Scroll the user's last message to the top now
1273 - // that the bot's full reply has rendered.
1274 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1275 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1276 - if ($lastUserMsgStreamDone.length) {
1277 - scrollElementToTop($lastUserMsgStreamDone, botId);
1278 - }
1279 -
1280 871 if (callback) {
1281 872 callback(accumulatedContent);
1282 873 }
1283 874 return;
@@ -1334,9 +925,8 @@
1334 925 }
1335 926
1336 927 processStream();
1337 928 }).catch(streamError => {
1338 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1339 929 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1340 930 callMxChat(message, callback, botId);
1341 931 });
1342 932 }
@@ -1343,9 +933,8 @@
1343 933
1344 934 processStream();
1345 935 })
1346 936 .catch(error => {
1347 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1348 937 // Check if we have server error data with chat mode
1349 938 if (error && error.isServerError && error.data) {
1350 939 // Check for chat mode in error data
1351 940 if (error.data.chat_mode) {
@@ -1358,9 +947,8 @@
1358 947 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1359 948 callMxChat(message, callback, botId);
1360 949 }
1361 950 });
1362 - }); // refreshNonceIfNeeded
1363 951 }
1364 952
1365 953 // Helper function to handle non-streaming responses
1366 954 function handleNonStreamResponse(data, callback, botId) {
@@ -1399,16 +987,21 @@
1399 987 errorMessage = "An error occurred. Please try again or contact support.";
1400 988 }
1401 989
1402 990 // Handle session reset action (IP changed, session expired, etc.)
1403 - // Silent reset — keep chat UI intact, just get a new session and retry
1404 991 if (data.data && data.data.action === 'reset_session') {
1405 - MxChatInstances.silentResetSession(botId);
1406 - // 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
1407 995 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1408 996 if (originalMessage) {
1409 997 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1410 - 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';
1411 1004 if (shouldUseStreaming(currentModel)) {
1412 1005 callMxChatStream(originalMessage, callback, botId);
1413 1006 } else {
1414 1007 callMxChat(originalMessage, callback, botId);
@@ -1430,22 +1023,8 @@
1430 1023 }
1431 1024 return; // Exit early for errors
1432 1025 }
1433 1026
1434 - // Check for live agent response
1435 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1436 - removeThinkingDots(botId);
1437 - // Also remove any leftover bot-message that lost its temporary-message class
1438 - var $chatBox = getElement(botId, 'chat-box');
1439 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1440 - updateChatModeIndicator('agent', botId);
1441 - enableChatInput(botId);
1442 - if (callback) {
1443 - callback('');
1444 - }
1445 - return;
1446 - }
1447 -
1448 1027 // Handle different response formats
1449 1028 if (data.text || data.html || data.message) {
1450 1029
1451 1030 // Apply response hooks
@@ -1490,15 +1069,19 @@
1490 1069 }
1491 1070
1492 1071 // Enhanced updateChatModeIndicator function for immediate DOM updates
1493 1072 function updateChatModeIndicator(mode, botId) {
1073 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1494 1074 botId = botId || 'default';
1495 1075 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1076 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1496 1077 if (indicator) {
1497 1078 const oldText = indicator.textContent;
1079 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1498 1080
1499 1081 if (mode === 'agent') {
1500 1082 indicator.textContent = 'Live Agent';
1083 + console.log('[MxChat] Mode is agent, calling startPolling...');
1501 1084 startPolling(botId);
1502 1085 } else {
1503 1086 // Everything else is AI mode
1504 1087 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1569,21 +1152,9 @@
1569 1152 // Update the event handlers to use the correct function names (using event delegation)
1570 1153 // Use class-based selectors for multi-instance support
1571 1154 $(document).on('click', '.send-button', function() {
1572 1155 var botId = getBotIdFromElement(this);
1573 - // While a response is streaming the button is a Stop control.
1574 - if (this.classList.contains('mxchat-stop-mode')) {
1575 - // Same click that just started this stream (an add-on's direct handler
1576 - // ran before this delegated one) — not a Stop press. See
1577 - // mxchatShowStopButton for the full story (plan-4bba64).
1578 - if (this.__mxchatStopJustShown) return;
1579 - mxchatStopStreaming(botId);
1580 - return;
1581 - }
1582 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1583 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1584 - disableChatInput(botId);
1585 - }
1156 + disableChatInput(botId);
1586 1157 sendMessage(botId);
1587 1158 });
1588 1159
1589 1160 // Override enter key handler (using event delegation)
@@ -1590,378 +1161,14 @@
1590 1161 $(document).on('keypress', '.chat-input', function(e) {
1591 1162 if (e.which == 13 && !e.shiftKey) {
1592 1163 e.preventDefault();
1593 1164 var botId = getBotIdFromElement(this);
1594 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1595 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1596 - disableChatInput(botId);
1597 - }
1165 + disableChatInput(botId);
1598 1166 sendMessage(botId);
1599 1167 }
1600 1168 });
1601 1169
1602 -// Chat input character counter + soft limit feedback (plan 7091a2).
1603 -// Language-neutral: numbers + color only, no translatable strings. The counter
1604 -// reveals near the cap and ramps neutral -> amber -> red; an over-limit keystroke
1605 -// or trimmed paste produces a brief border-flash/shake so the maxlength cap (plan
1606 -// a3fae2) is never a silent "input jumps back". Per-bot scoped via .input-container.
1607 -function mxchatUpdateCharCounter(inputEl) {
1608 - if (!inputEl || !inputEl.closest) return;
1609 - var max = parseInt(inputEl.getAttribute('maxlength'), 10);
1610 - var container = inputEl.closest('.input-container');
1611 - if (!container || !max || max <= 0) return;
1612 - var counter = container.querySelector('.mxchat-char-counter');
1613 - if (!counter) return;
1614 - var len = inputEl.value.length;
1615 - var ratio = len / max;
1616 - var nearThreshold = 0.8; // start surfacing the counter at 80% of the cap
1617 - var cur = counter.querySelector('.mxchat-char-counter-current');
1618 - if (cur) cur.textContent = len;
1619 - var warn = ratio >= nearThreshold && len < max;
1620 - var full = len >= max;
1621 - counter.classList.toggle('is-visible', ratio >= nearThreshold);
1622 - counter.classList.toggle('is-warn', warn);
1623 - counter.classList.toggle('is-full', full);
1624 - container.classList.toggle('mxchat-input-near-limit', warn);
1625 - container.classList.toggle('mxchat-input-at-limit', full);
1626 -}
1627 -
1628 -function mxchatBumpInput(inputEl) {
1629 - var container = inputEl && inputEl.closest ? inputEl.closest('.input-container') : null;
1630 - if (!container) return;
1631 - container.classList.remove('mxchat-input-bump');
1632 - void container.offsetWidth; // reflow so a rapid second hit retriggers the animation
1633 - container.classList.add('mxchat-input-bump');
1634 - clearTimeout($(container).data('mxchatBumpTimeout'));
1635 - var t = setTimeout(function() { container.classList.remove('mxchat-input-bump'); }, 220);
1636 - $(container).data('mxchatBumpTimeout', t);
1637 -}
1638 -
1639 -// Live counter update on every input.
1640 -$(document).on('input', '.chat-input', function() {
1641 - mxchatUpdateCharCounter(this);
1642 -});
1643 -
1644 -// Visible "you've hit the edge" feedback when a printable keystroke is about to be
1645 -// rejected at the cap (maxlength silently swallows it otherwise).
1646 -$(document).on('keydown', '.chat-input', function(e) {
1647 - var max = parseInt(this.getAttribute('maxlength'), 10);
1648 - if (!max || max <= 0 || this.value.length < max) return;
1649 - if (e.ctrlKey || e.metaKey || e.altKey) return;
1650 - // A single printable char with no selection to overwrite WILL be rejected.
1651 - if (e.key && e.key.length === 1 && this.selectionStart === this.selectionEnd) {
1652 - mxchatBumpInput(this);
1653 - }
1654 -});
1655 -
1656 -// A paste that gets trimmed to the cap also bumps, so truncation is never silent.
1657 -$(document).on('paste', '.chat-input', function() {
1658 - var el = this;
1659 - var max = parseInt(el.getAttribute('maxlength'), 10);
1660 - if (!max || max <= 0) return;
1661 - setTimeout(function() {
1662 - mxchatUpdateCharCounter(el);
1663 - if (el.value.length >= max) mxchatBumpInput(el);
1664 - }, 0);
1665 -});
1666 -
1667 -// Builds the list of overflow-menu items for a given bot.
1668 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1669 -function mxchatGetHeaderMenuItems(botId) {
1670 - var items = [];
1671 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1672 -
1673 - // The `print_button_*` keys still gate this item for back-compat with
1674 - // existing user options. The action is now a transcript download, not print.
1675 - if (settings.print_button_enabled === 'on') {
1676 - items.push({
1677 - id: 'download-transcript',
1678 - label: settings.print_button_label || 'Download Transcript',
1679 - 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>',
1680 - action: function() {
1681 - mxchatDownloadTranscript(botId);
1682 - }
1683 - });
1684 - }
1685 -
1686 - // "Start new chat" — surfaces the EXISTING per-conversation reset
1687 - // (MxChatInstances.resetChatSession) so a visitor can start a fresh thread
1688 - // without the site owner disabling chat persistence globally. Default OFF;
1689 - // gated by the reset_chat_enabled option. plan ac2e81.
1690 - if (settings.reset_chat_enabled === 'on') {
1691 - items.push({
1692 - id: 'reset-chat',
1693 - label: settings.reset_chat_label || 'Start new chat',
1694 - icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>',
1695 - action: function() {
1696 - var confirmMsg = settings.reset_chat_confirm || 'Start a new chat? This clears the current conversation.';
1697 - if (window.confirm(confirmMsg)) {
1698 - MxChatInstances.resetChatSession(botId);
1699 - }
1700 - }
1701 - });
1702 - }
1703 -
1704 - return items;
1705 -}
1706 -
1707 -// Builds a clean markdown transcript of the current conversation and triggers
1708 -// a file download. Used by the "Download Transcript" menu item.
1709 -function mxchatDownloadTranscript(botId) {
1710 - var $chatBox = getElement(botId, 'chat-box');
1711 - if (!$chatBox || !$chatBox.length) return;
1712 -
1713 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1714 - var headerTitle = settings.print_header_title || 'Chat transcript';
1715 - var now = new Date();
1716 - var stamp = now.toLocaleString();
1717 -
1718 - var lines = [];
1719 - lines.push('# ' + headerTitle);
1720 - lines.push('');
1721 - lines.push('Exported: ' + stamp);
1722 - lines.push('');
1723 - lines.push('---');
1724 - lines.push('');
1725 -
1726 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1727 - var $msg = $(this);
1728 - // Skip thinking placeholders and any in-flight temporary messages.
1729 - if ($msg.find('.thinking-dots').length) return;
1730 - if ($msg.hasClass('temporary-message')) return;
1731 -
1732 - var sender;
1733 - if ($msg.hasClass('user-message')) sender = 'User';
1734 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1735 - else sender = 'AI Agent';
1736 -
1737 - // Strip interactive UI from the cloned message so we get the conversation text.
1738 - var $clone = $msg.clone();
1739 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1740 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1741 - if (!text) return;
1742 -
1743 - lines.push('**' + sender + '**');
1744 - lines.push('');
1745 - lines.push(text);
1746 - lines.push('');
1747 - });
1748 -
1749 - var content = lines.join('\n');
1750 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1751 - var fname = 'mxchat-transcript-' + iso + '.md';
1752 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1753 - var url = URL.createObjectURL(blob);
1754 - var a = document.createElement('a');
1755 - a.href = url;
1756 - a.download = fname;
1757 - a.style.display = 'none';
1758 - document.body.appendChild(a);
1759 - a.click();
1760 - setTimeout(function() {
1761 - if (a.parentNode) a.parentNode.removeChild(a);
1762 - URL.revokeObjectURL(url);
1763 - }, 100);
1764 -}
1765 -
1766 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1767 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1768 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1769 -function mxchatSyncMenuColors(botId, $wrap) {
1770 - if (!$wrap || !$wrap.length) return;
1771 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1772 - if (!$bot.length) return;
1773 - var cs = window.getComputedStyle($bot[0]);
1774 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1775 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1776 - }
1777 - // Bot text color usually lives on a child div, not .bot-message itself.
1778 - var $textChild = $bot.find('[style*="color"]').first();
1779 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1780 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1781 -}
1782 -
1783 -// Renders (or re-renders) the item list for one menu wrap. Split out of
1784 -// mxchatInitHeaderMenu so the dynamic-settings merge (plan-32db95) can
1785 -// rebuild items + trigger visibility WITHOUT re-binding the one-time
1786 -// open/close/keyboard wiring. closeMenu is passed in by the init closure;
1787 -// a rebuild before init (never happens, but harmless) just skips it.
1788 -function mxchatRenderHeaderMenuItems(botId, $wrap, closeMenuFn) {
1789 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1790 - var $menu = $wrap.find('.mxchat-header-menu');
1791 - var items = mxchatGetHeaderMenuItems(botId);
1792 -
1793 - $menu.empty();
1794 -
1795 - if (!items.length) {
1796 - $trigger.hide();
1797 - $menu.hide();
1798 - return;
1799 - }
1800 -
1801 - // Clear any inline display:none a previous zero-item render left behind —
1802 - // open/close visibility is governed by the hidden prop + is-open class.
1803 - $trigger.css('display', '');
1804 - $menu.css('display', '');
1805 -
1806 - items.forEach(function(item, idx) {
1807 - var $btn = $('<button>', {
1808 - type: 'button',
1809 - 'class': 'mxchat-menu-item',
1810 - 'role': 'menuitem',
1811 - 'tabindex': '-1',
1812 - 'data-menu-id': item.id,
1813 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1814 - '<span class="mxchat-menu-item-label"></span>'
1815 - });
1816 - $btn.find('.mxchat-menu-item-label').text(item.label);
1817 - $btn.on('click', function(e) {
1818 - e.preventDefault();
1819 - e.stopPropagation();
1820 - if (closeMenuFn) closeMenuFn();
1821 - try { item.action(); } catch (err) { /* no-op */ }
1822 - });
1823 - $menu.append($btn);
1824 - });
1825 -}
1826 -
1827 -// Re-render every menu on the page after a dynamic-settings merge
1828 -// (multi-bot: each wrap re-reads its items). An OPEN menu is left alone —
1829 -// swapping items under the user mid-interaction yanks focus — and the
1830 -// rebuild runs when it closes instead (closeMenu checks the pending flag).
1831 -function mxchatRebuildHeaderMenus() {
1832 - $('.mxchat-header-menu-wrap').each(function() {
1833 - var $wrap = $(this);
1834 - var botId = $wrap.data('bot-id');
1835 - if (!botId) return;
1836 - if (!$wrap.data('mxchatMenuReady')) {
1837 - mxchatInitHeaderMenu(botId);
1838 - return;
1839 - }
1840 - if ($wrap.find('.mxchat-header-menu').hasClass('is-open')) {
1841 - $wrap.data('mxchatMenuRebuildPending', true);
1842 - return;
1843 - }
1844 - mxchatRenderHeaderMenuItems(botId, $wrap, $wrap.data('mxchatMenuClose'));
1845 - });
1846 -}
1847 -
1848 -// One-time per-widget init: renders menu items, wires open/close,
1849 -// outside-click, Escape, and arrow-key navigation. If no items, hides the
1850 -// trigger. Wiring happens even when there are zero items at init, so a
1851 -// later dynamic-settings rebuild that adds items has a working trigger.
1852 -function mxchatInitHeaderMenu(botId) {
1853 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1854 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1855 -
1856 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1857 - var $menu = $wrap.find('.mxchat-header-menu');
1858 -
1859 - // Initial color sync — covers normal page load.
1860 - mxchatSyncMenuColors(botId, $wrap);
1861 -
1862 - function openMenu() {
1863 - // Re-sync each open in case the active theme changed since init.
1864 - mxchatSyncMenuColors(botId, $wrap);
1865 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1866 - $trigger.attr('aria-expanded', 'true');
1867 - // Focus the first item for keyboard users
1868 - setTimeout(function() {
1869 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1870 - }, 0);
1871 - }
1872 - function closeMenu(returnFocus) {
1873 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1874 - $trigger.attr('aria-expanded', 'false');
1875 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1876 - if (returnFocus) $trigger.trigger('focus');
1877 - // A dynamic-settings rebuild that arrived while the menu was open
1878 - // was deferred (mxchatRebuildHeaderMenus) — run it now.
1879 - if ($wrap.data('mxchatMenuRebuildPending')) {
1880 - $wrap.removeData('mxchatMenuRebuildPending');
1881 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1882 - }
1883 - }
1884 -
1885 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1886 - // click-to-collapse handler does not fire.
1887 - $trigger.on('click', function(e) {
1888 - e.preventDefault();
1889 - e.stopPropagation();
1890 - if ($menu.hasClass('is-open')) closeMenu();
1891 - else openMenu();
1892 - });
1893 -
1894 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1895 - $menu.on('click', function(e) {
1896 - e.stopPropagation();
1897 - });
1898 -
1899 - // Outside click closes the menu.
1900 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1901 - if (!$menu.hasClass('is-open')) return;
1902 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1903 - closeMenu();
1904 - });
1905 -
1906 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1907 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1908 - var $items = $menu.find('.mxchat-menu-item');
1909 - var idx = $items.index(this);
1910 - if (e.key === 'Escape') {
1911 - e.preventDefault();
1912 - closeMenu(true);
1913 - } else if (e.key === 'ArrowDown') {
1914 - e.preventDefault();
1915 - var $next = $items.eq((idx + 1) % $items.length);
1916 - $items.attr('tabindex', '-1');
1917 - $next.attr('tabindex', '0').trigger('focus');
1918 - } else if (e.key === 'ArrowUp') {
1919 - e.preventDefault();
1920 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1921 - $items.attr('tabindex', '-1');
1922 - $prev.attr('tabindex', '0').trigger('focus');
1923 - } else if (e.key === 'Enter' || e.key === ' ') {
1924 - e.preventDefault();
1925 - $(this).trigger('click');
1926 - }
1927 - });
1928 - $trigger.on('keydown', function(e) {
1929 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1930 - e.preventDefault();
1931 - closeMenu(true);
1932 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1933 - e.preventDefault();
1934 - openMenu();
1935 - }
1936 - });
1937 -
1938 - // Expose closeMenu for out-of-closure re-renders (mxchatRebuildHeaderMenus),
1939 - // then do the initial item render.
1940 - $wrap.data('mxchatMenuClose', closeMenu);
1941 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1942 -
1943 - $wrap.data('mxchatMenuReady', true);
1944 -}
1945 -
1946 -// Initialize header menus for every rendered widget on DOM ready.
1947 -$(function() {
1948 - $('.mxchat-header-menu-wrap').each(function() {
1949 - var botId = $(this).data('bot-id');
1950 - if (botId) mxchatInitHeaderMenu(botId);
1951 - });
1952 -
1953 - // Embedded (non-floating) widgets are open from the moment the page
1954 - // renders — refresh dynamic settings at init (plan-32db95). Floating
1955 - // widgets refresh on first launcher open instead.
1956 - var hasEmbeddedWidget = $('.mxchat-chatbot-wrapper').filter(function() {
1957 - return !$(this).closest('.floating-chatbot').length;
1958 - }).length > 0;
1959 - if (hasEmbeddedWidget) {
1960 - mxchatRefreshDynamicSettings();
1961 - }
1962 -});
1963 -
1170 +
1964 1171 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1965 1172 try {
1966 1173 // Determine styles based on sender type
1967 1174 let messageClass, bgColor, fontColor;
@@ -1999,12 +1206,17 @@
1999 1206 'margin-bottom': '1em'
2000 1207 });
2001 1208 }
2002 1209
2003 - // Process the message content - always run linkify to convert markdown
2004 - // links and format text. linkify() handles existing HTML safely via
2005 - // negative lookaheads that skip URLs already inside <a> tags.
2006 - 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 + }
2007 1219
2008 1220 // Add images if provided
2009 1221 if (images && images.length > 0) {
2010 1222 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -2053,12 +1265,8 @@
2053 1265 if (lastUserMessage.length) {
2054 1266 scrollElementToTop(lastUserMessage, botId);
2055 1267 }
2056 1268 }
2057 -
2058 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
2059 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2060 - }
2061 1269 });
2062 1270
2063 1271 if (messageText.id) {
2064 1272 var instance = MxChatInstances.get(botId);
@@ -2143,12 +1351,26 @@
2143 1351 bgColor = botMessageBgColor;
2144 1352 fontColor = botMessageFontColor;
2145 1353 }
2146 1354
2147 - // Always run linkify to convert markdown links and format text.
2148 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
2149 - // to avoid double-processing URLs that are already inside <a> tags).
2150 - 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 + }
2151 1373
2152 1374 if (responseHtml) {
2153 1375 // Only add line breaks if there's actual text content before the HTML
2154 1376 if (fullMessage && fullMessage.trim()) {
@@ -2205,12 +1427,8 @@
2205 1427 }
2206 1428
2207 1429 // Re-enable chat input after response is displayed
2208 1430 enableChatInput(botId);
2209 -
2210 - if (sender === "bot" || sender === "agent") {
2211 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2212 - }
2213 1431 } else {
2214 1432 appendMessage(sender, responseText, responseHtml, images, false, botId);
2215 1433 // Re-enable chat input after response is displayed
2216 1434 enableChatInput(botId);
@@ -2219,15 +1437,8 @@
2219 1437
2220 1438
2221 1439 function appendThinkingMessage(botId) {
2222 1440 botId = botId || 'default';
2223 -
2224 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
2225 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
2226 - if (indicator && indicator.textContent === 'Live Agent') {
2227 - return;
2228 - }
2229 -
2230 1441 var $chatBox = getElement(botId, 'chat-box');
2231 1442
2232 1443 // Remove any existing thinking dots in this bot's chat first
2233 1444 $chatBox.find('.thinking-dots').remove();
@@ -2249,9 +1460,9 @@
2249 1460 '</div>' +
2250 1461 '</div>';
2251 1462
2252 1463 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
2253 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1464 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
2254 1465 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
2255 1466 scrollToBottom(botId);
2256 1467 }
2257 1468
@@ -2257,11 +1468,9 @@
2257 1468
2258 1469 function removeThinkingDots(botId) {
2259 1470 botId = botId || 'default';
2260 1471 var $chatBox = getElement(botId, 'chat-box');
2261 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
2262 1472 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
2263 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
2264 1473 }
2265 1474
2266 1475 // ====================================
2267 1476 // TEXT FORMATTING & PROCESSING
@@ -2295,12 +1504,9 @@
2295 1504 processedText = formatTextStyling(processedText);
2296 1505
2297 1506 // Process code blocks BEFORE processing links
2298 1507 processedText = formatCodeBlocks(processedText);
2299 -
2300 - // Process markdown tables BEFORE converting newlines to paragraphs
2301 - processedText = formatMarkdownTables(processedText);
2302 -
1508 +
2303 1509 // NOW convert to paragraphs
2304 1510 processedText = convertNewlinesToBreaks(processedText);
2305 1511
2306 1512 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -2313,63 +1519,37 @@
2313 1519 // Return as a proper link without the brackets
2314 1520 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2315 1521 });
2316 1522
2317 - // Process markdown links: [text](url) and [](url)
2318 - // Uses balanced parenthesis matching to handle URLs containing parens
2319 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
2320 - processedText = (function(input) {
2321 - var result = '';
2322 - var i = 0;
2323 - while (i < input.length) {
2324 - // Look for [ at current position
2325 - if (input[i] === '[') {
2326 - // Find closing ]
2327 - var closeBracket = input.indexOf(']', i + 1);
2328 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
2329 - result += input[i];
2330 - i++;
2331 - continue;
2332 - }
2333 - var linkText = input.substring(i + 1, closeBracket);
2334 - // Check if URL starts with http
2335 - var urlStart = closeBracket + 2;
2336 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
2337 - result += input[i];
2338 - i++;
2339 - continue;
2340 - }
2341 - // Find balanced closing paren
2342 - var depth = 1;
2343 - var j = urlStart;
2344 - while (j < input.length && depth > 0) {
2345 - if (input[j] === '(') depth++;
2346 - else if (input[j] === ')') depth--;
2347 - if (depth > 0) j++;
2348 - }
2349 - if (depth !== 0) {
2350 - result += input[i];
2351 - i++;
2352 - continue;
2353 - }
2354 - var url = input.substring(urlStart, j);
2355 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
2356 - var encodedUrl = safeEncodeUrl(cleanUrl);
2357 - if (!linkText || !linkText.trim()) {
2358 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
2359 - } else {
2360 - var safeText = sanitizeUserInput(linkText);
2361 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
2362 - }
2363 - i = j + 1; // Skip past the closing )
2364 - } else {
2365 - result += input[i];
2366 - i++;
2367 - }
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>`;
2368 1533 }
2369 - return result;
2370 - })(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 + });
2371 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 +
2372 1552 // Process phone numbers: [text](tel:number)
2373 1553 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
2374 1554 processedText = processedText.replace(phonePattern, (match, text, phone) => {
2375 1555 const safePhone = safeEncodeUrl(phone);
@@ -2521,78 +1701,9 @@
2521 1701 });
2522 1702
2523 1703 return text;
2524 1704 }
2525 -
2526 - function formatMarkdownTables(text) {
2527 - var lines = text.split('\n');
2528 - var result = [];
2529 - var i = 0;
2530 -
2531 - while (i < lines.length) {
2532 - // Check for a table: current line has pipes AND next line is a separator row
2533 - if (i + 1 < lines.length &&
2534 - lines[i].indexOf('|') !== -1 &&
2535 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2536 -
2537 - var tableLines = [];
2538 - var headerLine = lines[i];
2539 - var separatorLine = lines[i + 1];
2540 - tableLines.push(headerLine);
2541 - tableLines.push(separatorLine);
2542 -
2543 - // Collect remaining table rows
2544 - var j = i + 2;
2545 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2546 - tableLines.push(lines[j]);
2547 - j++;
2548 - }
2549 -
2550 - // Parse alignment from separator row
2551 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2552 - var alignments = sepCells.map(function(cell) {
2553 - var trimmed = cell.trim();
2554 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2555 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2556 - return 'left';
2557 - });
2558 -
2559 - // Build HTML table
2560 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2561 -
2562 - // Header row
2563 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2564 - html += '<thead><tr>';
2565 - headerCells.forEach(function(cell, idx) {
2566 - var align = alignments[idx] || 'left';
2567 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2568 - });
2569 - html += '</tr></thead>';
2570 -
2571 - // Body rows
2572 - html += '<tbody>';
2573 - for (var r = 2; r < tableLines.length; r++) {
2574 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2575 - html += '<tr>';
2576 - rowCells.forEach(function(cell, idx) {
2577 - var align = alignments[idx] || 'left';
2578 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2579 - });
2580 - html += '</tr>';
2581 - }
2582 - html += '</tbody></table></div>';
2583 -
2584 - result.push(html);
2585 - i = j;
2586 - } else {
2587 - result.push(lines[i]);
2588 - i++;
2589 - }
2590 - }
2591 -
2592 - return result.join('\n');
2593 - }
2594 -
1705 +
2595 1706 function sanitizeUserInput(text) {
2596 1707 const div = document.createElement('div');
2597 1708 div.textContent = text;
2598 1709 return div.innerHTML;
@@ -2663,14 +1774,13 @@
2663 1774 requestAnimationFrame(smoothScroll);
2664 1775 }
2665 1776 }
2666 1777
2667 - function scrollElementToTop(element, botId, topOffset) {
1778 + function scrollElementToTop(element, botId) {
2668 1779 botId = botId || 'default';
2669 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2670 1780 var chatBox = getElement(botId, 'chat-box');
2671 1781 var elementTop = element.position().top + chatBox.scrollTop();
2672 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1782 + chatBox.animate({ scrollTop: elementTop }, 500);
2673 1783 }
2674 1784
2675 1785 function showChatWidget(botId) {
2676 1786 botId = botId || 'default';
@@ -2814,12 +1924,15 @@
2814 1924 // LIVE AGENT FUNCTIONALITY
2815 1925 // ====================================
2816 1926
2817 1927 function startPolling(botId) {
1928 + console.log('[MxChat] startPolling called for botId:', botId);
2818 1929 botId = botId || 'default';
2819 1930 var instance = MxChatInstances.get(botId);
2820 1931 // Clear any existing interval first
2821 1932 stopPolling(botId);
1933 + // Start new polling interval
1934 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2822 1935 instance.pollingInterval = setInterval(function() {
2823 1936 checkForAgentMessages(botId);
2824 1937 }, 5000);
2825 1938 }
@@ -2824,17 +1937,20 @@
2824 1937 }, 5000);
2825 1938 }
2826 1939
2827 1940 function stopPolling(botId) {
1941 + console.log('[MxChat] stopPolling called for botId:', botId);
2828 1942 botId = botId || 'default';
2829 1943 var instance = MxChatInstances.get(botId);
2830 1944 if (instance.pollingInterval) {
2831 1945 clearInterval(instance.pollingInterval);
2832 1946 instance.pollingInterval = null;
1947 + console.log('[MxChat] Polling stopped for botId:', botId);
2833 1948 }
2834 1949 }
2835 1950
2836 1951 function checkForAgentMessages(botId) {
1952 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2837 1953 botId = botId || 'default';
2838 1954 var instance = MxChatInstances.get(botId);
2839 1955 const sessionId = getChatSession(botId);
2840 1956 $.ajax({
@@ -2860,12 +1976,8 @@
2860 1976 instance.processedMessageIds.add(message.id);
2861 1977 }
2862 1978 });
2863 1979
2864 - if (hasNewMessage) {
2865 - enableChatInput(botId);
2866 - }
2867 -
2868 1980 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2869 1981 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2870 1982 showNotification(botId);
2871 1983 }
@@ -2871,13 +1983,8 @@
2871 1983 }
2872 1984
2873 1985 scrollToBottom(botId, true);
2874 1986 }
2875 -
2876 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2877 - if (response.success && response.data?.chat_mode) {
2878 - updateChatModeIndicator(response.data.chat_mode, botId);
2879 - }
2880 1987 },
2881 1988 error: function (xhr, status, error) {
2882 1989 // Polling error - silently continue
2883 1990 }
@@ -2887,29 +1994,20 @@
2887 1994 // ====================================
2888 1995 // CHAT HISTORY & PERSISTENCE
2889 1996 // ====================================
2890 1997
2891 -function loadChatHistory(botId, onComplete) {
1998 +function loadChatHistory(botId) {
2892 1999 botId = botId || 'default';
2893 2000 var instance = MxChatInstances.get(botId);
2894 2001
2895 2002 // Prevent duplicate loading
2896 2003 if (instance.chatHistoryLoaded) {
2897 - if (onComplete) onComplete();
2898 2004 return;
2899 2005 }
2900 2006
2901 - // Use getChatSession which returns null if no session exists (does NOT create one)
2902 2007 var sessionId = getChatSession(botId);
2903 2008 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2904 2009
2905 - // No session yet — nothing to load. History will load after first message via ensureSession.
2906 - if (!sessionId) {
2907 - instance.chatHistoryLoaded = true;
2908 - if (onComplete) onComplete();
2909 - return;
2910 - }
2911 -
2912 2010 if (chatPersistenceEnabled && sessionId) {
2913 2011 $.ajax({
2914 2012 url: mxchatChat.ajax_url,
2915 2013 type: 'POST',
@@ -2920,12 +2018,11 @@
2920 2018 },
2921 2019 success: function(response) {
2922 2020 // Handle session reset (IP changed while user was away)
2923 2021 if (response.success === false && response.data && response.data.action === 'reset_session') {
2924 - // Silent reset — new session but don't clear UI
2925 - MxChatInstances.silentResetSession(botId);
2022 + // Silently reset session - user will start fresh
2023 + resetChatSession(botId);
2926 2024 instance.chatHistoryLoaded = true; // Prevent retry loop
2927 - if (onComplete) onComplete();
2928 2025 return;
2929 2026 }
2930 2027
2931 2028 // Check if the response indicates success
@@ -2981,19 +2078,9 @@
2981 2078 var content = message.content;
2982 2079 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2983 2080 content = decodeHTMLEntities(content);
2984 2081
2985 - // Skip linkify for messages containing structured HTML
2986 - // (forms, product cards, galleries, etc.) to avoid
2987 - // markdown formatting corrupting HTML attributes
2988 - // (e.g. underscores in name="field_name" becoming <em> tags)
2989 - if (content.includes("mxchat-product-card") ||
2990 - content.includes("mxchat-image-gallery") ||
2991 - content.includes("mxchat-featured-products") ||
2992 - content.includes("<form") ||
2993 - content.includes("<input") ||
2994 - content.includes("<select") ||
2995 - content.includes("<textarea")) {
2082 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2996 2083 messageElement.html(content);
2997 2084 } else {
2998 2085 var formattedContent = linkify(content);
2999 2086 messageElement.html(formattedContent);
@@ -3033,17 +2120,13 @@
3033 2120 instance.chatHistoryLoaded = true;
3034 2121 }
3035 2122 }
3036 2123 }
3037 - if (onComplete) onComplete();
3038 2124 },
3039 2125 error: function(xhr, status, error) {
3040 2126 // Error loading chat history - silently continue
3041 - if (onComplete) onComplete();
3042 2127 }
3043 2128 });
3044 - } else {
3045 - if (onComplete) onComplete();
3046 2129 }
3047 2130 }
3048 2131
3049 2132
@@ -3107,10 +2190,10 @@
3107 2190 .then(data => {
3108 2191 if (data.success) {
3109 2192 container.style.display = 'none';
3110 2193 nameElement.textContent = '';
3111 - instance.activePdfFile = null;
3112 - appendMessage('bot', 'PDF removed.', '', [], false, botId);
2194 + activePdfFile = null;
2195 + appendMessage('bot', 'PDF removed.');
3113 2196 }
3114 2197 })
3115 2198 .catch(error => {
3116 2199 // Error removing PDF - silently continue
@@ -3116,16 +2199,14 @@
3116 2199 // Error removing PDF - silently continue
3117 2200 });
3118 2201 }
3119 2202
3120 - function removeActiveWord(botId) {
3121 - botId = botId || 'default';
3122 - var instance = MxChatInstances.get(botId);
3123 - const container = getElementDOM(botId, 'active-word-container');
3124 - const nameElement = getElementDOM(botId, 'active-word-name');
3125 -
3126 - if (!container || !nameElement || !instance.activeWordFile) return;
3127 -
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 +
3128 2209 fetch(mxchatChat.ajax_url, {
3129 2210 method: 'POST',
3130 2211 headers: {
3131 2212 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3131,9 +2212,9 @@
3131 2212 'Content-Type': 'application/x-www-form-urlencoded',
3132 2213 },
3133 2214 body: new URLSearchParams({
3134 2215 'action': 'mxchat_remove_word',
3135 - 'session_id': getChatSession(botId),
2216 + 'session_id': sessionId,
3136 2217 'nonce': mxchatChat.nonce
3137 2218 })
3138 2219 })
3139 2220 .then(response => response.json())
@@ -3140,10 +2221,10 @@
3140 2221 .then(data => {
3141 2222 if (data.success) {
3142 2223 container.style.display = 'none';
3143 2224 nameElement.textContent = '';
3144 - instance.activeWordFile = null;
3145 - appendMessage('bot', 'Word document removed.', '', [], false, botId);
2225 + activeWordFile = null;
2226 + appendMessage('bot', 'Word document removed.');
3146 2227 }
3147 2228 })
3148 2229 .catch(error => {
3149 2230 // Error removing Word document - silently continue
@@ -3221,35 +2302,45 @@
3221 2302 // ====================================
3222 2303
3223 2304 function checkPreChatDismissal(botId) {
3224 2305 botId = botId || 'default';
3225 - try {
3226 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
3227 - if (dismissedAt) {
3228 - // Re-show after 24 hours
3229 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
3230 - 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 {
3231 2317 getElement(botId, 'pre-chat-message').hide();
3232 - return;
3233 2318 }
3234 - // Expired — clear and show again
3235 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2319 + },
2320 + error: function() {
2321 + // Error checking pre-chat dismissal - silently continue
3236 2322 }
3237 - getElement(botId, 'pre-chat-message').fadeIn(250);
3238 - } catch (e) {
3239 - // localStorage unavailable — show the message
3240 - getElement(botId, 'pre-chat-message').fadeIn(250);
3241 - }
2323 + });
3242 2324 }
3243 2325
3244 2326 function handlePreChatDismissal(botId) {
3245 2327 botId = botId || 'default';
3246 2328 getElement(botId, 'pre-chat-message').fadeOut(200);
3247 - try {
3248 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
3249 - } catch (e) {
3250 - // localStorage unavailable — dismissal won't persist
3251 - }
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 + });
3252 2343 }
3253 2344
3254 2345
3255 2346 // ====================================
@@ -3304,14 +2395,9 @@
3304 2395 collapseQuickQuestions(botId);
3305 2396 });
3306 2397
3307 2398 // Chatbot visibility toggle handlers - use class selector for multi-instance support
3308 - // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
3309 - $(document).on('click keydown', '.floating-chatbot-button', function(e) {
3310 - if (e.type === 'keydown') {
3311 - if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
3312 - e.preventDefault();
3313 - }
2399 + $(document).on('click', '.floating-chatbot-button', function() {
3314 2400 var botId = getBotIdFromElement(this);
3315 2401 var $chatbot = getElement(botId, 'floating-chatbot');
3316 2402 var $badge = getElement(botId, 'chat-notification-badge');
3317 2403 var $preChat = getElement(botId, 'pre-chat-message');
@@ -3316,88 +2402,35 @@
3316 2402 var $badge = getElement(botId, 'chat-notification-badge');
3317 2403 var $preChat = getElement(botId, 'pre-chat-message');
3318 2404
3319 2405 if ($chatbot.hasClass('hidden')) {
3320 - $chatbot.removeClass('hidden').addClass('visible')
3321 - .attr('aria-modal', 'true').attr('role', 'dialog');
3322 - $(this).addClass('hidden').attr('aria-expanded', 'true');
2406 + $chatbot.removeClass('hidden').addClass('visible');
2407 + $(this).addClass('hidden');
3323 2408 $badge.hide(); // Hide notification when opening chat
3324 2409 disableScroll();
3325 2410 $preChat.fadeOut(250);
3326 -
3327 - // First open per page load: re-fetch behavior settings in case
3328 - // this page's inline values came from a stale full-page cache
3329 - // (plan-32db95). Idempotent — later opens are a no-op.
3330 - mxchatRefreshDynamicSettings();
3331 -
3332 - // Load chat history for returning visitors (persistence)
3333 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3334 - if (chatPersistenceEnabled) {
3335 - MxChatInstances.ensureSession(botId);
3336 - }
3337 -
3338 - // Deferred email check — only on first widget open
3339 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3340 - var instance = MxChatInstances.get(botId);
3341 - if (emailBlocker && !instance.emailCheckDone) {
3342 - instance.emailCheckDone = true;
3343 - resolveEmailState(botId);
3344 - } else if (!emailBlocker) {
3345 - // No email collection — still route through showChatContainerForBot
3346 - // so the loader is shown while chat history loads
3347 - showChatContainerForBot(botId);
3348 - }
3349 -
3350 - // Move keyboard focus into the message input after the open transition.
3351 - setTimeout(function() {
3352 - var chatInput = getElementDOM(botId, 'chat-input');
3353 - if (chatInput && !chatInput.disabled) {
3354 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
3355 - }
3356 - }, 300);
3357 2411 } else {
3358 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3359 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2412 + $chatbot.removeClass('visible').addClass('hidden');
2413 + $(this).removeClass('hidden');
3360 2414 enableScroll();
3361 2415 checkPreChatDismissal(botId);
3362 2416 }
3363 2417 });
3364 2418
3365 - // Allow clicking anywhere on the title bar to close the chatbot.
3366 - // Returns keyboard focus to the launcher so keyboard users don't get
3367 - // stranded at <body> (WCAG SC 2.4.3 Focus Order). :focus-visible is
3368 - // heuristic-based so mouse-triggered close won't show a focus ring.
2419 + // Allow clicking anywhere on the title bar to close the chatbot
3369 2420 $(document).on('click', '.chatbot-top-bar', function() {
3370 2421 var botId = getBotIdFromElement(this);
3371 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3372 - var $launcher = getElement(botId, 'floating-chatbot-button');
3373 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
2422 + getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2423 + getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3374 2424 enableScroll();
3375 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3376 2425 });
3377 2426
3378 - // Global Escape-key handler — closes any visible chat widget and
3379 - // returns focus to its launcher. Standard modal-dismissal pattern;
3380 - // pairs with aria-modal="true" set on the widget when it opens.
3381 - $(document).on('keydown', function(e) {
3382 - if (e.key !== 'Escape' && e.key !== 'Esc') return;
3383 - var $visible = $('.floating-chatbot.visible');
3384 - if (!$visible.length) return;
3385 - e.preventDefault();
3386 - $visible.each(function() {
3387 - var botId = getBotIdFromElement(this);
3388 - $(this).addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3389 - var $launcher = getElement(botId, 'floating-chatbot-button');
3390 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
3391 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3392 - });
3393 - enableScroll();
3394 - });
3395 -
3396 2427 $(document).on('click', '.close-pre-chat-message', function(e) {
3397 2428 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3398 2429 var botId = getBotIdFromElement(this);
3399 - handlePreChatDismissal(botId);
2430 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2431 + $(this).remove();
2432 + });
3400 2433 });
3401 2434
3402 2435
3403 2436 // PDF upload button handlers - use class selector
@@ -3413,20 +2446,17 @@
3413 2446 var wordInput = getElementDOM(botId, 'word-upload');
3414 2447 if (wordInput) wordInput.click();
3415 2448 });
3416 2449
3417 - // PDF file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'pdf-upload')
3418 - $(document).on('change', '.pdf-upload', async function(e) {
3419 - var botId = getBotIdFromElement(this);
3420 - var instance = MxChatInstances.get(botId);
3421 - const file = this.files[0];
3422 - const sessionId = MxChatInstances.ensureSession(botId);
3423 -
2450 + // PDF file input change handler
2451 + addSafeEventListener('pdf-upload', 'change', async function(e) {
2452 + const file = e.target.files[0];
2453 +
3424 2454 if (!file || file.type !== 'application/pdf') {
3425 2455 alert('Please select a valid PDF file.');
3426 2456 return;
3427 2457 }
3428 -
2458 +
3429 2459 if (!sessionId) {
3430 2460 alert('Error: No session ID found');
3431 2461 return;
3432 2462 }
@@ -3434,49 +2464,47 @@
3434 2464 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3435 2465 alert('Error: Ajax configuration missing');
3436 2466 return;
3437 2467 }
3438 -
2468 +
3439 2469 // Disable buttons and show loading state
3440 - const uploadBtn = getElementDOM(botId, 'pdf-upload-btn');
3441 - const sendBtn = getElementDOM(botId, 'send-button');
3442 - if (!uploadBtn) return;
2470 + const uploadBtn = document.getElementById('pdf-upload-btn');
2471 + const sendBtn = document.getElementById('send-button');
3443 2472 const originalBtnContent = uploadBtn.innerHTML;
3444 -
2473 +
3445 2474 try {
3446 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3447 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3448 2475 const formData = new FormData();
3449 2476 formData.append('action', 'mxchat_upload_pdf');
3450 2477 formData.append('pdf_file', file);
3451 2478 formData.append('session_id', sessionId);
3452 2479 formData.append('nonce', mxchatChat.nonce);
3453 -
2480 +
3454 2481 uploadBtn.disabled = true;
3455 - if (sendBtn) sendBtn.disabled = true;
2482 + sendBtn.disabled = true;
3456 2483 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3457 2484 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3458 2485 </svg>`;
3459 -
2486 +
3460 2487 const response = await fetch(mxchatChat.ajax_url, {
3461 2488 method: 'POST',
3462 2489 body: formData
3463 2490 });
3464 -
2491 +
3465 2492 const data = await response.json();
3466 -
2493 +
3467 2494 if (data.success) {
3468 2495 // Hide popular questions if they exist
3469 - if (hasQuickQuestions(botId)) {
3470 - collapseQuickQuestions(botId);
2496 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2497 + if (hasQuickQuestions()) {
2498 + collapseQuickQuestions();
3471 2499 }
3472 -
2500 +
3473 2501 // Show the active PDF name
3474 - showActivePdf(data.data.filename, botId);
3475 -
3476 - appendMessage('bot', data.data.message, '', [], false, botId);
3477 - scrollToBottom(botId);
3478 - instance.activePdfFile = data.data.filename;
2502 + showActivePdf(data.data.filename);
2503 +
2504 + appendMessage('bot', data.data.message);
2505 + scrollToBottom();
2506 + activePdfFile = data.data.filename;
3479 2507 } else {
3480 2508 alert('Failed to upload PDF. Please try again.');
3481 2509 }
3482 2510 } catch (error) {
@@ -3482,76 +2510,66 @@
3482 2510 } catch (error) {
3483 2511 alert('Error uploading file. Please try again.');
3484 2512 } finally {
3485 2513 uploadBtn.disabled = false;
3486 - if (sendBtn) sendBtn.disabled = false;
2514 + sendBtn.disabled = false;
3487 2515 uploadBtn.innerHTML = originalBtnContent;
3488 2516 this.value = ''; // Reset file input
3489 2517 }
3490 2518 });
3491 2519
3492 - // Word file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'word-upload')
3493 - $(document).on('change', '.word-upload', async function(e) {
3494 - var botId = getBotIdFromElement(this);
3495 - var instance = MxChatInstances.get(botId);
3496 - const file = this.files[0];
3497 - const sessionId = MxChatInstances.ensureSession(botId);
3498 -
2520 + // Word file input change handler
2521 + addSafeEventListener('word-upload', 'change', async function(e) {
2522 + const file = e.target.files[0];
2523 +
3499 2524 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
3500 2525 alert('Please select a valid Word document (.docx).');
3501 2526 return;
3502 2527 }
3503 -
2528 +
3504 2529 if (!sessionId) {
3505 2530 alert('Error: No session ID found');
3506 2531 return;
3507 2532 }
3508 2533
3509 - if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3510 - alert('Error: Ajax configuration missing');
3511 - return;
3512 - }
3513 -
3514 2534 // Disable buttons and show loading state
3515 - const uploadBtn = getElementDOM(botId, 'word-upload-btn');
3516 - const sendBtn = getElementDOM(botId, 'send-button');
3517 - if (!uploadBtn) return;
2535 + const uploadBtn = document.getElementById('word-upload-btn');
2536 + const sendBtn = document.getElementById('send-button');
3518 2537 const originalBtnContent = uploadBtn.innerHTML;
3519 -
2538 +
3520 2539 try {
3521 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3522 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3523 2540 const formData = new FormData();
3524 2541 formData.append('action', 'mxchat_upload_word');
3525 2542 formData.append('word_file', file);
3526 2543 formData.append('session_id', sessionId);
3527 2544 formData.append('nonce', mxchatChat.nonce);
3528 -
2545 +
3529 2546 uploadBtn.disabled = true;
3530 - if (sendBtn) sendBtn.disabled = true;
2547 + sendBtn.disabled = true;
3531 2548 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3532 2549 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3533 2550 </svg>`;
3534 -
2551 +
3535 2552 const response = await fetch(mxchatChat.ajax_url, {
3536 2553 method: 'POST',
3537 2554 body: formData
3538 2555 });
3539 -
2556 +
3540 2557 const data = await response.json();
3541 -
2558 +
3542 2559 if (data.success) {
3543 2560 // Hide popular questions if they exist
3544 - if (hasQuickQuestions(botId)) {
3545 - collapseQuickQuestions(botId);
2561 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2562 + if (hasQuickQuestions()) {
2563 + collapseQuickQuestions();
3546 2564 }
3547 -
2565 +
3548 2566 // Show the active Word document name
3549 - showActiveWord(data.data.filename, botId);
3550 -
3551 - appendMessage('bot', data.data.message, '', [], false, botId);
3552 - scrollToBottom(botId);
3553 - instance.activeWordFile = data.data.filename;
2567 + showActiveWord(data.data.filename);
2568 +
2569 + appendMessage('bot', data.data.message);
2570 + scrollToBottom();
2571 + activeWordFile = data.data.filename;
3554 2572 } else {
3555 2573 alert('Failed to upload Word document. Please try again.');
3556 2574 }
3557 2575 } catch (error) {
@@ -3557,25 +2575,25 @@
3557 2575 } catch (error) {
3558 2576 alert('Error uploading file. Please try again.');
3559 2577 } finally {
3560 2578 uploadBtn.disabled = false;
3561 - if (sendBtn) sendBtn.disabled = false;
2579 + sendBtn.disabled = false;
3562 2580 uploadBtn.innerHTML = originalBtnContent;
3563 2581 this.value = ''; // Reset file input
3564 2582 }
3565 2583 });
3566 2584
3567 - // Remove button click handlers - delegated, bot-aware (were bound to stale un-suffixed ids)
3568 - $(document).on('click', '.remove-pdf-btn', function(e) {
2585 + // Remove button click handlers
2586 + document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
3569 2587 e.preventDefault();
3570 2588 e.stopPropagation();
3571 - removeActivePdf(getBotIdFromElement(this));
2589 + removeActivePdf();
3572 2590 });
3573 -
3574 - $(document).on('click', '.remove-word-btn', function(e) {
2591 +
2592 + document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
3575 2593 e.preventDefault();
3576 2594 e.stopPropagation();
3577 - removeActiveWord(getBotIdFromElement(this));
2595 + removeActiveWord();
3578 2596 });
3579 2597
3580 2598 // Window resize handlers
3581 2599 $(window).on('resize orientationchange', function() {
@@ -3613,437 +2631,380 @@
3613 2631 });
3614 2632
3615 2633
3616 2634 // ====================================
3617 -// INIT LOADER & CHAT CONTAINER HELPERS
2635 +// EMAIL COLLECTION SETUP - FIXED VERSION
3618 2636 // ====================================
3619 -// These must be outside the email collection block so they're always available
3620 -// (used by persistence loading even when email collection is off)
3621 -
3622 -function showInitLoader(botId) {
3623 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3624 - if (loader) loader.style.display = 'flex';
3625 -}
3626 -
3627 -function hideInitLoader(botId) {
3628 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3629 - if (loader) loader.style.display = 'none';
3630 -}
3631 -
3632 -function showEmailFormForBot(botId) {
3633 - hideInitLoader(botId);
3634 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3635 - var chatContainer = getElementDOM(botId, 'chat-container');
3636 - if (emailBlocker) emailBlocker.style.display = 'flex';
3637 - if (chatContainer) chatContainer.style.display = 'none';
3638 -}
3639 -
3640 -function showChatContainerForBot(botId) {
3641 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3642 - var chatContainer = getElementDOM(botId, 'chat-container');
3643 - if (emailBlocker) emailBlocker.style.display = 'none';
3644 -
3645 - var instance = MxChatInstances.get(botId);
3646 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3647 -
3648 - // If persistence is on and history hasn't loaded yet, show loader
3649 - // while history loads to prevent flash of empty chat
3650 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3651 - if (chatContainer) chatContainer.style.display = 'none';
3652 - showInitLoader(botId);
3653 - loadChatHistory(botId, function() {
3654 - hideInitLoader(botId);
3655 - if (chatContainer) chatContainer.style.display = 'flex';
3656 - scrollToBottom(botId, true);
3657 - });
3658 - } else {
3659 - hideInitLoader(botId);
3660 - if (chatContainer) chatContainer.style.display = 'flex';
3661 - if (typeof loadChatHistory === 'function') {
3662 - loadChatHistory(botId);
3663 - }
3664 - }
3665 -}
3666 -
3667 -// ====================================
3668 -// EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3669 -// ====================================
3670 2637 // Only run email collection setup if it's enabled
3671 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');
3672 2643
3673 - // Track submitting state per bot
3674 - 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 + }
3675 2654
3676 - // Add CSS animations for email form (once globally)
3677 - if (!document.getElementById('email-error-styles')) {
3678 - const style = document.createElement('style');
3679 - style.id = 'email-error-styles';
3680 - style.textContent = `
3681 - @keyframes fadeInError {
3682 - from { opacity: 0; transform: translateY(-5px); }
3683 - 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();
3684 2663 }
3685 - .email-input-shake {
3686 - animation: shake 0.5s ease-in-out;
3687 - }
3688 - @keyframes shake {
3689 - 0%, 100% { transform: translateX(0); }
3690 - 25% { transform: translateX(-5px); }
3691 - 75% { transform: translateX(5px); }
3692 - }
3693 - @keyframes spin {
3694 - from { transform: rotate(0deg); }
3695 - to { transform: rotate(360deg); }
3696 - }
3697 - .email-spinner {
3698 - display: inline-block;
3699 - vertical-align: middle;
3700 - }
3701 - `;
3702 - document.head.appendChild(style);
3703 - }
2664 + }
3704 2665
3705 - function isValidEmailAddress(email) {
3706 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3707 - return emailRegex.test(email.trim()) && email.length <= 254;
3708 - }
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 + }
3709 2671
3710 - function isValidNameInput(name) {
3711 - return name && name.trim().length >= 2 && name.trim().length <= 100;
3712 - }
2672 + // Enhanced name validation
2673 + function isValidName(name) {
2674 + return name && name.trim().length >= 2 && name.trim().length <= 100;
2675 + }
3713 2676
3714 - /**
3715 - * Replace {visitor_name} placeholder in intro message with actual visitor name
3716 - * @param {string} botId - The bot instance ID
3717 - * @param {string} visitorName - The visitor's name to insert
3718 - */
3719 - function replaceVisitorNamePlaceholder(botId, visitorName) {
3720 - var chatBox = getElementDOM(botId, 'chat-box');
3721 - if (!chatBox) return;
3722 -
3723 - // Find the first bot message (intro message)
3724 - var introMessage = chatBox.querySelector('.bot-message');
3725 - if (!introMessage) return;
3726 -
3727 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3728 - if (!messageContent) return;
3729 -
3730 - var html = messageContent.innerHTML;
3731 -
3732 - // Replace {visitor_name} placeholder (case-insensitive)
3733 - if (visitorName && visitorName.trim()) {
3734 - // Escape HTML to prevent XSS
3735 - var safeName = $('<div>').text(visitorName.trim()).html();
3736 - html = html.replace(/\{visitor_name\}/gi, safeName);
3737 - } else {
3738 - // Remove placeholder and clean up spacing if no name provided
3739 - html = html.replace(/\{visitor_name\}/gi, '');
3740 - // Clean up any double spaces that might result
3741 - 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 + }
3742 2722 }
3743 2723
3744 - messageContent.innerHTML = html;
3745 - }
3746 -
3747 - function setEmailSubmissionState(botId, loading) {
3748 - var submitButton = getElementDOM(botId, 'email-submit-button');
3749 - var emailInput = getElementDOM(botId, 'user-email');
3750 - var nameInput = getElementDOM(botId, 'user-name');
3751 -
3752 - if (loading) {
3753 - emailSubmittingState[botId] = true;
3754 - if (submitButton) submitButton.disabled = true;
3755 - if (emailInput) emailInput.disabled = true;
3756 - if (nameInput) nameInput.disabled = true;
3757 -
3758 - if (submitButton && !submitButton.getAttribute('data-original-html')) {
3759 - submitButton.setAttribute('data-original-html', submitButton.innerHTML);
3760 - const originalText = submitButton.textContent;
3761 - submitButton.innerHTML = `
3762 - <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
3763 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
3764 - <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
3765 - <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
3766 - </circle>
3767 - </svg>
3768 - ${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 + }
3769 2764 `;
3770 - submitButton.style.opacity = '0.8';
2765 + document.head.appendChild(style);
3771 2766 }
3772 - } else {
3773 - emailSubmittingState[botId] = false;
3774 - if (submitButton) submitButton.disabled = false;
3775 - if (emailInput) emailInput.disabled = false;
3776 - if (nameInput) nameInput.disabled = false;
3777 -
3778 - if (submitButton) {
3779 - const originalHtml = submitButton.getAttribute('data-original-html');
3780 - if (originalHtml) {
3781 - submitButton.innerHTML = originalHtml;
3782 - }
3783 - 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);
3784 2779 }
2780 +
2781 + if (nameInput) {
2782 + nameInput.classList.add('email-input-shake');
2783 + setTimeout(() => {
2784 + nameInput.classList.remove('email-input-shake');
2785 + }, 500);
2786 + }
3785 2787 }
3786 - }
3787 2788
3788 - function showEmailError(botId, message) {
3789 - clearEmailError(botId);
3790 -
3791 - var emailForm = getElementDOM(botId, 'email-collection-form');
3792 - if (!emailForm) return;
3793 -
3794 - const errorDiv = document.createElement('div');
3795 - errorDiv.className = 'email-error';
3796 - errorDiv.style.cssText = `
3797 - color: #e74c3c;
3798 - font-size: 12px;
3799 - margin-top: 8px;
3800 - padding: 4px 0;
3801 - animation: fadeInError 0.3s ease;
3802 - `;
3803 - errorDiv.textContent = message;
3804 - emailForm.appendChild(errorDiv);
3805 -
3806 - // Add shake animation to inputs
3807 - var emailInput = getElementDOM(botId, 'user-email');
3808 - var nameInput = getElementDOM(botId, 'user-name');
3809 -
3810 - if (emailInput) {
3811 - emailInput.classList.add('email-input-shake');
3812 - setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
3813 - }
3814 - if (nameInput) {
3815 - nameInput.classList.add('email-input-shake');
3816 - setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
3817 - }
3818 - }
3819 -
3820 - function clearEmailError(botId) {
3821 - var emailForm = getElementDOM(botId, 'email-collection-form');
3822 - if (emailForm) {
2789 + function clearEmailError() {
3823 2790 const existingErrors = emailForm.querySelectorAll('.email-error');
3824 2791 existingErrors.forEach(error => error.remove());
3825 2792 }
3826 - }
3827 2793
3828 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3829 - function resolveEmailState(botId) {
3830 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3831 - if (mxchatChat.initial_email_state.show_email_form) {
3832 - showEmailFormForBot(botId);
3833 - } else {
3834 - showChatContainerForBot(botId);
3835 - }
3836 - } else {
3837 - checkSessionAndEmailForBot(botId);
3838 - }
3839 - }
2794 + // MAIN FORM SUBMIT HANDLER
2795 + // Remove any existing event listeners first
2796 + emailForm.removeEventListener('submit', handleFormSubmit);
3840 2797
3841 - function checkSessionAndEmailForBot(botId) {
3842 - const sessionId = MxChatInstances.ensureSession(botId);
2798 + // Add the form submit handler
2799 + emailForm.addEventListener('submit', handleFormSubmit);
3843 2800
3844 - // Hide both panels while we check — show loader instead
3845 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3846 - var chatContainer = getElementDOM(botId, 'chat-container');
3847 - if (emailBlocker) emailBlocker.style.display = 'none';
3848 - if (chatContainer) chatContainer.style.display = 'none';
3849 - showInitLoader(botId);
2801 + function handleFormSubmit(event) {
2802 + event.preventDefault();
2803 + event.stopPropagation();
3850 2804
3851 - fetch(mxchatChat.ajax_url, {
3852 - method: 'POST',
3853 - headers: {
3854 - 'Content-Type': 'application/x-www-form-urlencoded',
3855 - },
3856 - body: new URLSearchParams({
3857 - action: 'mxchat_check_email_provided',
3858 - session_id: sessionId,
3859 - nonce: mxchatChat.nonce,
3860 - })
3861 - })
3862 - .then((response) => {
3863 - if (!response.ok) {
3864 - throw new Error(`HTTP error! status: ${response.status}`);
2805 + // Prevent double submission
2806 + if (isSubmitting) {
2807 + return false;
3865 2808 }
3866 - return response.json();
3867 - })
3868 - .then((data) => {
3869 - if (data.success) {
3870 - if (data.data.logged_in || data.data.email) {
3871 - showChatContainerForBot(botId);
3872 - } else {
3873 - showEmailFormForBot(botId);
3874 - }
3875 - } else {
3876 - showEmailFormForBot(botId);
3877 - }
3878 - })
3879 - .catch((error) => {
3880 - showEmailFormForBot(botId);
3881 - });
3882 - }
3883 2809
3884 - // Event delegation for email form submission
3885 - $(document).on('submit', '.email-collection-form', function(e) {
3886 - e.preventDefault();
3887 - 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();
3888 2814
3889 - var botId = getBotIdFromElement(this);
2815 + // Validate email before submission
2816 + if (!userEmail) {
2817 + showEmailError('Please enter your email address.');
2818 + return false;
2819 + }
3890 2820
3891 - // Prevent double submission
3892 - if (emailSubmittingState[botId]) {
3893 - return false;
3894 - }
2821 + if (!isValidEmail(userEmail)) {
2822 + showEmailError('Please enter a valid email address.');
2823 + return false;
2824 + }
3895 2825
3896 - var emailInput = getElementDOM(botId, 'user-email');
3897 - var nameInput = getElementDOM(botId, 'user-name');
3898 - var userEmail = emailInput ? emailInput.value.trim() : '';
3899 - var userName = nameInput ? nameInput.value.trim() : '';
3900 - 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 + }
3901 2831
3902 - // Validate email
3903 - if (!userEmail) {
3904 - showEmailError(botId, 'Please enter your email address.');
3905 - return false;
3906 - }
2832 + // Clear any existing errors
2833 + clearEmailError();
2834 + setSubmissionState(true);
3907 2835
3908 - if (!isValidEmailAddress(userEmail)) {
3909 - showEmailError(botId, 'Please enter a valid email address.');
3910 - return false;
3911 - }
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 + });
3912 2843
3913 - // Validate name if field exists and has content
3914 - if (nameInput && userName && !isValidNameInput(userName)) {
3915 - showEmailError(botId, 'Please enter a valid name (2-100 characters).');
3916 - return false;
3917 - }
2844 + // Add name to form data if provided
2845 + if (userName) {
2846 + formData.append('name', userName);
2847 + }
3918 2848
3919 - clearEmailError(botId);
3920 - 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);
3921 2864
3922 - // Prepare form data
3923 - const formData = new URLSearchParams({
3924 - action: 'mxchat_handle_save_email_and_response',
3925 - email: userEmail,
3926 - session_id: sessionId,
3927 - nonce: mxchatChat.nonce,
3928 - });
2865 + if (data.success) {
2866 + // Show chat immediately
2867 + showChatContainer();
3929 2868
3930 - if (userName) {
3931 - 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
3932 2888 }
3933 2889
3934 - fetch(mxchatChat.ajax_url, {
3935 - method: 'POST',
3936 - headers: {
3937 - 'Content-Type': 'application/x-www-form-urlencoded',
3938 - },
3939 - body: formData
3940 - })
3941 - .then((response) => {
3942 - if (!response.ok) {
3943 - throw new Error(`HTTP error! status: ${response.status}`);
3944 - }
3945 - return response.json();
3946 - })
3947 - .then((data) => {
3948 - 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 + });
3949 2911
3950 - if (data.success) {
3951 - 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 + }
3952 2920
3953 - // Replace {visitor_name} placeholder in intro message with actual name
3954 - if (userName) {
3955 - replaceVisitorNamePlaceholder(botId, userName);
3956 - } else {
3957 - // Remove placeholder if no name provided
3958 - 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);
3959 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 + });
3960 2942
3961 - if (data.message && typeof appendMessage === 'function') {
3962 - setTimeout(() => {
3963 - appendMessage('bot', data.message, '', [], false, botId);
3964 - if (typeof scrollToBottom === 'function') {
3965 - scrollToBottom(botId);
3966 - }
3967 - }, 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'));
3968 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();
3969 2957 } else {
3970 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
2958 + showChatContainer();
3971 2959 }
3972 - })
3973 - .catch((error) => {
3974 - setEmailSubmissionState(botId, false);
3975 - showEmailError(botId, 'An error occurred. Please try again.');
3976 - });
3977 -
3978 - return false;
3979 - });
3980 -
3981 - // Real-time email validation using event delegation
3982 - $(document).on('input', '.mxchat-email-input', function() {
3983 - var botId = getBotIdFromElement(this);
3984 - var $input = $(this);
3985 -
3986 - // Clear previous timeout
3987 - clearTimeout($input.data('validationTimeout'));
3988 -
3989 - // Debounce validation
3990 - var timeout = setTimeout(() => {
3991 - var email = this.value.trim();
3992 - clearEmailError(botId);
3993 -
3994 - if (email && !isValidEmailAddress(email)) {
3995 - showEmailError(botId, 'Please enter a valid email address.');
3996 - }
3997 - }, 500);
3998 -
3999 - $input.data('validationTimeout', timeout);
4000 - });
4001 -
4002 - // Handle Enter key in email input
4003 - $(document).on('keypress', '.mxchat-email-input', function(e) {
4004 - if (e.key === 'Enter') {
4005 - e.preventDefault();
4006 - var botId = getBotIdFromElement(this);
4007 - if (!emailSubmittingState[botId]) {
4008 - $(this).closest('.email-collection-form').submit();
4009 - }
2960 + } else {
2961 + // Check email status via AJAX
2962 + setTimeout(checkSessionAndEmail, 100);
4010 2963 }
4011 - });
4012 2964
4013 - // Handle Enter key in name input
4014 - $(document).on('keypress', '.mxchat-name-input', function(e) {
4015 - if (e.key === 'Enter') {
4016 - e.preventDefault();
4017 - var botId = getBotIdFromElement(this);
4018 - if (!emailSubmittingState[botId]) {
4019 - $(this).closest('.email-collection-form').submit();
4020 - }
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 + });
4021 3002 }
4022 - });
4023 3003
4024 - // Initialize email check for all bot instances
4025 - // For floating bots: defer until widget is opened (zero passive AJAX)
4026 - // For embedded bots: check immediately since the form is visible
4027 - $('.mxchat-chatbot-wrapper').each(function() {
4028 - var botId = $(this).data('bot-id') || 'default';
4029 - var emailBlocker = getElementDOM(botId, 'email-blocker');
4030 -
4031 - if (emailBlocker) {
4032 - if (isEmbeddedBot(botId)) {
4033 - // Embedded bots are always visible — check now
4034 - resolveEmailState(botId);
4035 - }
4036 - // Floating bots: handled in the widget open handler
4037 - } else if (isEmbeddedBot(botId)) {
4038 - // Embedded bot, no email collection — load history with loader
4039 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
4040 - if (chatPersistenceEnabled) {
4041 - MxChatInstances.ensureSession(botId);
4042 - showChatContainerForBot(botId);
4043 - }
4044 - }
4045 - });
3004 + } else {
3005 + // Email collection is enabled but essential elements are missing - silently continue
3006 + }
4046 3007 }
4047 3008
4048 3009 // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
4049 3010 $(document).on('click', '.pre-chat-message', function() {
@@ -4051,32 +3012,39 @@
4051 3012 var $chatbot = getElement(botId, 'floating-chatbot');
4052 3013 if ($chatbot.hasClass('hidden')) {
4053 3014 $chatbot.removeClass('hidden').addClass('visible');
4054 3015 getElement(botId, 'floating-chatbot-button').addClass('hidden');
4055 - handlePreChatDismissal(botId);
3016 + $(this).fadeOut(250); // Hide pre-chat message
4056 3017 disableScroll(); // Disable scroll when chatbot opens
3018 + }
3019 + });
4057 3020
4058 - // Load chat history for returning visitors (persistence)
4059 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
4060 - if (chatPersistenceEnabled) {
4061 - MxChatInstances.ensureSession(botId);
4062 - }
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
4063 3027
4064 - // Deferred email check — only on first widget open
4065 - var emailBlocker = getElementDOM(botId, 'email-blocker');
4066 - var instance = MxChatInstances.get(botId);
4067 - if (emailBlocker && !instance.emailCheckDone) {
4068 - instance.emailCheckDone = true;
4069 - resolveEmailState(botId);
4070 - } else if (!emailBlocker) {
4071 - 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
4072 3042 }
4073 - }
3043 + });
4074 3044 });
4075 3045
4076 - // Legacy duplicate close handler removed — handled by single event delegation above
4077 3046
4078 -
4079 3047 function hasQuickQuestions(botId) {
4080 3048 botId = botId || 'default';
4081 3049 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
4082 3050 if (!questionsContainer) return false;
@@ -4212,11 +3180,18 @@
4212 3180 });
4213 3181
4214 3182 // Initialize when document is ready
4215 3183 setFullHeight();
3184 + trackOriginatingPage();
4216 3185
4217 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
4218 - // 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 + }
4219 3194
4220 3195 // Initialize chat visibility for all instances
4221 3196 $('.mxchat-chatbot-wrapper').each(function() {
4222 3197 var botId = $(this).data('bot-id') || 'default';
@@ -4269,312 +3244,6 @@
4269 3244 }, 2000);
4270 3245 });
4271 3246 }
4272 3247 }
4273 -});
4274 -
4275 -// ============================================================================
4276 -// SATISFACTION RATING (v3.2.6)
4277 -// ============================================================================
4278 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
4279 -// inactivity following a bot reply. One prompt per session, deduped via
4280 -// localStorage. Runs ONLY when the satisfaction_rating_enabled option is on —
4281 -// the option (default off) is authoritative.
4282 -jQuery(function($) {
4283 - if (typeof mxchatChat === 'undefined') return;
4284 - // wp_localize_script stringifies scalars: a PHP boolean false arrives as
4285 - // '' and true as '1', so this must be an explicit-enable allowlist — the
4286 - // old "disabled when exactly false/'off'" check let '' through and the
4287 - // bubble rendered on sites with the option off/unset (plan-4bba64). PHP
4288 - // now emits 'on'/'off' strings; true/'1'/1 keep cached pre-fix HTML
4289 - // (boolean-true localizations) working.
4290 - // NOTE (plan-32db95): this gate reads the INLINE value at DOM ready and is
4291 - // deliberately NOT re-evaluated after the widget's dynamic-settings refresh
4292 - // merges fresh values over mxchatChat (that merge fires on first widget
4293 - // open, after this module has already decided). Re-evaluating would mean
4294 - // restructuring the whole module to late-bind its listeners — not worth it
4295 - // for a prompt that is at worst stale for one page load on a cached page.
4296 - var sre = mxchatChat.satisfaction_rating_enabled;
4297 - if (sre !== 'on' && sre !== true && sre !== '1' && sre !== 1) return;
4298 -
4299 - // wp_localize_script stringifies ints, so accept both number and numeric string.
4300 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
4301 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
4302 - if (!isFinite(idleSeconds)) idleSeconds = 60;
4303 - if (idleSeconds < 5) idleSeconds = 5;
4304 - if (idleSeconds > 600) idleSeconds = 600;
4305 - var IDLE_MS = idleSeconds * 1000;
4306 - var MIN_BOT_REPLIES = 2;
4307 - var ratingState = {};
4308 -
4309 - function getState(botId) {
4310 - if (!ratingState[botId]) {
4311 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
4312 - }
4313 - return ratingState[botId];
4314 - }
4315 -
4316 - function getSessionId(botId) {
4317 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
4318 - return MxChatInstances.getChatSession(botId);
4319 - }
4320 - return null;
4321 - }
4322 -
4323 - function isAlreadyRated(sessionId) {
4324 - if (!sessionId) return false;
4325 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
4326 - }
4327 -
4328 - function markRated(sessionId) {
4329 - if (!sessionId) return;
4330 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
4331 - }
4332 -
4333 - function esc(s) {
4334 - return String(s == null ? '' : s)
4335 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
4336 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
4337 - }
4338 -
4339 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
4340 - function ratingSkipInlineColors(botId) {
4341 - if (mxchatChat.skip_inline_colors) return true;
4342 - var botAssignments = mxchatChat.bot_theme_assignments || {};
4343 - return botAssignments.hasOwnProperty(botId);
4344 - }
4345 -
4346 - function botBubbleStyleAttr(botId) {
4347 - if (ratingSkipInlineColors(botId)) return '';
4348 - var bg = mxchatChat.bot_message_bg_color;
4349 - var fg = mxchatChat.bot_message_font_color;
4350 - if (!bg && !fg) return '';
4351 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
4352 - }
4353 -
4354 - // Reads the rating bubble's actual computed fg+bg (whatever paints it —
4355 - // the inline color pickers OR the mxchat-theme AI customizer's injected CSS)
4356 - // and paints the filled "Send" pill so it fills with the bot font color and
4357 - // labels in the bubble bg. Mirrors mxchatSyncMenuColors(~:1512) for the read.
4358 - // We paint the submit button DIRECTLY (inline longhand) rather than relying
4359 - // on the CSS rule's var()s: Chromium resolves an INHERITED custom property
4360 - // unreliably inside a descendant's `background`, so a bubble-level var would
4361 - // silently fall back to the literal (white-block bug all over again). Inline
4362 - // longhand always wins. Same transparent-guard as the menu so we never paint
4363 - // a see-through value — in that case the CSS literal fallbacks keep it legible.
4364 - function syncRatingBubbleColors(botId) {
4365 - var $chatBox = getChatBoxByBotId(botId);
4366 - if (!$chatBox || !$chatBox.length) return;
4367 - var bubbleEl = $chatBox.find('.mxchat-rating-bot-bubble').last()[0];
4368 - if (!bubbleEl) return;
4369 - var cs = window.getComputedStyle(bubbleEl);
4370 - var fg = cs.color;
4371 - var bg = cs.backgroundColor;
4372 - var hasFg = fg && fg !== 'rgba(0, 0, 0, 0)' && fg !== 'transparent';
4373 - var hasBg = bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent';
4374 - // Expose on the bubble too, for any inheriting styles / future use.
4375 - if (hasFg) bubbleEl.style.setProperty('--mxchat-bot-fg', fg);
4376 - if (hasBg) bubbleEl.style.setProperty('--mxchat-bot-bg', bg);
4377 - // Paint the Send pill directly — the part that actually fixes the bug.
4378 - var submitEl = bubbleEl.querySelector('.mxchat-rating-submit');
4379 - if (submitEl) {
4380 - if (hasFg) submitEl.style.backgroundColor = fg; // fill = bot font color
4381 - if (hasBg) submitEl.style.color = bg; // label = bubble background
4382 - }
4383 - }
4384 -
4385 - function copy(key) {
4386 - var c = mxchatChat.satisfaction_rating_copy || {};
4387 - var d = {
4388 - question: 'Was this helpful?',
4389 - helpful: 'Helpful',
4390 - not_helpful: 'Not helpful',
4391 - dismiss: 'Dismiss',
4392 - thanks: 'Thanks! Anything we should improve? (optional)',
4393 - placeholder: 'Tell us what could be better…',
4394 - send: 'Send',
4395 - skip: 'Skip',
4396 - saved: 'Thanks for the feedback.'
4397 - };
4398 - return c[key] || d[key];
4399 - }
4400 -
4401 - function thumbUpSvg() {
4402 - 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>';
4403 - }
4404 - function thumbDownSvg() {
4405 - 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>';
4406 - }
4407 -
4408 - function buildPromptHtml(botId) {
4409 - var styleAttr = botBubbleStyleAttr(botId);
4410 - return ''
4411 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4412 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
4413 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
4414 - + '<div class="mxchat-rating-actions">'
4415 - + '<span class="mxchat-rating-buttons">'
4416 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
4417 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
4418 - + '</span>'
4419 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
4420 - + '</div>'
4421 - + '</div>'
4422 - + '</div>';
4423 - }
4424 -
4425 - function buildFeedbackHtml(botId, rating) {
4426 - var styleAttr = botBubbleStyleAttr(botId);
4427 - return ''
4428 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4429 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
4430 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
4431 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
4432 - + '<div class="mxchat-rating-feedback-actions">'
4433 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
4434 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
4435 - + '</div>'
4436 - + '</div>'
4437 - + '</div>';
4438 - }
4439 -
4440 - function buildSavedHtml(botId) {
4441 - var styleAttr = botBubbleStyleAttr(botId);
4442 - return ''
4443 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4444 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
4445 - + '</div>';
4446 - }
4447 -
4448 - function getChatBoxByBotId(botId) {
4449 - var $byId = $('#chat-box-' + botId);
4450 - if ($byId.length) return $byId.first();
4451 - return $('.chat-box').first();
4452 - }
4453 -
4454 - function scrollChatBoxToBottom($chatBox) {
4455 - if (!$chatBox || !$chatBox.length) return;
4456 - $chatBox.scrollTop($chatBox[0].scrollHeight);
4457 - }
4458 -
4459 - function showPrompt(botId) {
4460 - var s = getState(botId);
4461 - if (s.promptShown || s.dismissed) return;
4462 - var sessionId = getSessionId(botId);
4463 - if (!sessionId) return;
4464 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4465 - var $chatBox = getChatBoxByBotId(botId);
4466 - if (!$chatBox.length) return;
4467 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
4468 - $chatBox.append(buildPromptHtml(botId));
4469 - syncRatingBubbleColors(botId);
4470 - s.promptShown = true;
4471 - scrollChatBoxToBottom($chatBox);
4472 - }
4473 -
4474 - function submitRating(botId, rating, feedback) {
4475 - var sessionId = getSessionId(botId);
4476 - if (!sessionId) return;
4477 - $.post(mxchatChat.ajax_url, {
4478 - action: 'mxchat_save_rating',
4479 - session_id: sessionId,
4480 - bot_id: botId,
4481 - rating: rating,
4482 - feedback: feedback || ''
4483 - });
4484 - markRated(sessionId);
4485 - }
4486 -
4487 - function onBotReply(botId) {
4488 - var s = getState(botId);
4489 - s.botReplies += 1;
4490 - if (s.promptShown || s.dismissed) return;
4491 - var sessionId = getSessionId(botId);
4492 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4493 - if (s.botReplies < MIN_BOT_REPLIES) return;
4494 - if (s.idleTimer) clearTimeout(s.idleTimer);
4495 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4496 - }
4497 -
4498 - function onUserMessage(botId) {
4499 - var s = getState(botId);
4500 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4501 - }
4502 -
4503 - function botIdFromChatBox(el) {
4504 - var id = el && el.id ? el.id : '';
4505 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4506 - }
4507 -
4508 - function setupObserver(chatBox) {
4509 - var botId = botIdFromChatBox(chatBox);
4510 - try {
4511 - var observer = new MutationObserver(function(mutations) {
4512 - mutations.forEach(function(m) {
4513 - for (var i = 0; i < m.addedNodes.length; i++) {
4514 - var node = m.addedNodes[i];
4515 - if (!node || node.nodeType !== 1) continue;
4516 - var $n = $(node);
4517 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4518 - 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)
4519 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4520 - }
4521 - });
4522 - });
4523 - observer.observe(chatBox, { childList: true });
4524 - } catch (e) { /* noop */ }
4525 - }
4526 -
4527 - $('.chat-box').each(function() { setupObserver(this); });
4528 -
4529 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4530 - e.preventDefault();
4531 - var $btn = $(this);
4532 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4533 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4534 - var botId = $prompt.data('bot-id') || 'default';
4535 - var rating = parseInt($btn.attr('data-rating'), 10);
4536 - if (rating !== 1 && rating !== -1) return;
4537 - submitRating(botId, rating, '');
4538 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4539 - syncRatingBubbleColors(botId);
4540 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4541 - });
4542 -
4543 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4544 - e.preventDefault();
4545 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4546 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4547 - var botId = $prompt.data('bot-id') || 'default';
4548 - var s = getState(botId);
4549 - s.dismissed = true;
4550 - markRated(getSessionId(botId));
4551 - ($wrap.length ? $wrap : $prompt).remove();
4552 - });
4553 -
4554 - function closeFeedback($fb) {
4555 - var botId = $fb.data('bot-id') || 'default';
4556 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4557 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4558 - syncRatingBubbleColors(botId);
4559 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4560 - }
4561 -
4562 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4563 - e.preventDefault();
4564 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4565 - });
4566 -
4567 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4568 - e.preventDefault();
4569 - var $fb = $(this).closest('.mxchat-rating-feedback');
4570 - var botId = $fb.data('bot-id') || 'default';
4571 - var rating = parseInt($fb.attr('data-rating'), 10);
4572 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4573 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4574 - if (text !== '') {
4575 - submitRating(botId, rating, text);
4576 - }
4577 - closeFeedback($fb);
4578 - });
4579 3248 });
4580 3249