PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.3
MxChat – AI Chatbot & Content Generation for WordPress v3.0.3
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 -1907 3.2.113.0.3 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();
@@ -1236,16 +846,8 @@
1236 846
1237 847 // Re-enable chat input when stream ends with content
1238 848 enableChatInput(botId);
1239 849
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 850 if (callback) {
1249 851 callback(accumulatedContent);
1250 852 }
1251 853 return;
@@ -1268,16 +870,8 @@
1268 870
1269 871 // Re-enable chat input after streaming completes
1270 872 enableChatInput(botId);
1271 873
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 874 if (callback) {
1281 875 callback(accumulatedContent);
1282 876 }
1283 877 return;
@@ -1334,9 +928,8 @@
1334 928 }
1335 929
1336 930 processStream();
1337 931 }).catch(streamError => {
1338 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1339 932 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1340 933 callMxChat(message, callback, botId);
1341 934 });
1342 935 }
@@ -1343,9 +936,8 @@
1343 936
1344 937 processStream();
1345 938 })
1346 939 .catch(error => {
1347 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1348 940 // Check if we have server error data with chat mode
1349 941 if (error && error.isServerError && error.data) {
1350 942 // Check for chat mode in error data
1351 943 if (error.data.chat_mode) {
@@ -1358,9 +950,8 @@
1358 950 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1359 951 callMxChat(message, callback, botId);
1360 952 }
1361 953 });
1362 - }); // refreshNonceIfNeeded
1363 954 }
1364 955
1365 956 // Helper function to handle non-streaming responses
1366 957 function handleNonStreamResponse(data, callback, botId) {
@@ -1399,16 +990,21 @@
1399 990 errorMessage = "An error occurred. Please try again or contact support.";
1400 991 }
1401 992
1402 993 // Handle session reset action (IP changed, session expired, etc.)
1403 - // Silent reset — keep chat UI intact, just get a new session and retry
1404 994 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)
995 + // Clear the old session and generate a new one
996 + resetChatSession(botId);
997 + // Re-send the original message with the new session
1407 998 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1408 999 if (originalMessage) {
1409 1000 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1410 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1001 + // Re-add the user message and thinking indicator
1002 + appendMessage("user", originalMessage, '', [], false, botId);
1003 + appendThinkingMessage(botId);
1004 + scrollToBottom(botId);
1005 + // Determine whether to use streaming
1006 + const currentModel = mxchatChat.model || 'gpt-4o';
1411 1007 if (shouldUseStreaming(currentModel)) {
1412 1008 callMxChatStream(originalMessage, callback, botId);
1413 1009 } else {
1414 1010 callMxChat(originalMessage, callback, botId);
@@ -1430,22 +1026,8 @@
1430 1026 }
1431 1027 return; // Exit early for errors
1432 1028 }
1433 1029
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 1030 // Handle different response formats
1449 1031 if (data.text || data.html || data.message) {
1450 1032
1451 1033 // Apply response hooks
@@ -1490,15 +1072,19 @@
1490 1072 }
1491 1073
1492 1074 // Enhanced updateChatModeIndicator function for immediate DOM updates
1493 1075 function updateChatModeIndicator(mode, botId) {
1076 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1494 1077 botId = botId || 'default';
1495 1078 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1079 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1496 1080 if (indicator) {
1497 1081 const oldText = indicator.textContent;
1082 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1498 1083
1499 1084 if (mode === 'agent') {
1500 1085 indicator.textContent = 'Live Agent';
1086 + console.log('[MxChat] Mode is agent, calling startPolling...');
1501 1087 startPolling(botId);
1502 1088 } else {
1503 1089 // Everything else is AI mode
1504 1090 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1569,21 +1155,9 @@
1569 1155 // Update the event handlers to use the correct function names (using event delegation)
1570 1156 // Use class-based selectors for multi-instance support
1571 1157 $(document).on('click', '.send-button', function() {
1572 1158 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 - }
1159 + disableChatInput(botId);
1586 1160 sendMessage(botId);
1587 1161 });
1588 1162
1589 1163 // Override enter key handler (using event delegation)
@@ -1590,378 +1164,14 @@
1590 1164 $(document).on('keypress', '.chat-input', function(e) {
1591 1165 if (e.which == 13 && !e.shiftKey) {
1592 1166 e.preventDefault();
1593 1167 var botId = getBotIdFromElement(this);
1594 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1595 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1596 - disableChatInput(botId);
1597 - }
1168 + disableChatInput(botId);
1598 1169 sendMessage(botId);
1599 1170 }
1600 1171 });
1601 1172
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 -
1173 +
1964 1174 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1965 1175 try {
1966 1176 // Determine styles based on sender type
1967 1177 let messageClass, bgColor, fontColor;
@@ -1999,12 +1209,17 @@
1999 1209 'margin-bottom': '1em'
2000 1210 });
2001 1211 }
2002 1212
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);
1213 + // Process the message content based on sender
1214 + let fullMessage;
1215 + if (sender === "user") {
1216 + // For user messages, apply linkify after sanitization
1217 + fullMessage = linkify(messageText);
1218 + } else {
1219 + // For bot/agent messages, preserve HTML
1220 + fullMessage = messageText;
1221 + }
2007 1222
2008 1223 // Add images if provided
2009 1224 if (images && images.length > 0) {
2010 1225 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -2053,12 +1268,8 @@
2053 1268 if (lastUserMessage.length) {
2054 1269 scrollElementToTop(lastUserMessage, botId);
2055 1270 }
2056 1271 }
2057 -
2058 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
2059 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2060 - }
2061 1272 });
2062 1273
2063 1274 if (messageText.id) {
2064 1275 var instance = MxChatInstances.get(botId);
@@ -2143,12 +1354,26 @@
2143 1354 bgColor = botMessageBgColor;
2144 1355 fontColor = botMessageFontColor;
2145 1356 }
2146 1357
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);
1358 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1359 + // This prevents double-processing of URLs that are already formatted as HTML
1360 + var fullMessage;
1361 + if (sender === "user") {
1362 + // Always linkify user messages (they're plain text)
1363 + fullMessage = linkify(responseText);
1364 + } else {
1365 + // For bot/agent messages, check if HTML already exists
1366 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1367 + responseText.includes('<img') || responseText.includes('<div') ||
1368 + responseText.includes('<p>') || responseText.includes('<br>')) {
1369 + // Response already has HTML, don't process it
1370 + fullMessage = responseText;
1371 + } else {
1372 + // Plain text response, apply linkify
1373 + fullMessage = linkify(responseText);
1374 + }
1375 + }
2151 1376
2152 1377 if (responseHtml) {
2153 1378 // Only add line breaks if there's actual text content before the HTML
2154 1379 if (fullMessage && fullMessage.trim()) {
@@ -2205,12 +1430,8 @@
2205 1430 }
2206 1431
2207 1432 // Re-enable chat input after response is displayed
2208 1433 enableChatInput(botId);
2209 -
2210 - if (sender === "bot" || sender === "agent") {
2211 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2212 - }
2213 1434 } else {
2214 1435 appendMessage(sender, responseText, responseHtml, images, false, botId);
2215 1436 // Re-enable chat input after response is displayed
2216 1437 enableChatInput(botId);
@@ -2219,15 +1440,8 @@
2219 1440
2220 1441
2221 1442 function appendThinkingMessage(botId) {
2222 1443 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 1444 var $chatBox = getElement(botId, 'chat-box');
2231 1445
2232 1446 // Remove any existing thinking dots in this bot's chat first
2233 1447 $chatBox.find('.thinking-dots').remove();
@@ -2249,9 +1463,9 @@
2249 1463 '</div>' +
2250 1464 '</div>';
2251 1465
2252 1466 // 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 + ';"';
1467 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
2254 1468 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
2255 1469 scrollToBottom(botId);
2256 1470 }
2257 1471
@@ -2257,11 +1471,9 @@
2257 1471
2258 1472 function removeThinkingDots(botId) {
2259 1473 botId = botId || 'default';
2260 1474 var $chatBox = getElement(botId, 'chat-box');
2261 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
2262 1475 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
2263 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
2264 1476 }
2265 1477
2266 1478 // ====================================
2267 1479 // TEXT FORMATTING & PROCESSING
@@ -2295,12 +1507,9 @@
2295 1507 processedText = formatTextStyling(processedText);
2296 1508
2297 1509 // Process code blocks BEFORE processing links
2298 1510 processedText = formatCodeBlocks(processedText);
2299 -
2300 - // Process markdown tables BEFORE converting newlines to paragraphs
2301 - processedText = formatMarkdownTables(processedText);
2302 -
1511 +
2303 1512 // NOW convert to paragraphs
2304 1513 processedText = convertNewlinesToBreaks(processedText);
2305 1514
2306 1515 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -2313,63 +1522,37 @@
2313 1522 // Return as a proper link without the brackets
2314 1523 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2315 1524 });
2316 1525
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 - }
1526 + // Process proper markdown links with text: [text](url)
1527 + // This MUST have non-empty text in the first brackets
1528 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1529 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1530 + // Make sure we have actual text (not just whitespace)
1531 + if (!text || !text.trim()) {
1532 + // If no text, treat the URL as the text
1533 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1534 + const safeUrl = safeEncodeUrl(cleanUrl);
1535 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2368 1536 }
2369 - return result;
2370 - })(processedText);
1537 +
1538 + // Clean the URL
1539 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1540 + const safeUrl = safeEncodeUrl(cleanUrl);
1541 + const safeText = sanitizeUserInput(text);
1542 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1543 + });
2371 1544
1545 + // Handle empty markdown links: [](url)
1546 + // This is a specific case where there's no text
1547 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1548 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1549 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1550 + const safeUrl = safeEncodeUrl(cleanUrl);
1551 + // Use the URL itself as the link text
1552 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1553 + });
1554 +
2372 1555 // Process phone numbers: [text](tel:number)
2373 1556 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
2374 1557 processedText = processedText.replace(phonePattern, (match, text, phone) => {
2375 1558 const safePhone = safeEncodeUrl(phone);
@@ -2521,78 +1704,9 @@
2521 1704 });
2522 1705
2523 1706 return text;
2524 1707 }
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 -
1708 +
2595 1709 function sanitizeUserInput(text) {
2596 1710 const div = document.createElement('div');
2597 1711 div.textContent = text;
2598 1712 return div.innerHTML;
@@ -2663,14 +1777,13 @@
2663 1777 requestAnimationFrame(smoothScroll);
2664 1778 }
2665 1779 }
2666 1780
2667 - function scrollElementToTop(element, botId, topOffset) {
1781 + function scrollElementToTop(element, botId) {
2668 1782 botId = botId || 'default';
2669 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2670 1783 var chatBox = getElement(botId, 'chat-box');
2671 1784 var elementTop = element.position().top + chatBox.scrollTop();
2672 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1785 + chatBox.animate({ scrollTop: elementTop }, 500);
2673 1786 }
2674 1787
2675 1788 function showChatWidget(botId) {
2676 1789 botId = botId || 'default';
@@ -2814,12 +1927,15 @@
2814 1927 // LIVE AGENT FUNCTIONALITY
2815 1928 // ====================================
2816 1929
2817 1930 function startPolling(botId) {
1931 + console.log('[MxChat] startPolling called for botId:', botId);
2818 1932 botId = botId || 'default';
2819 1933 var instance = MxChatInstances.get(botId);
2820 1934 // Clear any existing interval first
2821 1935 stopPolling(botId);
1936 + // Start new polling interval
1937 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2822 1938 instance.pollingInterval = setInterval(function() {
2823 1939 checkForAgentMessages(botId);
2824 1940 }, 5000);
2825 1941 }
@@ -2824,17 +1940,20 @@
2824 1940 }, 5000);
2825 1941 }
2826 1942
2827 1943 function stopPolling(botId) {
1944 + console.log('[MxChat] stopPolling called for botId:', botId);
2828 1945 botId = botId || 'default';
2829 1946 var instance = MxChatInstances.get(botId);
2830 1947 if (instance.pollingInterval) {
2831 1948 clearInterval(instance.pollingInterval);
2832 1949 instance.pollingInterval = null;
1950 + console.log('[MxChat] Polling stopped for botId:', botId);
2833 1951 }
2834 1952 }
2835 1953
2836 1954 function checkForAgentMessages(botId) {
1955 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2837 1956 botId = botId || 'default';
2838 1957 var instance = MxChatInstances.get(botId);
2839 1958 const sessionId = getChatSession(botId);
2840 1959 $.ajax({
@@ -2860,12 +1979,8 @@
2860 1979 instance.processedMessageIds.add(message.id);
2861 1980 }
2862 1981 });
2863 1982
2864 - if (hasNewMessage) {
2865 - enableChatInput(botId);
2866 - }
2867 -
2868 1983 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2869 1984 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2870 1985 showNotification(botId);
2871 1986 }
@@ -2871,13 +1986,8 @@
2871 1986 }
2872 1987
2873 1988 scrollToBottom(botId, true);
2874 1989 }
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 1990 },
2881 1991 error: function (xhr, status, error) {
2882 1992 // Polling error - silently continue
2883 1993 }
@@ -2887,29 +1997,20 @@
2887 1997 // ====================================
2888 1998 // CHAT HISTORY & PERSISTENCE
2889 1999 // ====================================
2890 2000
2891 -function loadChatHistory(botId, onComplete) {
2001 +function loadChatHistory(botId) {
2892 2002 botId = botId || 'default';
2893 2003 var instance = MxChatInstances.get(botId);
2894 2004
2895 2005 // Prevent duplicate loading
2896 2006 if (instance.chatHistoryLoaded) {
2897 - if (onComplete) onComplete();
2898 2007 return;
2899 2008 }
2900 2009
2901 - // Use getChatSession which returns null if no session exists (does NOT create one)
2902 2010 var sessionId = getChatSession(botId);
2903 2011 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2904 2012
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 2013 if (chatPersistenceEnabled && sessionId) {
2913 2014 $.ajax({
2914 2015 url: mxchatChat.ajax_url,
2915 2016 type: 'POST',
@@ -2920,12 +2021,11 @@
2920 2021 },
2921 2022 success: function(response) {
2922 2023 // Handle session reset (IP changed while user was away)
2923 2024 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);
2025 + // Silently reset session - user will start fresh
2026 + resetChatSession(botId);
2926 2027 instance.chatHistoryLoaded = true; // Prevent retry loop
2927 - if (onComplete) onComplete();
2928 2028 return;
2929 2029 }
2930 2030
2931 2031 // Check if the response indicates success
@@ -2981,19 +2081,9 @@
2981 2081 var content = message.content;
2982 2082 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2983 2083 content = decodeHTMLEntities(content);
2984 2084
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")) {
2085 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2996 2086 messageElement.html(content);
2997 2087 } else {
2998 2088 var formattedContent = linkify(content);
2999 2089 messageElement.html(formattedContent);
@@ -3033,17 +2123,13 @@
3033 2123 instance.chatHistoryLoaded = true;
3034 2124 }
3035 2125 }
3036 2126 }
3037 - if (onComplete) onComplete();
3038 2127 },
3039 2128 error: function(xhr, status, error) {
3040 2129 // Error loading chat history - silently continue
3041 - if (onComplete) onComplete();
3042 2130 }
3043 2131 });
3044 - } else {
3045 - if (onComplete) onComplete();
3046 2132 }
3047 2133 }
3048 2134
3049 2135
@@ -3107,10 +2193,10 @@
3107 2193 .then(data => {
3108 2194 if (data.success) {
3109 2195 container.style.display = 'none';
3110 2196 nameElement.textContent = '';
3111 - instance.activePdfFile = null;
3112 - appendMessage('bot', 'PDF removed.', '', [], false, botId);
2197 + activePdfFile = null;
2198 + appendMessage('bot', 'PDF removed.');
3113 2199 }
3114 2200 })
3115 2201 .catch(error => {
3116 2202 // Error removing PDF - silently continue
@@ -3116,16 +2202,14 @@
3116 2202 // Error removing PDF - silently continue
3117 2203 });
3118 2204 }
3119 2205
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 -
2206 + function removeActiveWord() {
2207 + const container = document.getElementById('active-word-container');
2208 + const nameElement = document.getElementById('active-word-name');
2209 +
2210 + if (!container || !nameElement || !activeWordFile) return;
2211 +
3128 2212 fetch(mxchatChat.ajax_url, {
3129 2213 method: 'POST',
3130 2214 headers: {
3131 2215 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3131,9 +2215,9 @@
3131 2215 'Content-Type': 'application/x-www-form-urlencoded',
3132 2216 },
3133 2217 body: new URLSearchParams({
3134 2218 'action': 'mxchat_remove_word',
3135 - 'session_id': getChatSession(botId),
2219 + 'session_id': sessionId,
3136 2220 'nonce': mxchatChat.nonce
3137 2221 })
3138 2222 })
3139 2223 .then(response => response.json())
@@ -3140,10 +2224,10 @@
3140 2224 .then(data => {
3141 2225 if (data.success) {
3142 2226 container.style.display = 'none';
3143 2227 nameElement.textContent = '';
3144 - instance.activeWordFile = null;
3145 - appendMessage('bot', 'Word document removed.', '', [], false, botId);
2228 + activeWordFile = null;
2229 + appendMessage('bot', 'Word document removed.');
3146 2230 }
3147 2231 })
3148 2232 .catch(error => {
3149 2233 // Error removing Word document - silently continue
@@ -3221,35 +2305,45 @@
3221 2305 // ====================================
3222 2306
3223 2307 function checkPreChatDismissal(botId) {
3224 2308 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) {
2309 + $.ajax({
2310 + url: mxchatChat.ajax_url,
2311 + type: 'POST',
2312 + data: {
2313 + action: 'mxchat_check_pre_chat_message_status',
2314 + _ajax_nonce: mxchatChat.nonce
2315 + },
2316 + success: function(response) {
2317 + if (response.success && !response.data.dismissed) {
2318 + getElement(botId, 'pre-chat-message').fadeIn(250);
2319 + } else {
3231 2320 getElement(botId, 'pre-chat-message').hide();
3232 - return;
3233 2321 }
3234 - // Expired — clear and show again
3235 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2322 + },
2323 + error: function() {
2324 + // Error checking pre-chat dismissal - silently continue
3236 2325 }
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 - }
2326 + });
3242 2327 }
3243 2328
3244 2329 function handlePreChatDismissal(botId) {
3245 2330 botId = botId || 'default';
3246 2331 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 - }
2332 + $.ajax({
2333 + url: mxchatChat.ajax_url,
2334 + type: 'POST',
2335 + data: {
2336 + action: 'mxchat_dismiss_pre_chat_message',
2337 + _ajax_nonce: mxchatChat.nonce
2338 + },
2339 + success: function() {
2340 + $('#pre-chat-message').hide();
2341 + },
2342 + error: function() {
2343 + // Error dismissing pre-chat message - silently continue
2344 + }
2345 + });
3252 2346 }
3253 2347
3254 2348
3255 2349 // ====================================
@@ -3304,14 +2398,9 @@
3304 2398 collapseQuickQuestions(botId);
3305 2399 });
3306 2400
3307 2401 // 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 - }
2402 + $(document).on('click', '.floating-chatbot-button', function() {
3314 2403 var botId = getBotIdFromElement(this);
3315 2404 var $chatbot = getElement(botId, 'floating-chatbot');
3316 2405 var $badge = getElement(botId, 'chat-notification-badge');
3317 2406 var $preChat = getElement(botId, 'pre-chat-message');
@@ -3316,88 +2405,35 @@
3316 2405 var $badge = getElement(botId, 'chat-notification-badge');
3317 2406 var $preChat = getElement(botId, 'pre-chat-message');
3318 2407
3319 2408 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');
2409 + $chatbot.removeClass('hidden').addClass('visible');
2410 + $(this).addClass('hidden');
3323 2411 $badge.hide(); // Hide notification when opening chat
3324 2412 disableScroll();
3325 2413 $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 2414 } else {
3358 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3359 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2415 + $chatbot.removeClass('visible').addClass('hidden');
2416 + $(this).removeClass('hidden');
3360 2417 enableScroll();
3361 2418 checkPreChatDismissal(botId);
3362 2419 }
3363 2420 });
3364 2421
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.
2422 + // Allow clicking anywhere on the title bar to close the chatbot
3369 2423 $(document).on('click', '.chatbot-top-bar', function() {
3370 2424 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');
2425 + getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2426 + getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3374 2427 enableScroll();
3375 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3376 2428 });
3377 2429
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 2430 $(document).on('click', '.close-pre-chat-message', function(e) {
3397 2431 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3398 2432 var botId = getBotIdFromElement(this);
3399 - handlePreChatDismissal(botId);
2433 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2434 + $(this).remove();
2435 + });
3400 2436 });
3401 2437
3402 2438
3403 2439 // PDF upload button handlers - use class selector
@@ -3413,20 +2449,17 @@
3413 2449 var wordInput = getElementDOM(botId, 'word-upload');
3414 2450 if (wordInput) wordInput.click();
3415 2451 });
3416 2452
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 -
2453 + // PDF file input change handler
2454 + addSafeEventListener('pdf-upload', 'change', async function(e) {
2455 + const file = e.target.files[0];
2456 +
3424 2457 if (!file || file.type !== 'application/pdf') {
3425 2458 alert('Please select a valid PDF file.');
3426 2459 return;
3427 2460 }
3428 -
2461 +
3429 2462 if (!sessionId) {
3430 2463 alert('Error: No session ID found');
3431 2464 return;
3432 2465 }
@@ -3434,49 +2467,47 @@
3434 2467 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3435 2468 alert('Error: Ajax configuration missing');
3436 2469 return;
3437 2470 }
3438 -
2471 +
3439 2472 // 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;
2473 + const uploadBtn = document.getElementById('pdf-upload-btn');
2474 + const sendBtn = document.getElementById('send-button');
3443 2475 const originalBtnContent = uploadBtn.innerHTML;
3444 -
2476 +
3445 2477 try {
3446 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3447 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3448 2478 const formData = new FormData();
3449 2479 formData.append('action', 'mxchat_upload_pdf');
3450 2480 formData.append('pdf_file', file);
3451 2481 formData.append('session_id', sessionId);
3452 2482 formData.append('nonce', mxchatChat.nonce);
3453 -
2483 +
3454 2484 uploadBtn.disabled = true;
3455 - if (sendBtn) sendBtn.disabled = true;
2485 + sendBtn.disabled = true;
3456 2486 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3457 2487 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3458 2488 </svg>`;
3459 -
2489 +
3460 2490 const response = await fetch(mxchatChat.ajax_url, {
3461 2491 method: 'POST',
3462 2492 body: formData
3463 2493 });
3464 -
2494 +
3465 2495 const data = await response.json();
3466 -
2496 +
3467 2497 if (data.success) {
3468 2498 // Hide popular questions if they exist
3469 - if (hasQuickQuestions(botId)) {
3470 - collapseQuickQuestions(botId);
2499 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2500 + if (hasQuickQuestions()) {
2501 + collapseQuickQuestions();
3471 2502 }
3472 -
2503 +
3473 2504 // 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;
2505 + showActivePdf(data.data.filename);
2506 +
2507 + appendMessage('bot', data.data.message);
2508 + scrollToBottom();
2509 + activePdfFile = data.data.filename;
3479 2510 } else {
3480 2511 alert('Failed to upload PDF. Please try again.');
3481 2512 }
3482 2513 } catch (error) {
@@ -3482,76 +2513,66 @@
3482 2513 } catch (error) {
3483 2514 alert('Error uploading file. Please try again.');
3484 2515 } finally {
3485 2516 uploadBtn.disabled = false;
3486 - if (sendBtn) sendBtn.disabled = false;
2517 + sendBtn.disabled = false;
3487 2518 uploadBtn.innerHTML = originalBtnContent;
3488 2519 this.value = ''; // Reset file input
3489 2520 }
3490 2521 });
3491 2522
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 -
2523 + // Word file input change handler
2524 + addSafeEventListener('word-upload', 'change', async function(e) {
2525 + const file = e.target.files[0];
2526 +
3499 2527 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
3500 2528 alert('Please select a valid Word document (.docx).');
3501 2529 return;
3502 2530 }
3503 -
2531 +
3504 2532 if (!sessionId) {
3505 2533 alert('Error: No session ID found');
3506 2534 return;
3507 2535 }
3508 2536
3509 - if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3510 - alert('Error: Ajax configuration missing');
3511 - return;
3512 - }
3513 -
3514 2537 // 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;
2538 + const uploadBtn = document.getElementById('word-upload-btn');
2539 + const sendBtn = document.getElementById('send-button');
3518 2540 const originalBtnContent = uploadBtn.innerHTML;
3519 -
2541 +
3520 2542 try {
3521 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3522 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3523 2543 const formData = new FormData();
3524 2544 formData.append('action', 'mxchat_upload_word');
3525 2545 formData.append('word_file', file);
3526 2546 formData.append('session_id', sessionId);
3527 2547 formData.append('nonce', mxchatChat.nonce);
3528 -
2548 +
3529 2549 uploadBtn.disabled = true;
3530 - if (sendBtn) sendBtn.disabled = true;
2550 + sendBtn.disabled = true;
3531 2551 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3532 2552 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3533 2553 </svg>`;
3534 -
2554 +
3535 2555 const response = await fetch(mxchatChat.ajax_url, {
3536 2556 method: 'POST',
3537 2557 body: formData
3538 2558 });
3539 -
2559 +
3540 2560 const data = await response.json();
3541 -
2561 +
3542 2562 if (data.success) {
3543 2563 // Hide popular questions if they exist
3544 - if (hasQuickQuestions(botId)) {
3545 - collapseQuickQuestions(botId);
2564 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2565 + if (hasQuickQuestions()) {
2566 + collapseQuickQuestions();
3546 2567 }
3547 -
2568 +
3548 2569 // 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;
2570 + showActiveWord(data.data.filename);
2571 +
2572 + appendMessage('bot', data.data.message);
2573 + scrollToBottom();
2574 + activeWordFile = data.data.filename;
3554 2575 } else {
3555 2576 alert('Failed to upload Word document. Please try again.');
3556 2577 }
3557 2578 } catch (error) {
@@ -3557,25 +2578,25 @@
3557 2578 } catch (error) {
3558 2579 alert('Error uploading file. Please try again.');
3559 2580 } finally {
3560 2581 uploadBtn.disabled = false;
3561 - if (sendBtn) sendBtn.disabled = false;
2582 + sendBtn.disabled = false;
3562 2583 uploadBtn.innerHTML = originalBtnContent;
3563 2584 this.value = ''; // Reset file input
3564 2585 }
3565 2586 });
3566 2587
3567 - // Remove button click handlers - delegated, bot-aware (were bound to stale un-suffixed ids)
3568 - $(document).on('click', '.remove-pdf-btn', function(e) {
2588 + // Remove button click handlers
2589 + document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
3569 2590 e.preventDefault();
3570 2591 e.stopPropagation();
3571 - removeActivePdf(getBotIdFromElement(this));
2592 + removeActivePdf();
3572 2593 });
3573 -
3574 - $(document).on('click', '.remove-word-btn', function(e) {
2594 +
2595 + document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
3575 2596 e.preventDefault();
3576 2597 e.stopPropagation();
3577 - removeActiveWord(getBotIdFromElement(this));
2598 + removeActiveWord();
3578 2599 });
3579 2600
3580 2601 // Window resize handlers
3581 2602 $(window).on('resize orientationchange', function() {
@@ -3613,437 +2634,380 @@
3613 2634 });
3614 2635
3615 2636
3616 2637 // ====================================
3617 -// INIT LOADER & CHAT CONTAINER HELPERS
2638 +// EMAIL COLLECTION SETUP - FIXED VERSION
3618 2639 // ====================================
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 2640 // Only run email collection setup if it's enabled
3671 2641 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2642 + // Email collection form setup and handlers
2643 + const emailForm = document.getElementById('email-collection-form');
2644 + const emailBlocker = document.getElementById('email-blocker');
2645 + const chatbotWrapper = document.getElementById('chat-container');
3672 2646
3673 - // Track submitting state per bot
3674 - const emailSubmittingState = {};
2647 + if (emailForm && emailBlocker && chatbotWrapper) {
2648 +
2649 + // Add loading state management
2650 + let isSubmitting = false;
2651 +
2652 + // Optimized UI transition functions
2653 + function showEmailForm() {
2654 + emailBlocker.style.display = 'flex';
2655 + chatbotWrapper.style.display = 'none';
2656 + }
3675 2657
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); }
2658 + function showChatContainer() {
2659 + // Show chat immediately without delay
2660 + emailBlocker.style.display = 'none';
2661 + chatbotWrapper.style.display = 'flex';
2662 +
2663 + // Load chat history only after showing chat container
2664 + if (typeof loadChatHistory === 'function') {
2665 + loadChatHistory();
3684 2666 }
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 - }
2667 + }
3704 2668
3705 - function isValidEmailAddress(email) {
3706 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3707 - return emailRegex.test(email.trim()) && email.length <= 254;
3708 - }
2669 + // Enhanced email validation
2670 + function isValidEmail(email) {
2671 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2672 + return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2673 + }
3709 2674
3710 - function isValidNameInput(name) {
3711 - return name && name.trim().length >= 2 && name.trim().length <= 100;
3712 - }
2675 + // Enhanced name validation
2676 + function isValidName(name) {
2677 + return name && name.trim().length >= 2 && name.trim().length <= 100;
2678 + }
3713 2679
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();
2680 + // Show loading state with spinner
2681 + function setSubmissionState(loading) {
2682 + const submitButton = document.getElementById('email-submit-button');
2683 + const emailInput = document.getElementById('user-email');
2684 + const nameInput = document.getElementById('user-name');
2685 +
2686 + if (loading) {
2687 + isSubmitting = true;
2688 + if (submitButton) submitButton.disabled = true;
2689 + if (emailInput) emailInput.disabled = true;
2690 + if (nameInput) nameInput.disabled = true;
2691 +
2692 + // Store original content and add spinner
2693 + if (submitButton && !submitButton.getAttribute('data-original-html')) {
2694 + submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2695 +
2696 + // Add loading spinner while keeping original text
2697 + const originalText = submitButton.textContent;
2698 + submitButton.innerHTML = `
2699 + <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2700 + <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2701 + <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2702 + <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2703 + </circle>
2704 + </svg>
2705 + ${originalText}
2706 + `;
2707 +
2708 + submitButton.style.opacity = '0.8';
2709 + }
2710 + } else {
2711 + isSubmitting = false;
2712 + if (submitButton) submitButton.disabled = false;
2713 + if (emailInput) emailInput.disabled = false;
2714 + if (nameInput) nameInput.disabled = false;
2715 +
2716 + // Restore original content
2717 + if (submitButton) {
2718 + const originalHtml = submitButton.getAttribute('data-original-html');
2719 + if (originalHtml) {
2720 + submitButton.innerHTML = originalHtml;
2721 + }
2722 + submitButton.style.opacity = '1';
2723 + }
2724 + }
3742 2725 }
3743 2726
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}
2727 + // Error display functions
2728 + function showEmailError(message) {
2729 + clearEmailError();
2730 +
2731 + const errorDiv = document.createElement('div');
2732 + errorDiv.className = 'email-error';
2733 + errorDiv.style.cssText = `
2734 + color: #e74c3c;
2735 + font-size: 12px;
2736 + margin-top: 8px;
2737 + padding: 4px 0;
2738 + animation: fadeInError 0.3s ease;
2739 + `;
2740 + errorDiv.textContent = message;
2741 +
2742 + // Add CSS animation if not already present
2743 + if (!document.getElementById('email-error-styles')) {
2744 + const style = document.createElement('style');
2745 + style.id = 'email-error-styles';
2746 + style.textContent = `
2747 + @keyframes fadeInError {
2748 + from { opacity: 0; transform: translateY(-5px); }
2749 + to { opacity: 1; transform: translateY(0); }
2750 + }
2751 + .email-input-shake {
2752 + animation: shake 0.5s ease-in-out;
2753 + }
2754 + @keyframes shake {
2755 + 0%, 100% { transform: translateX(0); }
2756 + 25% { transform: translateX(-5px); }
2757 + 75% { transform: translateX(5px); }
2758 + }
2759 + @keyframes spin {
2760 + from { transform: rotate(0deg); }
2761 + to { transform: rotate(360deg); }
2762 + }
2763 + .email-spinner {
2764 + display: inline-block;
2765 + vertical-align: middle;
2766 + }
3769 2767 `;
3770 - submitButton.style.opacity = '0.8';
2768 + document.head.appendChild(style);
3771 2769 }
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';
2770 +
2771 + emailForm.appendChild(errorDiv);
2772 +
2773 + // Add shake animation to inputs
2774 + const emailInput = document.getElementById('user-email');
2775 + const nameInput = document.getElementById('user-name');
2776 +
2777 + if (emailInput) {
2778 + emailInput.classList.add('email-input-shake');
2779 + setTimeout(() => {
2780 + emailInput.classList.remove('email-input-shake');
2781 + }, 500);
3784 2782 }
2783 +
2784 + if (nameInput) {
2785 + nameInput.classList.add('email-input-shake');
2786 + setTimeout(() => {
2787 + nameInput.classList.remove('email-input-shake');
2788 + }, 500);
2789 + }
3785 2790 }
3786 - }
3787 2791
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) {
2792 + function clearEmailError() {
3823 2793 const existingErrors = emailForm.querySelectorAll('.email-error');
3824 2794 existingErrors.forEach(error => error.remove());
3825 2795 }
3826 - }
3827 2796
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 - }
2797 + // MAIN FORM SUBMIT HANDLER
2798 + // Remove any existing event listeners first
2799 + emailForm.removeEventListener('submit', handleFormSubmit);
3840 2800
3841 - function checkSessionAndEmailForBot(botId) {
3842 - const sessionId = MxChatInstances.ensureSession(botId);
2801 + // Add the form submit handler
2802 + emailForm.addEventListener('submit', handleFormSubmit);
3843 2803
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);
2804 + function handleFormSubmit(event) {
2805 + event.preventDefault();
2806 + event.stopPropagation();
3850 2807
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}`);
2808 + // Prevent double submission
2809 + if (isSubmitting) {
2810 + return false;
3865 2811 }
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 2812
3884 - // Event delegation for email form submission
3885 - $(document).on('submit', '.email-collection-form', function(e) {
3886 - e.preventDefault();
3887 - e.stopPropagation();
2813 + const userEmail = document.getElementById('user-email').value.trim();
2814 + const nameInput = document.getElementById('user-name');
2815 + const userName = nameInput ? nameInput.value.trim() : '';
2816 + const sessionId = getChatSession();
3888 2817
3889 - var botId = getBotIdFromElement(this);
2818 + // Validate email before submission
2819 + if (!userEmail) {
2820 + showEmailError('Please enter your email address.');
2821 + return false;
2822 + }
3890 2823
3891 - // Prevent double submission
3892 - if (emailSubmittingState[botId]) {
3893 - return false;
3894 - }
2824 + if (!isValidEmail(userEmail)) {
2825 + showEmailError('Please enter a valid email address.');
2826 + return false;
2827 + }
3895 2828
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);
2829 + // Validate name if field exists
2830 + if (nameInput && !isValidName(userName)) {
2831 + showEmailError('Please enter a valid name (2-100 characters).');
2832 + return false;
2833 + }
3901 2834
3902 - // Validate email
3903 - if (!userEmail) {
3904 - showEmailError(botId, 'Please enter your email address.');
3905 - return false;
3906 - }
2835 + // Clear any existing errors
2836 + clearEmailError();
2837 + setSubmissionState(true);
3907 2838
3908 - if (!isValidEmailAddress(userEmail)) {
3909 - showEmailError(botId, 'Please enter a valid email address.');
3910 - return false;
3911 - }
2839 + // Prepare form data with optional name
2840 + const formData = new URLSearchParams({
2841 + action: 'mxchat_handle_save_email_and_response',
2842 + email: userEmail,
2843 + session_id: sessionId,
2844 + nonce: mxchatChat.nonce,
2845 + });
3912 2846
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 - }
2847 + // Add name to form data if provided
2848 + if (userName) {
2849 + formData.append('name', userName);
2850 + }
3918 2851
3919 - clearEmailError(botId);
3920 - setEmailSubmissionState(botId, true);
2852 + fetch(mxchatChat.ajax_url, {
2853 + method: 'POST',
2854 + headers: {
2855 + 'Content-Type': 'application/x-www-form-urlencoded',
2856 + },
2857 + body: formData
2858 + })
2859 + .then((response) => {
2860 + if (!response.ok) {
2861 + throw new Error(`HTTP error! status: ${response.status}`);
2862 + }
2863 + return response.json();
2864 + })
2865 + .then((data) => {
2866 + setSubmissionState(false);
3921 2867
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 - });
2868 + if (data.success) {
2869 + // Show chat immediately
2870 + showChatContainer();
3929 2871
3930 - if (userName) {
3931 - formData.append('name', userName);
2872 + // Handle bot response if provided
2873 + if (data.message && typeof appendMessage === 'function') {
2874 + setTimeout(() => {
2875 + appendMessage('bot', data.message);
2876 + if (typeof scrollToBottom === 'function') {
2877 + scrollToBottom();
2878 + }
2879 + }, 100);
2880 + }
2881 + } else {
2882 + showEmailError(data.message || 'Failed to save email. Please try again.');
2883 + }
2884 + })
2885 + .catch((error) => {
2886 + setSubmissionState(false);
2887 + showEmailError('An error occurred. Please try again.');
2888 + });
2889 +
2890 + return false; // Extra prevention
3932 2891 }
3933 2892
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);
2893 + // Real-time email validation
2894 + const emailInput = document.getElementById('user-email');
2895 + if (emailInput) {
2896 + let validationTimeout;
2897 +
2898 + emailInput.addEventListener('input', function() {
2899 + // Clear previous validation timeout
2900 + if (validationTimeout) {
2901 + clearTimeout(validationTimeout);
2902 + }
2903 +
2904 + // Debounce validation
2905 + validationTimeout = setTimeout(() => {
2906 + const email = this.value.trim();
2907 + clearEmailError();
2908 +
2909 + if (email && !isValidEmail(email)) {
2910 + showEmailError('Please enter a valid email address.');
2911 + }
2912 + }, 500);
2913 + });
3949 2914
3950 - if (data.success) {
3951 - showChatContainerForBot(botId);
2915 + // Handle Enter key
2916 + emailInput.addEventListener('keypress', function(e) {
2917 + if (e.key === 'Enter' && !isSubmitting) {
2918 + e.preventDefault();
2919 + emailForm.dispatchEvent(new Event('submit'));
2920 + }
2921 + });
2922 + }
3952 2923
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, '');
2924 + // Real-time name validation
2925 + const nameInput = document.getElementById('user-name');
2926 + if (nameInput) {
2927 + let nameValidationTimeout;
2928 +
2929 + nameInput.addEventListener('input', function() {
2930 + // Clear previous validation timeout
2931 + if (nameValidationTimeout) {
2932 + clearTimeout(nameValidationTimeout);
3959 2933 }
2934 +
2935 + // Debounce validation
2936 + nameValidationTimeout = setTimeout(() => {
2937 + const name = this.value.trim();
2938 + clearEmailError();
2939 +
2940 + if (name && !isValidName(name)) {
2941 + showEmailError('Name must be between 2 and 100 characters.');
2942 + }
2943 + }, 500);
2944 + });
3960 2945
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);
2946 + // Handle Enter key
2947 + nameInput.addEventListener('keypress', function(e) {
2948 + if (e.key === 'Enter' && !isSubmitting) {
2949 + e.preventDefault();
2950 + emailForm.dispatchEvent(new Event('submit'));
3968 2951 }
2952 + });
2953 + }
2954 +
2955 + // Initial state check
2956 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2957 + const emailState = mxchatChat.initial_email_state;
2958 + if (emailState.show_email_form) {
2959 + showEmailForm();
3969 2960 } else {
3970 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
2961 + showChatContainer();
3971 2962 }
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 - }
2963 + } else {
2964 + // Check email status via AJAX
2965 + setTimeout(checkSessionAndEmail, 100);
4010 2966 }
4011 - });
4012 2967
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 - }
2968 + // Check if email exists for the current session
2969 + function checkSessionAndEmail() {
2970 + const sessionId = getChatSession();
2971 +
2972 + fetch(mxchatChat.ajax_url, {
2973 + method: 'POST',
2974 + headers: {
2975 + 'Content-Type': 'application/x-www-form-urlencoded',
2976 + },
2977 + body: new URLSearchParams({
2978 + action: 'mxchat_check_email_provided',
2979 + session_id: sessionId,
2980 + nonce: mxchatChat.nonce,
2981 + })
2982 + })
2983 + .then((response) => {
2984 + if (!response.ok) {
2985 + throw new Error(`HTTP error! status: ${response.status}`);
2986 + }
2987 + return response.json();
2988 + })
2989 + .then((data) => {
2990 + if (data.success) {
2991 + if (data.data.logged_in || data.data.email) {
2992 + showChatContainer();
2993 + } else {
2994 + showEmailForm();
2995 + }
2996 + } else {
2997 + // On error, default to showing email form
2998 + showEmailForm();
2999 + }
3000 + })
3001 + .catch((error) => {
3002 + // Email check failed - default to email form
3003 + showEmailForm();
3004 + });
4021 3005 }
4022 - });
4023 3006
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 - });
3007 + } else {
3008 + // Email collection is enabled but essential elements are missing - silently continue
3009 + }
4046 3010 }
4047 3011
4048 3012 // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
4049 3013 $(document).on('click', '.pre-chat-message', function() {
@@ -4051,32 +3015,39 @@
4051 3015 var $chatbot = getElement(botId, 'floating-chatbot');
4052 3016 if ($chatbot.hasClass('hidden')) {
4053 3017 $chatbot.removeClass('hidden').addClass('visible');
4054 3018 getElement(botId, 'floating-chatbot-button').addClass('hidden');
4055 - handlePreChatDismissal(botId);
3019 + $(this).fadeOut(250); // Hide pre-chat message
4056 3020 disableScroll(); // Disable scroll when chatbot opens
3021 + }
3022 + });
4057 3023
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 - }
3024 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
3025 + // This is a fallback for legacy support
3026 + $(document).on('click', '.close-pre-chat-message', function() {
3027 + var botId = getBotIdFromElement(this);
3028 + var $preChat = getElement(botId, 'pre-chat-message');
3029 + $preChat.fadeOut(200); // Hide the message
4063 3030
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);
3031 + // Send an AJAX request to set the transient flag for 24 hours
3032 + $.ajax({
3033 + url: mxchatChat.ajax_url,
3034 + type: 'POST',
3035 + data: {
3036 + action: 'mxchat_dismiss_pre_chat_message',
3037 + _ajax_nonce: mxchatChat.nonce
3038 + },
3039 + success: function() {
3040 + // Ensure the message is hidden after dismissal
3041 + $preChat.hide();
3042 + },
3043 + error: function() {
3044 + // Error dismissing pre-chat message - silently continue
4072 3045 }
4073 - }
3046 + });
4074 3047 });
4075 3048
4076 - // Legacy duplicate close handler removed — handled by single event delegation above
4077 3049
4078 -
4079 3050 function hasQuickQuestions(botId) {
4080 3051 botId = botId || 'default';
4081 3052 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
4082 3053 if (!questionsContainer) return false;
@@ -4212,11 +3183,18 @@
4212 3183 });
4213 3184
4214 3185 // Initialize when document is ready
4215 3186 setFullHeight();
3187 + trackOriginatingPage();
4216 3188
4217 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
4218 - // until the user's first interaction via MxChatInstances.ensureSession()
3189 + // Only load chat history if email collection is disabled
3190 + if (mxchatChat.email_collection_enabled !== 'on') {
3191 + // Load history for all instances
3192 + $('.mxchat-chatbot-wrapper').each(function() {
3193 + var botId = $(this).data('bot-id') || 'default';
3194 + loadChatHistory(botId);
3195 + });
3196 + }
4219 3197
4220 3198 // Initialize chat visibility for all instances
4221 3199 $('.mxchat-chatbot-wrapper').each(function() {
4222 3200 var botId = $(this).data('bot-id') || 'default';
@@ -4269,312 +3247,6 @@
4269 3247 }, 2000);
4270 3248 });
4271 3249 }
4272 3250 }
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 3251 });
4580 3252