PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.7
MxChat – AI Chatbot & Content Generation for WordPress v3.0.7
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/chat-script.js +285 -1663 3.2.193.0.7 View file →
@@ -1,166 +1,6 @@
1 1 jQuery(document).ready(function($) {
2 2
3 - // Nonce refresh — v2 (plan-6a68c9).
4 - //
5 - // The widget no longer relies on a nonce embedded in inline cached HTML.
6 - // Before each chat-send / stream-send / upload, we call the REST endpoint
7 - // GET /wp-json/mxchat/v1/nonce and use the freshly-issued value. The
8 - // endpoint creates the nonce with action `mxchat_chat_send`; the server-side
9 - // verifier ALSO still accepts the legacy `mxchat_chat_nonce` action for a
10 - // 30-day backwards-compat window so cached pages still in users' browsers
11 - // (which carry the legacy inline-localized nonce) keep working.
12 - //
13 - // Cache: a single module-scoped slot. TTL 12h conservatively (WP nonces are
14 - // 24h but we refetch at half-life so a freshly-cached-page user never sees
15 - // a borderline-stale nonce).
16 - var cachedFreshNonce = null;
17 - var cachedFreshNonceFetchedAt = 0;
18 - var NONCE_TTL_MS = 12 * 60 * 60 * 1000;
19 - var nonceRefreshState = 'idle'; // 'idle' | 'pending' | 'done'
20 - var nonceRefreshCallbacks = [];
21 -
22 - function getRestNonceUrl() {
23 - if (typeof mxchatChat !== 'undefined' && mxchatChat.rest_url) {
24 - return mxchatChat.rest_url.replace(/\/+$/, '') + '/nonce';
25 - }
26 - // Fallback: derive from current origin if mxchatChat.rest_url isn't set.
27 - return window.location.origin + '/wp-json/mxchat/v1/nonce';
28 - }
29 -
30 - function fetchFreshNonceFromRest() {
31 - return fetch(getRestNonceUrl(), {
32 - credentials: 'same-origin',
33 - headers: { 'Accept': 'application/json' }
34 - }).then(function (resp) {
35 - if (!resp.ok) {
36 - throw new Error('REST nonce fetch failed: ' + resp.status);
37 - }
38 - return resp.json();
39 - }).then(function (data) {
40 - if (data && data.nonce) {
41 - return data.nonce;
42 - }
43 - throw new Error('REST nonce response had no nonce field.');
44 - });
45 - }
46 -
47 - /**
48 - * withFreshNonce(cb) — invoke cb() after ensuring mxchatChat.nonce is fresh.
49 - * Tries REST endpoint first (cache-bypass design); falls back to the legacy
50 - * admin-ajax refresh path if REST is unavailable. Idempotent — concurrent
51 - * calls share the same in-flight refresh.
52 - */
53 - function withFreshNonce(callback) {
54 - if (typeof mxchatChat === 'undefined') {
55 - if (callback) callback();
56 - return;
57 - }
58 - var now = Date.now();
59 - if (cachedFreshNonce && (now - cachedFreshNonceFetchedAt) < NONCE_TTL_MS) {
60 - mxchatChat.nonce = cachedFreshNonce;
61 - if (callback) callback();
62 - return;
63 - }
64 - if (callback) nonceRefreshCallbacks.push(callback);
65 - if (nonceRefreshState === 'pending') return;
66 - nonceRefreshState = 'pending';
67 -
68 - var resolved = function (nonce) {
69 - if (nonce) {
70 - cachedFreshNonce = nonce;
71 - cachedFreshNonceFetchedAt = Date.now();
72 - mxchatChat.nonce = nonce;
73 - }
74 - nonceRefreshState = 'done';
75 - var pending = nonceRefreshCallbacks;
76 - nonceRefreshCallbacks = [];
77 - pending.forEach(function (cb) { try { cb(); } catch (e) {} });
78 - };
79 -
80 - fetchFreshNonceFromRest()
81 - .then(resolved)
82 - .catch(function () {
83 - // Fallback to the legacy admin-ajax refresh path (issued with the
84 - // old action `mxchat_chat_nonce`; the server still accepts both
85 - // during the compat window).
86 - if (mxchatChat.ajax_url) {
87 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' })
88 - .done(function (res) {
89 - if (res && res.success && res.data && res.data.nonce) {
90 - resolved(res.data.nonce);
91 - return;
92 - }
93 - resolved(null);
94 - })
95 - .fail(function () { resolved(null); });
96 - } else {
97 - resolved(null);
98 - }
99 - });
100 - }
101 -
102 - // Backwards-compat alias — every existing caller in this file (and any
103 - // out-of-tree consumer that hit this internal API) keeps working unchanged.
104 - function refreshNonceIfNeeded(callback) {
105 - return withFreshNonce(callback);
106 - }
107 -
108 - // Dynamic-settings refresh (plan-32db95).
109 - //
110 - // Every widget setting ships inline in cached page HTML, so behind a
111 - // full-page cache the site owner can't purge (host cache, CDN, the
112 - // browser itself) a toggled setting looks broken until the cache turns
113 - // over. Same distrust-cached-HTML reasoning as the per-request nonce:
114 - // on the FIRST widget open per page load we ask the nonce endpoint for
115 - // the current behavior-gate settings (?with_settings=1), merge them over
116 - // mxchatChat, and rebuild the header menu. Colors are NOT refreshed —
117 - // they're server-inline-styled, so a runtime swap would visibly flash.
118 - // On any failure we keep the inline values silently (nonce-fallback
119 - // posture). At most one request per page load, only if a widget opens.
120 - var dynamicSettingsState = 'idle'; // 'idle' | 'pending' | 'done'
121 -
122 - function mxchatRefreshDynamicSettings() {
123 - if (dynamicSettingsState !== 'idle') return;
124 - if (typeof mxchatChat === 'undefined') return;
125 - dynamicSettingsState = 'pending';
126 -
127 - var applied = function (data) {
128 - dynamicSettingsState = 'done';
129 - if (!data) return; // endpoint unavailable — inline values stand.
130 - if (data.nonce) {
131 - // Seed the nonce cache too: saves the first send's REST
132 - // round-trip and keeps us under the endpoint's rate limit.
133 - cachedFreshNonce = data.nonce;
134 - cachedFreshNonceFetchedAt = Date.now();
135 - mxchatChat.nonce = data.nonce;
136 - }
137 - if (data.settings && typeof data.settings === 'object') {
138 - $.extend(mxchatChat, data.settings);
139 - mxchatRebuildHeaderMenus();
140 - }
141 - };
142 -
143 - fetch(getRestNonceUrl() + '?with_settings=1', {
144 - credentials: 'same-origin',
145 - headers: { 'Accept': 'application/json' }
146 - }).then(function (resp) {
147 - if (!resp.ok) throw new Error('settings refresh failed: ' + resp.status);
148 - return resp.json();
149 - }).then(applied).catch(function () {
150 - // Fallback: legacy admin-ajax refresh path, same as withFreshNonce.
151 - if (mxchatChat.ajax_url) {
152 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce', with_settings: 1 })
153 - .done(function (res) {
154 - applied(res && res.success && res.data ? res.data : null);
155 - })
156 - .fail(function () { applied(null); });
157 - } else {
158 - applied(null);
159 - }
160 - });
161 - }
162 -
163 3 // ====================================
164 4 // MULTI-INSTANCE MANAGEMENT SYSTEM
165 5 // ====================================
166 6
@@ -176,9 +16,9 @@
176 16 var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
177 17
178 18 this.instances[botId] = {
179 19 botId: botId,
180 - sessionId: null,
20 + sessionId: this.getChatSession(botId),
181 21 lastSeenMessageId: '',
182 22 notificationCheckInterval: null,
183 23 pollingInterval: null,
184 24 processedMessageIds: new Set(),
@@ -203,77 +43,23 @@
203 43 return Object.keys(this.instances);
204 44 },
205 45
206 46 // Session management per bot
207 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
208 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
209 47 getChatSession: function(botId) {
210 48 var cookieName = 'mxchat_session_id_' + botId;
211 - var storageKey = 'mxchat_session_id_' + botId;
212 49 var sessionId = getCookie(cookieName);
213 50
214 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
215 51 if (!sessionId) {
216 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
52 + sessionId = generateSessionId();
53 + this.setChatSession(botId, sessionId);
217 54 }
218 55
219 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
220 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
221 - // storage). Without this, ensureSession() can generate and store an ID that
222 - // getChatSession() then can't read back, causing null session_ids on send.
223 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
224 - sessionId = this.instances[botId].sessionId;
225 - }
226 -
227 - // Guard against stored sentinel values that indicate earlier broken writes.
228 - if (sessionId === 'null' || sessionId === 'undefined') {
229 - sessionId = null;
230 - }
231 -
232 - // Re-sync cookie from localStorage if cookie was lost
233 - if (sessionId && !getCookie(cookieName)) {
234 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
235 - }
236 -
237 - return sessionId || null;
56 + return sessionId;
238 57 },
239 58
240 - // Lazy session initializer — called on first user interaction
241 - ensureSession: function(botId) {
242 - botId = botId || 'default';
243 - var instance = this.instances[botId] || this.init(botId);
244 -
245 - if (instance.sessionId) {
246 - return instance.sessionId;
247 - }
248 -
249 - // Check for existing session from cookie or localStorage
250 - var existingSession = this.getChatSession(botId);
251 -
252 - if (existingSession) {
253 - instance.sessionId = existingSession;
254 - } else {
255 - // Brand new session
256 - var newId = generateSessionId();
257 - this.setChatSession(botId, newId);
258 - instance.sessionId = newId;
259 - }
260 -
261 - // Now that we have a session, do the deferred work
262 - refreshNonceIfNeeded();
263 - trackOriginatingPage();
264 -
265 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
266 - // so we do NOT call it here to avoid a race condition.
267 -
268 - return instance.sessionId;
269 - },
270 -
271 59 setChatSession: function(botId, sessionId) {
272 60 var cookieName = 'mxchat_session_id_' + botId;
273 - var storageKey = 'mxchat_session_id_' + botId;
274 61 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
275 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
276 62 if (this.instances[botId]) {
277 63 this.instances[botId].sessionId = sessionId;
278 64 }
279 65 },
@@ -278,10 +64,8 @@
278 64 }
279 65 },
280 66
281 67 resetChatSession: function(botId) {
282 - // Clear old session from localStorage before setting new one
283 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
284 68 var newSessionId = generateSessionId();
285 69 this.setChatSession(botId, newSessionId);
286 70 var $chatBox = getElement(botId, 'chat-box');
287 71 if ($chatBox.length) {
@@ -290,20 +74,8 @@
290 74 if (this.instances[botId]) {
291 75 this.instances[botId].chatHistoryLoaded = false;
292 76 this.instances[botId].processedMessageIds = new Set();
293 77 }
294 - },
295 -
296 - // Silent reset — new session ID without clearing the chat UI
297 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
298 - silentResetSession: function(botId) {
299 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
300 - var newSessionId = generateSessionId();
301 - this.setChatSession(botId, newSessionId);
302 - if (this.instances[botId]) {
303 - this.instances[botId].sessionId = newSessionId;
304 - }
305 - return newSessionId;
306 78 }
307 79 };
308 80
309 81 // ====================================
@@ -343,20 +115,8 @@
343 115 var id = $floating.attr('id') || '';
344 116 var match = id.match(/floating-chatbot-(.+)/);
345 117 if (match) return match[1];
346 118 }
347 - // Fallback: the pre-chat teaser bubble (#pre-chat-message-{bot_id}) is a SIBLING
348 - // outside .mxchat-chatbot-wrapper / .floating-chatbot, so its children — e.g. the
349 - // .close-pre-chat-message button, which carries only a class and no id — miss both
350 - // branches above. Walk to the nearest ancestor whose id is pre-chat-message-{bot_id}
351 - // and read the suffix. (closest() includes the element itself, so a click directly on
352 - // #pre-chat-message-{bot_id} resolves here too.)
353 - var $preChat = $(element).closest('[id^="pre-chat-message-"]');
354 - if ($preChat.length) {
355 - var preId = $preChat.attr('id') || '';
356 - var preMatch = preId.match(/^pre-chat-message-(.+)$/);
357 - if (preMatch) return preMatch[1];
358 - }
359 119 // Fallback: check if element itself has an ID with bot suffix (e.g., floating-chatbot-button-{bot_id})
360 120 var elementId = $(element).attr('id') || '';
361 121 if (elementId) {
362 122 // Match patterns like: floating-chatbot-button-{bot_id}, pre-chat-message-{bot_id}
@@ -396,27 +156,9 @@
396 156 if (parts.length == 2) return parts.pop().split(";").shift();
397 157 }
398 158
399 159 function generateSessionId() {
400 - // Session IDs function as the de-facto bearer token for an anonymous
401 - // chat, so generate them with a CSPRNG when available. Math.random is a
402 - // legacy fallback for ancient/sandboxed environments that lack
403 - // window.crypto. The 'mxchat_chat_' prefix is preserved exactly (other
404 - // code pattern-matches on it). (plan-0c17b5)
405 - var rand;
406 - try {
407 - if (window.crypto && window.crypto.getRandomValues) {
408 - var buf = new Uint8Array(16); // 128 bits
409 - window.crypto.getRandomValues(buf);
410 - rand = Array.prototype.map.call(buf, function (b) {
411 - return ('0' + b.toString(16)).slice(-2);
412 - }).join('');
413 - }
414 - } catch (e) {}
415 - if (!rand) {
416 - rand = Math.random().toString(36).substr(2, 9); // legacy fallback
417 - }
418 - return 'mxchat_chat_' + rand;
160 + return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
419 161 }
420 162
421 163 // Legacy function - now delegates to instance manager
422 164 function getChatSession(botId) {
@@ -633,106 +375,13 @@
633 375 sendButton.disabled = false;
634 376 sendButton.style.opacity = '1';
635 377 sendButton.style.pointerEvents = 'auto';
636 378 }
637 - // Every completion path re-enables input, so this is the single restore
638 - // point for the streaming Stop affordance (no-op when not in stop mode).
639 - mxchatRestoreSendButton(botId);
640 379 }
641 380
642 -// --- Streaming Stop control -------------------------------------------------
643 -// One live stream handle per bot instance, so Stop on one widget never aborts
644 -// another bot on the same page.
645 -var mxchatActiveStreams = {};
646 -// Original send-button markup, captured once per bot the first time the Stop
647 -// state is shown (never captured while already in stop mode, so a rapid
648 -// stop-then-resend can't save the stop glyph as the "original").
649 -var mxchatSendMarkup = {};
650 -
651 -function mxchatShowStopButton(botId) {
652 - var btn = getElementDOM(botId, 'send-button');
653 - if (!btn) return;
654 - if (!btn.classList.contains('mxchat-stop-mode')) {
655 - mxchatSendMarkup[botId] = {
656 - html: btn.innerHTML,
657 - label: btn.getAttribute('aria-label')
658 - };
659 - }
660 -
661 - // Mirror the send icon's rendered size + color so the stop glyph looks
662 - // native, including custom send images/colors and theme overrides.
663 - var child = btn.querySelector('svg, img');
664 - var size = 25;
665 - var color = '';
666 - if (child) {
667 - var rect = child.getBoundingClientRect();
668 - if (rect.width) {
669 - size = Math.round(Math.min(rect.width, rect.height));
670 - }
671 - var cs = window.getComputedStyle(child);
672 - color = (child.tagName.toLowerCase() === 'svg' ? cs.fill : cs.color) || '';
673 - }
674 - var stopLabel = (typeof mxchatChat !== 'undefined' && mxchatChat.stop_button_label) || 'Stop response';
675 - 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>';
676 - // An add-on's DIRECT send-button handler (e.g. mxchat-vision's rebind) can
677 - // start a stream synchronously while the originating click is still
678 - // bubbling up to our delegated handler. Without this guard, that handler
679 - // reads the just-added stop-mode class as a user Stop press and aborts the
680 - // brand-new stream — the user's message renders but no reply ever fires
681 - // (plan-4bba64 silent message loss). The flag only spans the current event
682 - // dispatch: cleared on the next macrotask, long before a real Stop click.
683 - btn.__mxchatStopJustShown = true;
684 - setTimeout(function () { btn.__mxchatStopJustShown = false; }, 0);
685 - btn.classList.add('mxchat-stop-mode');
686 - btn.setAttribute('aria-label', stopLabel);
687 - btn.setAttribute('title', stopLabel);
688 - // disableChatInput() ran when the turn was sent; the Stop control itself
689 - // must stay clickable while the textarea remains disabled.
690 - btn.disabled = false;
691 - btn.style.opacity = '1';
692 - btn.style.pointerEvents = 'auto';
693 -}
694 -
695 -function mxchatRestoreSendButton(botId) {
696 - var btn = getElementDOM(botId, 'send-button');
697 - var saved = mxchatSendMarkup[botId];
698 - if (!btn || !btn.classList.contains('mxchat-stop-mode') || !saved) return;
699 - btn.innerHTML = saved.html;
700 - btn.classList.remove('mxchat-stop-mode');
701 - btn.removeAttribute('title');
702 - if (saved.label) {
703 - btn.setAttribute('aria-label', saved.label);
704 - }
705 -}
706 -
707 -function mxchatStopStreaming(botId) {
708 - var entry = mxchatActiveStreams[botId];
709 - if (!entry || !entry.controller) return;
710 - entry.aborted = true;
711 - try { entry.controller.abort(); } catch (e) {}
712 -}
713 -
714 -// Returns true when a stream rejection came from an intentional Stop click:
715 -// keep the partial text as the turn's answer — no error UI, no fallback resend.
716 -function mxchatHandleStreamAbort(botId, accumulatedContent, callback) {
717 - var entry = mxchatActiveStreams[botId];
718 - if (!entry || !entry.aborted) return false;
719 - delete mxchatActiveStreams[botId];
720 - if (!accumulatedContent) {
721 - // Stopped before the first chunk: drop the thinking bubble, no orphan message.
722 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
723 - }
724 - enableChatInput(botId); // also restores the send icon
725 - if (callback) {
726 - callback(accumulatedContent || '');
727 - }
728 - return true;
729 -}
730 -
731 381 // Update your existing sendMessage function
732 382 function sendMessage(botId) {
733 383 botId = botId || 'default';
734 - MxChatInstances.ensureSession(botId);
735 384 var $chatInput = getElement(botId, 'chat-input');
736 385 var message = $chatInput.val();
737 386
738 387 // ADD PROMPT HOOK HERE
@@ -740,18 +389,13 @@
740 389 message = customMxChatFilter(message, "prompt");
741 390 }
742 391
743 392 if (message) {
744 - // Don't disable input in live agent mode - let users chat freely
745 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
746 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
747 - if (!isAgentMode) {
748 - disableChatInput(botId);
749 - }
393 + // Disable input while waiting for response
394 + disableChatInput(botId);
750 395
751 396 appendMessage("user", message, '', [], false, botId);
752 397 $chatInput.val('');
753 - mxchatUpdateCharCounter($chatInput[0]); // reset the char counter after send (plan 7091a2)
754 398 $chatInput.css('height', 'auto');
755 399
756 400 if (hasQuickQuestions(botId)) {
757 401 collapseQuickQuestions(botId);
@@ -758,9 +402,9 @@
758 402 }
759 403 appendThinkingMessage(botId);
760 404 scrollToBottom(botId);
761 405
762 - const currentModel = mxchatChat.model || 'gpt-5.6-sol';
406 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
763 407
764 408 // Check if streaming is enabled AND supported for this model
765 409 if (shouldUseStreaming(currentModel)) {
766 410 callMxChatStream(message, function(response) {
@@ -776,9 +420,8 @@
776 420
777 421 // Update your existing sendMessageToChatbot function
778 422 function sendMessageToChatbot(message, botId) {
779 423 botId = botId || 'default';
780 - MxChatInstances.ensureSession(botId);
781 424
782 425 // ADD PROMPT HOOK HERE
783 426 if (typeof customMxChatFilter === 'function') {
784 427 message = customMxChatFilter(message, "prompt");
@@ -783,14 +426,10 @@
783 426 if (typeof customMxChatFilter === 'function') {
784 427 message = customMxChatFilter(message, "prompt");
785 428 }
786 429
787 - // Don't disable input in live agent mode - let users chat freely
788 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
789 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
790 - if (!isAgentMode) {
791 - disableChatInput(botId);
792 - }
430 + // Disable input while waiting for response
431 + disableChatInput(botId);
793 432
794 433 var sessionId = getChatSession(botId);
795 434
796 435 if (hasQuickQuestions(botId)) {
@@ -798,9 +437,9 @@
798 437 }
799 438 appendThinkingMessage(botId);
800 439 scrollToBottom(botId);
801 440
802 - const currentModel = mxchatChat.model || 'gpt-5.6-sol';
441 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
803 442
804 443 // Check if streaming is enabled AND supported for this model
805 444 if (shouldUseStreaming(currentModel)) {
806 445 callMxChatStream(message, function(response) {
@@ -869,15 +508,8 @@
869 508
870 509 function callMxChat(message, callback, botId) {
871 510 botId = botId || getMxChatBotId();
872 511
873 - // Streaming fallbacks land here: drop any leftover stream handle and
874 - // return the button to its send state (no-op for plain non-stream turns).
875 - if (mxchatActiveStreams[botId]) {
876 - delete mxchatActiveStreams[botId];
877 - }
878 - mxchatRestoreSendButton(botId);
879 -
880 512 // Store the message in case we need to retry after session reset
881 513 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
882 514
883 515 // Get page context if contextual awareness is enabled
@@ -885,28 +517,13 @@
885 517
886 518 // Get instance for session start timestamp (used when persistence is OFF)
887 519 var instance = MxChatInstances.get(botId);
888 520
889 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
890 - // and returns the guaranteed-present session id from the in-memory instance even when
891 - // cookie/localStorage writes are silently blocked by the browser.
892 - var sessionId = MxChatInstances.ensureSession(botId);
893 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
894 - // Last-resort generation to ensure we never POST a null marker.
895 - sessionId = generateSessionId();
896 - MxChatInstances.setChatSession(botId, sessionId);
897 - }
898 -
899 - // Wait for the page-cache nonce refresh to complete before firing the
900 - // chat-send AJAX. On cached pages the inline mxchatChat.nonce is stale
901 - // until refreshNonceIfNeeded() returns; constructing ajaxData inside the
902 - // callback guarantees we read the fresh value. See plan-c5457f.
903 - refreshNonceIfNeeded(function() {
904 521 // Prepare AJAX data
905 522 const ajaxData = {
906 523 action: 'mxchat_handle_chat_request',
907 524 message: message,
908 - session_id: sessionId,
525 + session_id: getChatSession(botId),
909 526 nonce: mxchatChat.nonce,
910 527 current_page_url: window.location.href,
911 528 current_page_title: document.title,
912 529 bot_id: botId,
@@ -912,14 +529,14 @@
912 529 bot_id: botId,
913 530 // Pass session start timestamp so AI context matches what user sees
914 531 session_start_timestamp: instance.sessionStartTimestamp || 0
915 532 };
916 -
533 +
917 534 // Add page context if available
918 535 if (pageContext) {
919 536 ajaxData.page_context = JSON.stringify(pageContext);
920 537 }
921 -
538 +
922 539 // CHECK FOR VISION FLAGS AND ADD THEM
923 540 if (window.mxchatVisionProcessed) {
924 541 ajaxData.vision_processed = true;
925 542 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -928,9 +545,9 @@
928 545 window.mxchatVisionProcessed = false;
929 546 window.mxchatOriginalMessage = null;
930 547 window.mxchatVisionImagesCount = 0;
931 548 }
932 -
549 +
933 550 $.ajax({
934 551 url: mxchatChat.ajax_url,
935 552 type: 'POST',
936 553 dataType: 'json',
@@ -968,16 +585,23 @@
968 585 errorMessage = "An error occurred. Please try again or contact support.";
969 586 }
970 587
971 588 // Handle session reset action (IP changed, session expired, etc.)
972 - // Silent reset — keep chat UI intact, just get a new session and retry
973 589 if (response.data && response.data.action === 'reset_session') {
974 - MxChatInstances.silentResetSession(botId);
975 - // Re-send the original message with the new session (user message is already displayed)
590 + // Clear the old session and generate a new one
591 + resetChatSession(botId);
592 + // Remove the temporary loading message
593 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
594 + // Re-send the original message with the new session
976 595 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
977 596 if (originalMessage) {
978 597 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
979 - var currentModel = mxchatChat.model || 'gpt-5.6-sol';
598 + // Re-add the user message and thinking indicator
599 + appendMessage("user", originalMessage, '', [], false, botId);
600 + appendThinkingMessage(botId);
601 + scrollToBottom(botId);
602 + // Determine whether to use streaming
603 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
980 604 if (shouldUseStreaming(currentModel)) {
981 605 callMxChatStream(originalMessage, function(response) {
982 606 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
983 607 }, botId);
@@ -1034,11 +658,9 @@
1034 658 }
1035 659
1036 660 // Check for live agent response
1037 661 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
1038 - removeThinkingDots(botId);
1039 662 updateChatModeIndicator('agent', botId);
1040 - enableChatInput(botId);
1041 663 return;
1042 664 }
1043 665
1044 666 // Handle the message and show notification if chat is hidden
@@ -1071,13 +693,9 @@
1071 693 $badge.show();
1072 694 }
1073 695 }
1074 696 } else {
1075 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
1076 - if (response.vectorstore_error) {
1077 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
1078 - }
1079 - replaceLastMessage("bot", emptyMsg, '', [], botId);
697 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
1080 698 }
1081 699
1082 700 if (response.message_id) {
1083 701 var instance = MxChatInstances.get(botId);
@@ -1119,9 +737,8 @@
1119 737
1120 738 replaceLastMessage("bot", errorMessage, '', [], botId);
1121 739 }
1122 740 });
1123 - }); // refreshNonceIfNeeded
1124 741 }
1125 742
1126 743 function callMxChatStream(message, callback, botId) {
1127 744 botId = botId || getMxChatBotId();
@@ -1128,9 +745,9 @@
1128 745
1129 746 // Store the message in case we need to retry after session reset
1130 747 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
1131 748
1132 - const currentModel = mxchatChat.model || 'gpt-5.6-sol';
749 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1133 750 if (!isStreamingSupported(currentModel)) {
1134 751 callMxChat(message, callback, botId);
1135 752 return;
1136 753 }
@@ -1140,25 +757,12 @@
1140 757
1141 758 // Get instance for session start timestamp (used when persistence is OFF)
1142 759 var instance = MxChatInstances.get(botId);
1143 760
1144 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
1145 - // non-string value via String(), so passing `null` would POST the literal string "null"
1146 - // and land in the transcripts table as a ghost session. ensureSession() always returns
1147 - // a real string even when cookies/localStorage are blocked.
1148 - var streamSessionId = MxChatInstances.ensureSession(botId);
1149 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
1150 - streamSessionId = generateSessionId();
1151 - MxChatInstances.setChatSession(botId, streamSessionId);
1152 - }
1153 -
1154 - // Wait for the page-cache nonce refresh before constructing formData (which
1155 - // captures mxchatChat.nonce by value). Mirrors callMxChat's wrapping. See plan-c5457f.
1156 - refreshNonceIfNeeded(function() {
1157 761 const formData = new FormData();
1158 762 formData.append('action', 'mxchat_stream_chat');
1159 763 formData.append('message', message);
1160 - formData.append('session_id', streamSessionId);
764 + formData.append('session_id', getChatSession(botId));
1161 765 formData.append('nonce', mxchatChat.nonce);
1162 766 formData.append('current_page_url', window.location.href);
1163 767 formData.append('current_page_title', document.title);
1164 768 formData.append('bot_id', botId);
@@ -1183,25 +787,13 @@
1183 787
1184 788 let accumulatedContent = '';
1185 789 let testingDataReceived = false;
1186 790 let streamingStarted = false;
1187 - // Server-pushed html to append as its OWN bot bubble once the stream
1188 - // finishes (e.g. the consent-safe YouTube embed, plan 03ba33). Rendering is
1189 - // deferred to [DONE] so the embed always lands BELOW the streamed text.
1190 - let pendingAppendHtml = '';
1191 791
1192 - // Abortable stream: a fresh controller per turn, keyed by bot instance.
1193 - // The Stop control (send button swapped in place) aborts both the read
1194 - // loop and the underlying request.
1195 - var streamControl = { controller: new AbortController(), aborted: false };
1196 - mxchatActiveStreams[botId] = streamControl;
1197 - mxchatShowStopButton(botId);
1198 -
1199 792 fetch(mxchatChat.ajax_url, {
1200 793 method: 'POST',
1201 794 body: formData,
1202 - credentials: 'same-origin',
1203 - signal: streamControl.controller.signal
795 + credentials: 'same-origin'
1204 796 })
1205 797 .then(response => {
1206 798 // Store the response for potential fallback handling
1207 799 const responseClone = response.clone();
@@ -1270,16 +862,8 @@
1270 862
1271 863 // Re-enable chat input when stream ends with content
1272 864 enableChatInput(botId);
1273 865
1274 - // Scroll the user's last message to the top now that the
1275 - // bot's full reply has rendered (gives max reading room).
1276 - var $chatBoxDone = getElement(botId, 'chat-box');
1277 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
1278 - if ($lastUserMsgDone.length) {
1279 - scrollElementToTop($lastUserMsgDone, botId);
1280 - }
1281 -
1282 866 if (callback) {
1283 867 callback(accumulatedContent);
1284 868 }
1285 869 return;
@@ -1302,25 +886,8 @@
1302 886
1303 887 // Re-enable chat input after streaming completes
1304 888 enableChatInput(botId);
1305 889
1306 - // Render any server-pushed appendix html (e.g. the
1307 - // YouTube embed) as its own bot bubble below the
1308 - // streamed text — mirrors how it is saved in the
1309 - // transcript, so history replays identically.
1310 - if (pendingAppendHtml) {
1311 - appendMessage("bot", "", pendingAppendHtml, [], false, botId);
1312 - pendingAppendHtml = '';
1313 - }
1314 -
1315 - // Scroll the user's last message to the top now
1316 - // that the bot's full reply has rendered.
1317 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1318 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1319 - if ($lastUserMsgStreamDone.length) {
1320 - scrollElementToTop($lastUserMsgStreamDone, botId);
1321 - }
1322 -
1323 890 if (callback) {
1324 891 callback(accumulatedContent);
1325 892 }
1326 893 return;
@@ -1346,12 +913,8 @@
1346 913 streamingStarted = true;
1347 914 accumulatedContent += json.content;
1348 915 updateStreamingMessage(accumulatedContent, botId);
1349 916 }
1350 - // Stash appendix html (e.g. video embed) for [DONE]
1351 - else if (json.append_html) {
1352 - pendingAppendHtml = json.append_html;
1353 - }
1354 917 // Handle complete response in stream (fallback response)
1355 918 else if (json.text || json.message || json.html) {
1356 919 handleNonStreamResponse(json, callback, botId);
1357 920 return;
@@ -1381,9 +944,8 @@
1381 944 }
1382 945
1383 946 processStream();
1384 947 }).catch(streamError => {
1385 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1386 948 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1387 949 callMxChat(message, callback, botId);
1388 950 });
1389 951 }
@@ -1390,9 +952,8 @@
1390 952
1391 953 processStream();
1392 954 })
1393 955 .catch(error => {
1394 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1395 956 // Check if we have server error data with chat mode
1396 957 if (error && error.isServerError && error.data) {
1397 958 // Check for chat mode in error data
1398 959 if (error.data.chat_mode) {
@@ -1405,9 +966,8 @@
1405 966 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1406 967 callMxChat(message, callback, botId);
1407 968 }
1408 969 });
1409 - }); // refreshNonceIfNeeded
1410 970 }
1411 971
1412 972 // Helper function to handle non-streaming responses
1413 973 function handleNonStreamResponse(data, callback, botId) {
@@ -1446,16 +1006,21 @@
1446 1006 errorMessage = "An error occurred. Please try again or contact support.";
1447 1007 }
1448 1008
1449 1009 // Handle session reset action (IP changed, session expired, etc.)
1450 - // Silent reset — keep chat UI intact, just get a new session and retry
1451 1010 if (data.data && data.data.action === 'reset_session') {
1452 - MxChatInstances.silentResetSession(botId);
1453 - // Re-send the original message with the new session (user message is already displayed)
1011 + // Clear the old session and generate a new one
1012 + resetChatSession(botId);
1013 + // Re-send the original message with the new session
1454 1014 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1455 1015 if (originalMessage) {
1456 1016 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1457 - var currentModel = mxchatChat.model || 'gpt-5.6-sol';
1017 + // Re-add the user message and thinking indicator
1018 + appendMessage("user", originalMessage, '', [], false, botId);
1019 + appendThinkingMessage(botId);
1020 + scrollToBottom(botId);
1021 + // Determine whether to use streaming
1022 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1458 1023 if (shouldUseStreaming(currentModel)) {
1459 1024 callMxChatStream(originalMessage, callback, botId);
1460 1025 } else {
1461 1026 callMxChat(originalMessage, callback, botId);
@@ -1477,22 +1042,8 @@
1477 1042 }
1478 1043 return; // Exit early for errors
1479 1044 }
1480 1045
1481 - // Check for live agent response
1482 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1483 - removeThinkingDots(botId);
1484 - // Also remove any leftover bot-message that lost its temporary-message class
1485 - var $chatBox = getElement(botId, 'chat-box');
1486 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1487 - updateChatModeIndicator('agent', botId);
1488 - enableChatInput(botId);
1489 - if (callback) {
1490 - callback('');
1491 - }
1492 - return;
1493 - }
1494 -
1495 1046 // Handle different response formats
1496 1047 if (data.text || data.html || data.message) {
1497 1048
1498 1049 // Apply response hooks
@@ -1537,15 +1088,19 @@
1537 1088 }
1538 1089
1539 1090 // Enhanced updateChatModeIndicator function for immediate DOM updates
1540 1091 function updateChatModeIndicator(mode, botId) {
1092 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1541 1093 botId = botId || 'default';
1542 1094 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1095 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1543 1096 if (indicator) {
1544 1097 const oldText = indicator.textContent;
1098 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1545 1099
1546 1100 if (mode === 'agent') {
1547 1101 indicator.textContent = 'Live Agent';
1102 + console.log('[MxChat] Mode is agent, calling startPolling...');
1548 1103 startPolling(botId);
1549 1104 } else {
1550 1105 // Everything else is AI mode
1551 1106 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1616,21 +1171,9 @@
1616 1171 // Update the event handlers to use the correct function names (using event delegation)
1617 1172 // Use class-based selectors for multi-instance support
1618 1173 $(document).on('click', '.send-button', function() {
1619 1174 var botId = getBotIdFromElement(this);
1620 - // While a response is streaming the button is a Stop control.
1621 - if (this.classList.contains('mxchat-stop-mode')) {
1622 - // Same click that just started this stream (an add-on's direct handler
1623 - // ran before this delegated one) — not a Stop press. See
1624 - // mxchatShowStopButton for the full story (plan-4bba64).
1625 - if (this.__mxchatStopJustShown) return;
1626 - mxchatStopStreaming(botId);
1627 - return;
1628 - }
1629 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1630 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1631 - disableChatInput(botId);
1632 - }
1175 + disableChatInput(botId);
1633 1176 sendMessage(botId);
1634 1177 });
1635 1178
1636 1179 // Override enter key handler (using event delegation)
@@ -1637,378 +1180,14 @@
1637 1180 $(document).on('keypress', '.chat-input', function(e) {
1638 1181 if (e.which == 13 && !e.shiftKey) {
1639 1182 e.preventDefault();
1640 1183 var botId = getBotIdFromElement(this);
1641 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1642 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1643 - disableChatInput(botId);
1644 - }
1184 + disableChatInput(botId);
1645 1185 sendMessage(botId);
1646 1186 }
1647 1187 });
1648 1188
1649 -// Chat input character counter + soft limit feedback (plan 7091a2).
1650 -// Language-neutral: numbers + color only, no translatable strings. The counter
1651 -// reveals near the cap and ramps neutral -> amber -> red; an over-limit keystroke
1652 -// or trimmed paste produces a brief border-flash/shake so the maxlength cap (plan
1653 -// a3fae2) is never a silent "input jumps back". Per-bot scoped via .input-container.
1654 -function mxchatUpdateCharCounter(inputEl) {
1655 - if (!inputEl || !inputEl.closest) return;
1656 - var max = parseInt(inputEl.getAttribute('maxlength'), 10);
1657 - var container = inputEl.closest('.input-container');
1658 - if (!container || !max || max <= 0) return;
1659 - var counter = container.querySelector('.mxchat-char-counter');
1660 - if (!counter) return;
1661 - var len = inputEl.value.length;
1662 - var ratio = len / max;
1663 - var nearThreshold = 0.8; // start surfacing the counter at 80% of the cap
1664 - var cur = counter.querySelector('.mxchat-char-counter-current');
1665 - if (cur) cur.textContent = len;
1666 - var warn = ratio >= nearThreshold && len < max;
1667 - var full = len >= max;
1668 - counter.classList.toggle('is-visible', ratio >= nearThreshold);
1669 - counter.classList.toggle('is-warn', warn);
1670 - counter.classList.toggle('is-full', full);
1671 - container.classList.toggle('mxchat-input-near-limit', warn);
1672 - container.classList.toggle('mxchat-input-at-limit', full);
1673 -}
1674 -
1675 -function mxchatBumpInput(inputEl) {
1676 - var container = inputEl && inputEl.closest ? inputEl.closest('.input-container') : null;
1677 - if (!container) return;
1678 - container.classList.remove('mxchat-input-bump');
1679 - void container.offsetWidth; // reflow so a rapid second hit retriggers the animation
1680 - container.classList.add('mxchat-input-bump');
1681 - clearTimeout($(container).data('mxchatBumpTimeout'));
1682 - var t = setTimeout(function() { container.classList.remove('mxchat-input-bump'); }, 220);
1683 - $(container).data('mxchatBumpTimeout', t);
1684 -}
1685 -
1686 -// Live counter update on every input.
1687 -$(document).on('input', '.chat-input', function() {
1688 - mxchatUpdateCharCounter(this);
1689 -});
1690 -
1691 -// Visible "you've hit the edge" feedback when a printable keystroke is about to be
1692 -// rejected at the cap (maxlength silently swallows it otherwise).
1693 -$(document).on('keydown', '.chat-input', function(e) {
1694 - var max = parseInt(this.getAttribute('maxlength'), 10);
1695 - if (!max || max <= 0 || this.value.length < max) return;
1696 - if (e.ctrlKey || e.metaKey || e.altKey) return;
1697 - // A single printable char with no selection to overwrite WILL be rejected.
1698 - if (e.key && e.key.length === 1 && this.selectionStart === this.selectionEnd) {
1699 - mxchatBumpInput(this);
1700 - }
1701 -});
1702 -
1703 -// A paste that gets trimmed to the cap also bumps, so truncation is never silent.
1704 -$(document).on('paste', '.chat-input', function() {
1705 - var el = this;
1706 - var max = parseInt(el.getAttribute('maxlength'), 10);
1707 - if (!max || max <= 0) return;
1708 - setTimeout(function() {
1709 - mxchatUpdateCharCounter(el);
1710 - if (el.value.length >= max) mxchatBumpInput(el);
1711 - }, 0);
1712 -});
1713 -
1714 -// Builds the list of overflow-menu items for a given bot.
1715 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1716 -function mxchatGetHeaderMenuItems(botId) {
1717 - var items = [];
1718 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1719 -
1720 - // The `print_button_*` keys still gate this item for back-compat with
1721 - // existing user options. The action is now a transcript download, not print.
1722 - if (settings.print_button_enabled === 'on') {
1723 - items.push({
1724 - id: 'download-transcript',
1725 - label: settings.print_button_label || 'Download Transcript',
1726 - 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>',
1727 - action: function() {
1728 - mxchatDownloadTranscript(botId);
1729 - }
1730 - });
1731 - }
1732 -
1733 - // "Start new chat" — surfaces the EXISTING per-conversation reset
1734 - // (MxChatInstances.resetChatSession) so a visitor can start a fresh thread
1735 - // without the site owner disabling chat persistence globally. Default OFF;
1736 - // gated by the reset_chat_enabled option. plan ac2e81.
1737 - if (settings.reset_chat_enabled === 'on') {
1738 - items.push({
1739 - id: 'reset-chat',
1740 - label: settings.reset_chat_label || 'Start new chat',
1741 - 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>',
1742 - action: function() {
1743 - var confirmMsg = settings.reset_chat_confirm || 'Start a new chat? This clears the current conversation.';
1744 - if (window.confirm(confirmMsg)) {
1745 - MxChatInstances.resetChatSession(botId);
1746 - }
1747 - }
1748 - });
1749 - }
1750 -
1751 - return items;
1752 -}
1753 -
1754 -// Builds a clean markdown transcript of the current conversation and triggers
1755 -// a file download. Used by the "Download Transcript" menu item.
1756 -function mxchatDownloadTranscript(botId) {
1757 - var $chatBox = getElement(botId, 'chat-box');
1758 - if (!$chatBox || !$chatBox.length) return;
1759 -
1760 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1761 - var headerTitle = settings.print_header_title || 'Chat transcript';
1762 - var now = new Date();
1763 - var stamp = now.toLocaleString();
1764 -
1765 - var lines = [];
1766 - lines.push('# ' + headerTitle);
1767 - lines.push('');
1768 - lines.push('Exported: ' + stamp);
1769 - lines.push('');
1770 - lines.push('---');
1771 - lines.push('');
1772 -
1773 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1774 - var $msg = $(this);
1775 - // Skip thinking placeholders and any in-flight temporary messages.
1776 - if ($msg.find('.thinking-dots').length) return;
1777 - if ($msg.hasClass('temporary-message')) return;
1778 -
1779 - var sender;
1780 - if ($msg.hasClass('user-message')) sender = 'User';
1781 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1782 - else sender = 'AI Agent';
1783 -
1784 - // Strip interactive UI from the cloned message so we get the conversation text.
1785 - var $clone = $msg.clone();
1786 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1787 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1788 - if (!text) return;
1789 -
1790 - lines.push('**' + sender + '**');
1791 - lines.push('');
1792 - lines.push(text);
1793 - lines.push('');
1794 - });
1795 -
1796 - var content = lines.join('\n');
1797 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1798 - var fname = 'mxchat-transcript-' + iso + '.md';
1799 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1800 - var url = URL.createObjectURL(blob);
1801 - var a = document.createElement('a');
1802 - a.href = url;
1803 - a.download = fname;
1804 - a.style.display = 'none';
1805 - document.body.appendChild(a);
1806 - a.click();
1807 - setTimeout(function() {
1808 - if (a.parentNode) a.parentNode.removeChild(a);
1809 - URL.revokeObjectURL(url);
1810 - }, 100);
1811 -}
1812 -
1813 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1814 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1815 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1816 -function mxchatSyncMenuColors(botId, $wrap) {
1817 - if (!$wrap || !$wrap.length) return;
1818 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1819 - if (!$bot.length) return;
1820 - var cs = window.getComputedStyle($bot[0]);
1821 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1822 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1823 - }
1824 - // Bot text color usually lives on a child div, not .bot-message itself.
1825 - var $textChild = $bot.find('[style*="color"]').first();
1826 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1827 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1828 -}
1829 -
1830 -// Renders (or re-renders) the item list for one menu wrap. Split out of
1831 -// mxchatInitHeaderMenu so the dynamic-settings merge (plan-32db95) can
1832 -// rebuild items + trigger visibility WITHOUT re-binding the one-time
1833 -// open/close/keyboard wiring. closeMenu is passed in by the init closure;
1834 -// a rebuild before init (never happens, but harmless) just skips it.
1835 -function mxchatRenderHeaderMenuItems(botId, $wrap, closeMenuFn) {
1836 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1837 - var $menu = $wrap.find('.mxchat-header-menu');
1838 - var items = mxchatGetHeaderMenuItems(botId);
1839 -
1840 - $menu.empty();
1841 -
1842 - if (!items.length) {
1843 - $trigger.hide();
1844 - $menu.hide();
1845 - return;
1846 - }
1847 -
1848 - // Clear any inline display:none a previous zero-item render left behind —
1849 - // open/close visibility is governed by the hidden prop + is-open class.
1850 - $trigger.css('display', '');
1851 - $menu.css('display', '');
1852 -
1853 - items.forEach(function(item, idx) {
1854 - var $btn = $('<button>', {
1855 - type: 'button',
1856 - 'class': 'mxchat-menu-item',
1857 - 'role': 'menuitem',
1858 - 'tabindex': '-1',
1859 - 'data-menu-id': item.id,
1860 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1861 - '<span class="mxchat-menu-item-label"></span>'
1862 - });
1863 - $btn.find('.mxchat-menu-item-label').text(item.label);
1864 - $btn.on('click', function(e) {
1865 - e.preventDefault();
1866 - e.stopPropagation();
1867 - if (closeMenuFn) closeMenuFn();
1868 - try { item.action(); } catch (err) { /* no-op */ }
1869 - });
1870 - $menu.append($btn);
1871 - });
1872 -}
1873 -
1874 -// Re-render every menu on the page after a dynamic-settings merge
1875 -// (multi-bot: each wrap re-reads its items). An OPEN menu is left alone —
1876 -// swapping items under the user mid-interaction yanks focus — and the
1877 -// rebuild runs when it closes instead (closeMenu checks the pending flag).
1878 -function mxchatRebuildHeaderMenus() {
1879 - $('.mxchat-header-menu-wrap').each(function() {
1880 - var $wrap = $(this);
1881 - var botId = $wrap.data('bot-id');
1882 - if (!botId) return;
1883 - if (!$wrap.data('mxchatMenuReady')) {
1884 - mxchatInitHeaderMenu(botId);
1885 - return;
1886 - }
1887 - if ($wrap.find('.mxchat-header-menu').hasClass('is-open')) {
1888 - $wrap.data('mxchatMenuRebuildPending', true);
1889 - return;
1890 - }
1891 - mxchatRenderHeaderMenuItems(botId, $wrap, $wrap.data('mxchatMenuClose'));
1892 - });
1893 -}
1894 -
1895 -// One-time per-widget init: renders menu items, wires open/close,
1896 -// outside-click, Escape, and arrow-key navigation. If no items, hides the
1897 -// trigger. Wiring happens even when there are zero items at init, so a
1898 -// later dynamic-settings rebuild that adds items has a working trigger.
1899 -function mxchatInitHeaderMenu(botId) {
1900 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1901 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1902 -
1903 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1904 - var $menu = $wrap.find('.mxchat-header-menu');
1905 -
1906 - // Initial color sync — covers normal page load.
1907 - mxchatSyncMenuColors(botId, $wrap);
1908 -
1909 - function openMenu() {
1910 - // Re-sync each open in case the active theme changed since init.
1911 - mxchatSyncMenuColors(botId, $wrap);
1912 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1913 - $trigger.attr('aria-expanded', 'true');
1914 - // Focus the first item for keyboard users
1915 - setTimeout(function() {
1916 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1917 - }, 0);
1918 - }
1919 - function closeMenu(returnFocus) {
1920 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1921 - $trigger.attr('aria-expanded', 'false');
1922 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1923 - if (returnFocus) $trigger.trigger('focus');
1924 - // A dynamic-settings rebuild that arrived while the menu was open
1925 - // was deferred (mxchatRebuildHeaderMenus) — run it now.
1926 - if ($wrap.data('mxchatMenuRebuildPending')) {
1927 - $wrap.removeData('mxchatMenuRebuildPending');
1928 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1929 - }
1930 - }
1931 -
1932 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1933 - // click-to-collapse handler does not fire.
1934 - $trigger.on('click', function(e) {
1935 - e.preventDefault();
1936 - e.stopPropagation();
1937 - if ($menu.hasClass('is-open')) closeMenu();
1938 - else openMenu();
1939 - });
1940 -
1941 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1942 - $menu.on('click', function(e) {
1943 - e.stopPropagation();
1944 - });
1945 -
1946 - // Outside click closes the menu.
1947 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1948 - if (!$menu.hasClass('is-open')) return;
1949 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1950 - closeMenu();
1951 - });
1952 -
1953 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1954 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1955 - var $items = $menu.find('.mxchat-menu-item');
1956 - var idx = $items.index(this);
1957 - if (e.key === 'Escape') {
1958 - e.preventDefault();
1959 - closeMenu(true);
1960 - } else if (e.key === 'ArrowDown') {
1961 - e.preventDefault();
1962 - var $next = $items.eq((idx + 1) % $items.length);
1963 - $items.attr('tabindex', '-1');
1964 - $next.attr('tabindex', '0').trigger('focus');
1965 - } else if (e.key === 'ArrowUp') {
1966 - e.preventDefault();
1967 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1968 - $items.attr('tabindex', '-1');
1969 - $prev.attr('tabindex', '0').trigger('focus');
1970 - } else if (e.key === 'Enter' || e.key === ' ') {
1971 - e.preventDefault();
1972 - $(this).trigger('click');
1973 - }
1974 - });
1975 - $trigger.on('keydown', function(e) {
1976 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1977 - e.preventDefault();
1978 - closeMenu(true);
1979 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1980 - e.preventDefault();
1981 - openMenu();
1982 - }
1983 - });
1984 -
1985 - // Expose closeMenu for out-of-closure re-renders (mxchatRebuildHeaderMenus),
1986 - // then do the initial item render.
1987 - $wrap.data('mxchatMenuClose', closeMenu);
1988 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1989 -
1990 - $wrap.data('mxchatMenuReady', true);
1991 -}
1992 -
1993 -// Initialize header menus for every rendered widget on DOM ready.
1994 -$(function() {
1995 - $('.mxchat-header-menu-wrap').each(function() {
1996 - var botId = $(this).data('bot-id');
1997 - if (botId) mxchatInitHeaderMenu(botId);
1998 - });
1999 -
2000 - // Embedded (non-floating) widgets are open from the moment the page
2001 - // renders — refresh dynamic settings at init (plan-32db95). Floating
2002 - // widgets refresh on first launcher open instead.
2003 - var hasEmbeddedWidget = $('.mxchat-chatbot-wrapper').filter(function() {
2004 - return !$(this).closest('.floating-chatbot').length;
2005 - }).length > 0;
2006 - if (hasEmbeddedWidget) {
2007 - mxchatRefreshDynamicSettings();
2008 - }
2009 -});
2010 -
1189 +
2011 1190 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
2012 1191 try {
2013 1192 // Determine styles based on sender type
2014 1193 let messageClass, bgColor, fontColor;
@@ -2046,12 +1225,17 @@
2046 1225 'margin-bottom': '1em'
2047 1226 });
2048 1227 }
2049 1228
2050 - // Process the message content - always run linkify to convert markdown
2051 - // links and format text. linkify() handles existing HTML safely via
2052 - // negative lookaheads that skip URLs already inside <a> tags.
2053 - let fullMessage = linkify(messageText);
1229 + // Process the message content based on sender
1230 + let fullMessage;
1231 + if (sender === "user") {
1232 + // For user messages, apply linkify after sanitization
1233 + fullMessage = linkify(messageText);
1234 + } else {
1235 + // For bot/agent messages, preserve HTML
1236 + fullMessage = messageText;
1237 + }
2054 1238
2055 1239 // Add images if provided
2056 1240 if (images && images.length > 0) {
2057 1241 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -2100,12 +1284,8 @@
2100 1284 if (lastUserMessage.length) {
2101 1285 scrollElementToTop(lastUserMessage, botId);
2102 1286 }
2103 1287 }
2104 -
2105 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
2106 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2107 - }
2108 1288 });
2109 1289
2110 1290 if (messageText.id) {
2111 1291 var instance = MxChatInstances.get(botId);
@@ -2190,12 +1370,26 @@
2190 1370 bgColor = botMessageBgColor;
2191 1371 fontColor = botMessageFontColor;
2192 1372 }
2193 1373
2194 - // Always run linkify to convert markdown links and format text.
2195 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
2196 - // to avoid double-processing URLs that are already inside <a> tags).
2197 - var fullMessage = linkify(responseText);
1374 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1375 + // This prevents double-processing of URLs that are already formatted as HTML
1376 + var fullMessage;
1377 + if (sender === "user") {
1378 + // Always linkify user messages (they're plain text)
1379 + fullMessage = linkify(responseText);
1380 + } else {
1381 + // For bot/agent messages, check if HTML already exists
1382 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1383 + responseText.includes('<img') || responseText.includes('<div') ||
1384 + responseText.includes('<p>') || responseText.includes('<br>')) {
1385 + // Response already has HTML, don't process it
1386 + fullMessage = responseText;
1387 + } else {
1388 + // Plain text response, apply linkify
1389 + fullMessage = linkify(responseText);
1390 + }
1391 + }
2198 1392
2199 1393 if (responseHtml) {
2200 1394 // Only add line breaks if there's actual text content before the HTML
2201 1395 if (fullMessage && fullMessage.trim()) {
@@ -2252,12 +1446,8 @@
2252 1446 }
2253 1447
2254 1448 // Re-enable chat input after response is displayed
2255 1449 enableChatInput(botId);
2256 -
2257 - if (sender === "bot" || sender === "agent") {
2258 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2259 - }
2260 1450 } else {
2261 1451 appendMessage(sender, responseText, responseHtml, images, false, botId);
2262 1452 // Re-enable chat input after response is displayed
2263 1453 enableChatInput(botId);
@@ -2266,15 +1456,8 @@
2266 1456
2267 1457
2268 1458 function appendThinkingMessage(botId) {
2269 1459 botId = botId || 'default';
2270 -
2271 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
2272 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
2273 - if (indicator && indicator.textContent === 'Live Agent') {
2274 - return;
2275 - }
2276 -
2277 1460 var $chatBox = getElement(botId, 'chat-box');
2278 1461
2279 1462 // Remove any existing thinking dots in this bot's chat first
2280 1463 $chatBox.find('.thinking-dots').remove();
@@ -2296,9 +1479,9 @@
2296 1479 '</div>' +
2297 1480 '</div>';
2298 1481
2299 1482 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
2300 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1483 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
2301 1484 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
2302 1485 scrollToBottom(botId);
2303 1486 }
2304 1487
@@ -2304,11 +1487,9 @@
2304 1487
2305 1488 function removeThinkingDots(botId) {
2306 1489 botId = botId || 'default';
2307 1490 var $chatBox = getElement(botId, 'chat-box');
2308 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
2309 1491 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
2310 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
2311 1492 }
2312 1493
2313 1494 // ====================================
2314 1495 // TEXT FORMATTING & PROCESSING
@@ -2342,12 +1523,9 @@
2342 1523 processedText = formatTextStyling(processedText);
2343 1524
2344 1525 // Process code blocks BEFORE processing links
2345 1526 processedText = formatCodeBlocks(processedText);
2346 -
2347 - // Process markdown tables BEFORE converting newlines to paragraphs
2348 - processedText = formatMarkdownTables(processedText);
2349 -
1527 +
2350 1528 // NOW convert to paragraphs
2351 1529 processedText = convertNewlinesToBreaks(processedText);
2352 1530
2353 1531 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -2360,63 +1538,37 @@
2360 1538 // Return as a proper link without the brackets
2361 1539 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2362 1540 });
2363 1541
2364 - // Process markdown links: [text](url) and [](url)
2365 - // Uses balanced parenthesis matching to handle URLs containing parens
2366 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
2367 - processedText = (function(input) {
2368 - var result = '';
2369 - var i = 0;
2370 - while (i < input.length) {
2371 - // Look for [ at current position
2372 - if (input[i] === '[') {
2373 - // Find closing ]
2374 - var closeBracket = input.indexOf(']', i + 1);
2375 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
2376 - result += input[i];
2377 - i++;
2378 - continue;
2379 - }
2380 - var linkText = input.substring(i + 1, closeBracket);
2381 - // Check if URL starts with http
2382 - var urlStart = closeBracket + 2;
2383 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
2384 - result += input[i];
2385 - i++;
2386 - continue;
2387 - }
2388 - // Find balanced closing paren
2389 - var depth = 1;
2390 - var j = urlStart;
2391 - while (j < input.length && depth > 0) {
2392 - if (input[j] === '(') depth++;
2393 - else if (input[j] === ')') depth--;
2394 - if (depth > 0) j++;
2395 - }
2396 - if (depth !== 0) {
2397 - result += input[i];
2398 - i++;
2399 - continue;
2400 - }
2401 - var url = input.substring(urlStart, j);
2402 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
2403 - var encodedUrl = safeEncodeUrl(cleanUrl);
2404 - if (!linkText || !linkText.trim()) {
2405 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
2406 - } else {
2407 - var safeText = sanitizeUserInput(linkText);
2408 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
2409 - }
2410 - i = j + 1; // Skip past the closing )
2411 - } else {
2412 - result += input[i];
2413 - i++;
2414 - }
1542 + // Process proper markdown links with text: [text](url)
1543 + // This MUST have non-empty text in the first brackets
1544 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1545 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1546 + // Make sure we have actual text (not just whitespace)
1547 + if (!text || !text.trim()) {
1548 + // If no text, treat the URL as the text
1549 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1550 + const safeUrl = safeEncodeUrl(cleanUrl);
1551 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2415 1552 }
2416 - return result;
2417 - })(processedText);
1553 +
1554 + // Clean the URL
1555 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1556 + const safeUrl = safeEncodeUrl(cleanUrl);
1557 + const safeText = sanitizeUserInput(text);
1558 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1559 + });
2418 1560
1561 + // Handle empty markdown links: [](url)
1562 + // This is a specific case where there's no text
1563 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1564 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1565 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1566 + const safeUrl = safeEncodeUrl(cleanUrl);
1567 + // Use the URL itself as the link text
1568 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1569 + });
1570 +
2419 1571 // Process phone numbers: [text](tel:number)
2420 1572 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
2421 1573 processedText = processedText.replace(phonePattern, (match, text, phone) => {
2422 1574 const safePhone = safeEncodeUrl(phone);
@@ -2568,78 +1720,9 @@
2568 1720 });
2569 1721
2570 1722 return text;
2571 1723 }
2572 -
2573 - function formatMarkdownTables(text) {
2574 - var lines = text.split('\n');
2575 - var result = [];
2576 - var i = 0;
2577 -
2578 - while (i < lines.length) {
2579 - // Check for a table: current line has pipes AND next line is a separator row
2580 - if (i + 1 < lines.length &&
2581 - lines[i].indexOf('|') !== -1 &&
2582 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2583 -
2584 - var tableLines = [];
2585 - var headerLine = lines[i];
2586 - var separatorLine = lines[i + 1];
2587 - tableLines.push(headerLine);
2588 - tableLines.push(separatorLine);
2589 -
2590 - // Collect remaining table rows
2591 - var j = i + 2;
2592 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2593 - tableLines.push(lines[j]);
2594 - j++;
2595 - }
2596 -
2597 - // Parse alignment from separator row
2598 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2599 - var alignments = sepCells.map(function(cell) {
2600 - var trimmed = cell.trim();
2601 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2602 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2603 - return 'left';
2604 - });
2605 -
2606 - // Build HTML table
2607 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2608 -
2609 - // Header row
2610 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2611 - html += '<thead><tr>';
2612 - headerCells.forEach(function(cell, idx) {
2613 - var align = alignments[idx] || 'left';
2614 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2615 - });
2616 - html += '</tr></thead>';
2617 -
2618 - // Body rows
2619 - html += '<tbody>';
2620 - for (var r = 2; r < tableLines.length; r++) {
2621 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2622 - html += '<tr>';
2623 - rowCells.forEach(function(cell, idx) {
2624 - var align = alignments[idx] || 'left';
2625 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2626 - });
2627 - html += '</tr>';
2628 - }
2629 - html += '</tbody></table></div>';
2630 -
2631 - result.push(html);
2632 - i = j;
2633 - } else {
2634 - result.push(lines[i]);
2635 - i++;
2636 - }
2637 - }
2638 -
2639 - return result.join('\n');
2640 - }
2641 -
1724 +
2642 1725 function sanitizeUserInput(text) {
2643 1726 const div = document.createElement('div');
2644 1727 div.textContent = text;
2645 1728 return div.innerHTML;
@@ -2710,14 +1793,13 @@
2710 1793 requestAnimationFrame(smoothScroll);
2711 1794 }
2712 1795 }
2713 1796
2714 - function scrollElementToTop(element, botId, topOffset) {
1797 + function scrollElementToTop(element, botId) {
2715 1798 botId = botId || 'default';
2716 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2717 1799 var chatBox = getElement(botId, 'chat-box');
2718 1800 var elementTop = element.position().top + chatBox.scrollTop();
2719 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1801 + chatBox.animate({ scrollTop: elementTop }, 500);
2720 1802 }
2721 1803
2722 1804 function showChatWidget(botId) {
2723 1805 botId = botId || 'default';
@@ -2861,12 +1943,15 @@
2861 1943 // LIVE AGENT FUNCTIONALITY
2862 1944 // ====================================
2863 1945
2864 1946 function startPolling(botId) {
1947 + console.log('[MxChat] startPolling called for botId:', botId);
2865 1948 botId = botId || 'default';
2866 1949 var instance = MxChatInstances.get(botId);
2867 1950 // Clear any existing interval first
2868 1951 stopPolling(botId);
1952 + // Start new polling interval
1953 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2869 1954 instance.pollingInterval = setInterval(function() {
2870 1955 checkForAgentMessages(botId);
2871 1956 }, 5000);
2872 1957 }
@@ -2871,17 +1956,20 @@
2871 1956 }, 5000);
2872 1957 }
2873 1958
2874 1959 function stopPolling(botId) {
1960 + console.log('[MxChat] stopPolling called for botId:', botId);
2875 1961 botId = botId || 'default';
2876 1962 var instance = MxChatInstances.get(botId);
2877 1963 if (instance.pollingInterval) {
2878 1964 clearInterval(instance.pollingInterval);
2879 1965 instance.pollingInterval = null;
1966 + console.log('[MxChat] Polling stopped for botId:', botId);
2880 1967 }
2881 1968 }
2882 1969
2883 1970 function checkForAgentMessages(botId) {
1971 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2884 1972 botId = botId || 'default';
2885 1973 var instance = MxChatInstances.get(botId);
2886 1974 const sessionId = getChatSession(botId);
2887 1975 $.ajax({
@@ -2907,12 +1995,8 @@
2907 1995 instance.processedMessageIds.add(message.id);
2908 1996 }
2909 1997 });
2910 1998
2911 - if (hasNewMessage) {
2912 - enableChatInput(botId);
2913 - }
2914 -
2915 1999 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2916 2000 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2917 2001 showNotification(botId);
2918 2002 }
@@ -2918,13 +2002,8 @@
2918 2002 }
2919 2003
2920 2004 scrollToBottom(botId, true);
2921 2005 }
2922 -
2923 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2924 - if (response.success && response.data?.chat_mode) {
2925 - updateChatModeIndicator(response.data.chat_mode, botId);
2926 - }
2927 2006 },
2928 2007 error: function (xhr, status, error) {
2929 2008 // Polling error - silently continue
2930 2009 }
@@ -2934,29 +2013,20 @@
2934 2013 // ====================================
2935 2014 // CHAT HISTORY & PERSISTENCE
2936 2015 // ====================================
2937 2016
2938 -function loadChatHistory(botId, onComplete) {
2017 +function loadChatHistory(botId) {
2939 2018 botId = botId || 'default';
2940 2019 var instance = MxChatInstances.get(botId);
2941 2020
2942 2021 // Prevent duplicate loading
2943 2022 if (instance.chatHistoryLoaded) {
2944 - if (onComplete) onComplete();
2945 2023 return;
2946 2024 }
2947 2025
2948 - // Use getChatSession which returns null if no session exists (does NOT create one)
2949 2026 var sessionId = getChatSession(botId);
2950 2027 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2951 2028
2952 - // No session yet — nothing to load. History will load after first message via ensureSession.
2953 - if (!sessionId) {
2954 - instance.chatHistoryLoaded = true;
2955 - if (onComplete) onComplete();
2956 - return;
2957 - }
2958 -
2959 2029 if (chatPersistenceEnabled && sessionId) {
2960 2030 $.ajax({
2961 2031 url: mxchatChat.ajax_url,
2962 2032 type: 'POST',
@@ -2967,12 +2037,11 @@
2967 2037 },
2968 2038 success: function(response) {
2969 2039 // Handle session reset (IP changed while user was away)
2970 2040 if (response.success === false && response.data && response.data.action === 'reset_session') {
2971 - // Silent reset — new session but don't clear UI
2972 - MxChatInstances.silentResetSession(botId);
2041 + // Silently reset session - user will start fresh
2042 + resetChatSession(botId);
2973 2043 instance.chatHistoryLoaded = true; // Prevent retry loop
2974 - if (onComplete) onComplete();
2975 2044 return;
2976 2045 }
2977 2046
2978 2047 // Check if the response indicates success
@@ -3028,20 +2097,9 @@
3028 2097 var content = message.content;
3029 2098 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
3030 2099 content = decodeHTMLEntities(content);
3031 2100
3032 - // Skip linkify for messages containing structured HTML
3033 - // (forms, product cards, galleries, etc.) to avoid
3034 - // markdown formatting corrupting HTML attributes
3035 - // (e.g. underscores in name="field_name" becoming <em> tags)
3036 - if (content.includes("mxchat-product-card") ||
3037 - content.includes("mxchat-image-gallery") ||
3038 - content.includes("mxchat-featured-products") ||
3039 - content.includes("mxchat-youtube-embed") ||
3040 - content.includes("<form") ||
3041 - content.includes("<input") ||
3042 - content.includes("<select") ||
3043 - content.includes("<textarea")) {
2101 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
3044 2102 messageElement.html(content);
3045 2103 } else {
3046 2104 var formattedContent = linkify(content);
3047 2105 messageElement.html(formattedContent);
@@ -3081,17 +2139,13 @@
3081 2139 instance.chatHistoryLoaded = true;
3082 2140 }
3083 2141 }
3084 2142 }
3085 - if (onComplete) onComplete();
3086 2143 },
3087 2144 error: function(xhr, status, error) {
3088 2145 // Error loading chat history - silently continue
3089 - if (onComplete) onComplete();
3090 2146 }
3091 2147 });
3092 - } else {
3093 - if (onComplete) onComplete();
3094 2148 }
3095 2149 }
3096 2150
3097 2151
@@ -3155,10 +2209,10 @@
3155 2209 .then(data => {
3156 2210 if (data.success) {
3157 2211 container.style.display = 'none';
3158 2212 nameElement.textContent = '';
3159 - instance.activePdfFile = null;
3160 - appendMessage('bot', 'PDF removed.', '', [], false, botId);
2213 + activePdfFile = null;
2214 + appendMessage('bot', 'PDF removed.');
3161 2215 }
3162 2216 })
3163 2217 .catch(error => {
3164 2218 // Error removing PDF - silently continue
@@ -3164,16 +2218,14 @@
3164 2218 // Error removing PDF - silently continue
3165 2219 });
3166 2220 }
3167 2221
3168 - function removeActiveWord(botId) {
3169 - botId = botId || 'default';
3170 - var instance = MxChatInstances.get(botId);
3171 - const container = getElementDOM(botId, 'active-word-container');
3172 - const nameElement = getElementDOM(botId, 'active-word-name');
3173 -
3174 - if (!container || !nameElement || !instance.activeWordFile) return;
3175 -
2222 + function removeActiveWord() {
2223 + const container = document.getElementById('active-word-container');
2224 + const nameElement = document.getElementById('active-word-name');
2225 +
2226 + if (!container || !nameElement || !activeWordFile) return;
2227 +
3176 2228 fetch(mxchatChat.ajax_url, {
3177 2229 method: 'POST',
3178 2230 headers: {
3179 2231 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3179,9 +2231,9 @@
3179 2231 'Content-Type': 'application/x-www-form-urlencoded',
3180 2232 },
3181 2233 body: new URLSearchParams({
3182 2234 'action': 'mxchat_remove_word',
3183 - 'session_id': getChatSession(botId),
2235 + 'session_id': sessionId,
3184 2236 'nonce': mxchatChat.nonce
3185 2237 })
3186 2238 })
3187 2239 .then(response => response.json())
@@ -3188,10 +2240,10 @@
3188 2240 .then(data => {
3189 2241 if (data.success) {
3190 2242 container.style.display = 'none';
3191 2243 nameElement.textContent = '';
3192 - instance.activeWordFile = null;
3193 - appendMessage('bot', 'Word document removed.', '', [], false, botId);
2244 + activeWordFile = null;
2245 + appendMessage('bot', 'Word document removed.');
3194 2246 }
3195 2247 })
3196 2248 .catch(error => {
3197 2249 // Error removing Word document - silently continue
@@ -3269,35 +2321,45 @@
3269 2321 // ====================================
3270 2322
3271 2323 function checkPreChatDismissal(botId) {
3272 2324 botId = botId || 'default';
3273 - try {
3274 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
3275 - if (dismissedAt) {
3276 - // Re-show after 24 hours
3277 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
3278 - if (elapsed < 86400000) {
2325 + $.ajax({
2326 + url: mxchatChat.ajax_url,
2327 + type: 'POST',
2328 + data: {
2329 + action: 'mxchat_check_pre_chat_message_status',
2330 + _ajax_nonce: mxchatChat.nonce
2331 + },
2332 + success: function(response) {
2333 + if (response.success && !response.data.dismissed) {
2334 + getElement(botId, 'pre-chat-message').fadeIn(250);
2335 + } else {
3279 2336 getElement(botId, 'pre-chat-message').hide();
3280 - return;
3281 2337 }
3282 - // Expired — clear and show again
3283 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2338 + },
2339 + error: function() {
2340 + // Error checking pre-chat dismissal - silently continue
3284 2341 }
3285 - getElement(botId, 'pre-chat-message').fadeIn(250);
3286 - } catch (e) {
3287 - // localStorage unavailable — show the message
3288 - getElement(botId, 'pre-chat-message').fadeIn(250);
3289 - }
2342 + });
3290 2343 }
3291 2344
3292 2345 function handlePreChatDismissal(botId) {
3293 2346 botId = botId || 'default';
3294 2347 getElement(botId, 'pre-chat-message').fadeOut(200);
3295 - try {
3296 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
3297 - } catch (e) {
3298 - // localStorage unavailable — dismissal won't persist
3299 - }
2348 + $.ajax({
2349 + url: mxchatChat.ajax_url,
2350 + type: 'POST',
2351 + data: {
2352 + action: 'mxchat_dismiss_pre_chat_message',
2353 + _ajax_nonce: mxchatChat.nonce
2354 + },
2355 + success: function() {
2356 + $('#pre-chat-message').hide();
2357 + },
2358 + error: function() {
2359 + // Error dismissing pre-chat message - silently continue
2360 + }
2361 + });
3300 2362 }
3301 2363
3302 2364
3303 2365 // ====================================
@@ -3350,38 +2412,11 @@
3350 2412 e.stopPropagation();
3351 2413 var botId = getBotIdFromElement(this);
3352 2414 collapseQuickQuestions(botId);
3353 2415 });
3354 -
3355 -// Consent-safe YouTube embed (plan 03ba33): the server only ever ships a
3356 -// thumbnail facade — no Google iframe exists until the visitor taps play.
3357 -// Delegated so it also works for embeds restored from chat history.
3358 -$(document).on('click', '.mxchat-youtube-embed .mxchat-youtube-facade', function(e) {
3359 - e.preventDefault();
3360 - var $wrap = $(this).closest('.mxchat-youtube-embed');
3361 - var videoId = String($wrap.data('video-id') || '').replace(/[^A-Za-z0-9_-]/g, '');
3362 - if (!videoId) {
3363 - return;
3364 - }
3365 - var title = $wrap.find('.mxchat-youtube-title').text() || 'YouTube video';
3366 - var $iframe = $('<iframe>', {
3367 - src: 'https://www.youtube-nocookie.com/embed/' + videoId + '?autoplay=1&rel=0',
3368 - title: title,
3369 - allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
3370 - allowfullscreen: true,
3371 - frameborder: 0
3372 - }).addClass('mxchat-youtube-iframe');
3373 - $wrap.addClass('mxchat-youtube-playing');
3374 - $(this).replaceWith($iframe);
3375 -});
3376 2416
3377 2417 // Chatbot visibility toggle handlers - use class selector for multi-instance support
3378 - // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
3379 - $(document).on('click keydown', '.floating-chatbot-button', function(e) {
3380 - if (e.type === 'keydown') {
3381 - if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
3382 - e.preventDefault();
3383 - }
2418 + $(document).on('click', '.floating-chatbot-button', function() {
3384 2419 var botId = getBotIdFromElement(this);
3385 2420 var $chatbot = getElement(botId, 'floating-chatbot');
3386 2421 var $badge = getElement(botId, 'chat-notification-badge');
3387 2422 var $preChat = getElement(botId, 'pre-chat-message');
@@ -3386,88 +2421,35 @@
3386 2421 var $badge = getElement(botId, 'chat-notification-badge');
3387 2422 var $preChat = getElement(botId, 'pre-chat-message');
3388 2423
3389 2424 if ($chatbot.hasClass('hidden')) {
3390 - $chatbot.removeClass('hidden').addClass('visible')
3391 - .attr('aria-modal', 'true').attr('role', 'dialog');
3392 - $(this).addClass('hidden').attr('aria-expanded', 'true');
2425 + $chatbot.removeClass('hidden').addClass('visible');
2426 + $(this).addClass('hidden');
3393 2427 $badge.hide(); // Hide notification when opening chat
3394 2428 disableScroll();
3395 2429 $preChat.fadeOut(250);
3396 -
3397 - // First open per page load: re-fetch behavior settings in case
3398 - // this page's inline values came from a stale full-page cache
3399 - // (plan-32db95). Idempotent — later opens are a no-op.
3400 - mxchatRefreshDynamicSettings();
3401 -
3402 - // Load chat history for returning visitors (persistence)
3403 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3404 - if (chatPersistenceEnabled) {
3405 - MxChatInstances.ensureSession(botId);
3406 - }
3407 -
3408 - // Deferred email check — only on first widget open
3409 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3410 - var instance = MxChatInstances.get(botId);
3411 - if (emailBlocker && !instance.emailCheckDone) {
3412 - instance.emailCheckDone = true;
3413 - resolveEmailState(botId);
3414 - } else if (!emailBlocker) {
3415 - // No email collection — still route through showChatContainerForBot
3416 - // so the loader is shown while chat history loads
3417 - showChatContainerForBot(botId);
3418 - }
3419 -
3420 - // Move keyboard focus into the message input after the open transition.
3421 - setTimeout(function() {
3422 - var chatInput = getElementDOM(botId, 'chat-input');
3423 - if (chatInput && !chatInput.disabled) {
3424 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
3425 - }
3426 - }, 300);
3427 2430 } else {
3428 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3429 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2431 + $chatbot.removeClass('visible').addClass('hidden');
2432 + $(this).removeClass('hidden');
3430 2433 enableScroll();
3431 2434 checkPreChatDismissal(botId);
3432 2435 }
3433 2436 });
3434 2437
3435 - // Allow clicking anywhere on the title bar to close the chatbot.
3436 - // Returns keyboard focus to the launcher so keyboard users don't get
3437 - // stranded at <body> (WCAG SC 2.4.3 Focus Order). :focus-visible is
3438 - // heuristic-based so mouse-triggered close won't show a focus ring.
2438 + // Allow clicking anywhere on the title bar to close the chatbot
3439 2439 $(document).on('click', '.chatbot-top-bar', function() {
3440 2440 var botId = getBotIdFromElement(this);
3441 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3442 - var $launcher = getElement(botId, 'floating-chatbot-button');
3443 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
2441 + getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2442 + getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3444 2443 enableScroll();
3445 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3446 2444 });
3447 2445
3448 - // Global Escape-key handler — closes any visible chat widget and
3449 - // returns focus to its launcher. Standard modal-dismissal pattern;
3450 - // pairs with aria-modal="true" set on the widget when it opens.
3451 - $(document).on('keydown', function(e) {
3452 - if (e.key !== 'Escape' && e.key !== 'Esc') return;
3453 - var $visible = $('.floating-chatbot.visible');
3454 - if (!$visible.length) return;
3455 - e.preventDefault();
3456 - $visible.each(function() {
3457 - var botId = getBotIdFromElement(this);
3458 - $(this).addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3459 - var $launcher = getElement(botId, 'floating-chatbot-button');
3460 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
3461 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3462 - });
3463 - enableScroll();
3464 - });
3465 -
3466 2446 $(document).on('click', '.close-pre-chat-message', function(e) {
3467 2447 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3468 2448 var botId = getBotIdFromElement(this);
3469 - handlePreChatDismissal(botId);
2449 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2450 + $(this).remove();
2451 + });
3470 2452 });
3471 2453
3472 2454
3473 2455 // PDF upload button handlers - use class selector
@@ -3483,20 +2465,17 @@
3483 2465 var wordInput = getElementDOM(botId, 'word-upload');
3484 2466 if (wordInput) wordInput.click();
3485 2467 });
3486 2468
3487 - // PDF file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'pdf-upload')
3488 - $(document).on('change', '.pdf-upload', async function(e) {
3489 - var botId = getBotIdFromElement(this);
3490 - var instance = MxChatInstances.get(botId);
3491 - const file = this.files[0];
3492 - const sessionId = MxChatInstances.ensureSession(botId);
3493 -
2469 + // PDF file input change handler
2470 + addSafeEventListener('pdf-upload', 'change', async function(e) {
2471 + const file = e.target.files[0];
2472 +
3494 2473 if (!file || file.type !== 'application/pdf') {
3495 2474 alert('Please select a valid PDF file.');
3496 2475 return;
3497 2476 }
3498 -
2477 +
3499 2478 if (!sessionId) {
3500 2479 alert('Error: No session ID found');
3501 2480 return;
3502 2481 }
@@ -3504,49 +2483,47 @@
3504 2483 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3505 2484 alert('Error: Ajax configuration missing');
3506 2485 return;
3507 2486 }
3508 -
2487 +
3509 2488 // Disable buttons and show loading state
3510 - const uploadBtn = getElementDOM(botId, 'pdf-upload-btn');
3511 - const sendBtn = getElementDOM(botId, 'send-button');
3512 - if (!uploadBtn) return;
2489 + const uploadBtn = document.getElementById('pdf-upload-btn');
2490 + const sendBtn = document.getElementById('send-button');
3513 2491 const originalBtnContent = uploadBtn.innerHTML;
3514 -
2492 +
3515 2493 try {
3516 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3517 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3518 2494 const formData = new FormData();
3519 2495 formData.append('action', 'mxchat_upload_pdf');
3520 2496 formData.append('pdf_file', file);
3521 2497 formData.append('session_id', sessionId);
3522 2498 formData.append('nonce', mxchatChat.nonce);
3523 -
2499 +
3524 2500 uploadBtn.disabled = true;
3525 - if (sendBtn) sendBtn.disabled = true;
2501 + sendBtn.disabled = true;
3526 2502 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3527 2503 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3528 2504 </svg>`;
3529 -
2505 +
3530 2506 const response = await fetch(mxchatChat.ajax_url, {
3531 2507 method: 'POST',
3532 2508 body: formData
3533 2509 });
3534 -
2510 +
3535 2511 const data = await response.json();
3536 -
2512 +
3537 2513 if (data.success) {
3538 2514 // Hide popular questions if they exist
3539 - if (hasQuickQuestions(botId)) {
3540 - collapseQuickQuestions(botId);
2515 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2516 + if (hasQuickQuestions()) {
2517 + collapseQuickQuestions();
3541 2518 }
3542 -
2519 +
3543 2520 // Show the active PDF name
3544 - showActivePdf(data.data.filename, botId);
3545 -
3546 - appendMessage('bot', data.data.message, '', [], false, botId);
3547 - scrollToBottom(botId);
3548 - instance.activePdfFile = data.data.filename;
2521 + showActivePdf(data.data.filename);
2522 +
2523 + appendMessage('bot', data.data.message);
2524 + scrollToBottom();
2525 + activePdfFile = data.data.filename;
3549 2526 } else {
3550 2527 alert('Failed to upload PDF. Please try again.');
3551 2528 }
3552 2529 } catch (error) {
@@ -3552,76 +2529,66 @@
3552 2529 } catch (error) {
3553 2530 alert('Error uploading file. Please try again.');
3554 2531 } finally {
3555 2532 uploadBtn.disabled = false;
3556 - if (sendBtn) sendBtn.disabled = false;
2533 + sendBtn.disabled = false;
3557 2534 uploadBtn.innerHTML = originalBtnContent;
3558 2535 this.value = ''; // Reset file input
3559 2536 }
3560 2537 });
3561 2538
3562 - // Word file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'word-upload')
3563 - $(document).on('change', '.word-upload', async function(e) {
3564 - var botId = getBotIdFromElement(this);
3565 - var instance = MxChatInstances.get(botId);
3566 - const file = this.files[0];
3567 - const sessionId = MxChatInstances.ensureSession(botId);
3568 -
2539 + // Word file input change handler
2540 + addSafeEventListener('word-upload', 'change', async function(e) {
2541 + const file = e.target.files[0];
2542 +
3569 2543 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
3570 2544 alert('Please select a valid Word document (.docx).');
3571 2545 return;
3572 2546 }
3573 -
2547 +
3574 2548 if (!sessionId) {
3575 2549 alert('Error: No session ID found');
3576 2550 return;
3577 2551 }
3578 2552
3579 - if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3580 - alert('Error: Ajax configuration missing');
3581 - return;
3582 - }
3583 -
3584 2553 // Disable buttons and show loading state
3585 - const uploadBtn = getElementDOM(botId, 'word-upload-btn');
3586 - const sendBtn = getElementDOM(botId, 'send-button');
3587 - if (!uploadBtn) return;
2554 + const uploadBtn = document.getElementById('word-upload-btn');
2555 + const sendBtn = document.getElementById('send-button');
3588 2556 const originalBtnContent = uploadBtn.innerHTML;
3589 -
2557 +
3590 2558 try {
3591 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3592 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3593 2559 const formData = new FormData();
3594 2560 formData.append('action', 'mxchat_upload_word');
3595 2561 formData.append('word_file', file);
3596 2562 formData.append('session_id', sessionId);
3597 2563 formData.append('nonce', mxchatChat.nonce);
3598 -
2564 +
3599 2565 uploadBtn.disabled = true;
3600 - if (sendBtn) sendBtn.disabled = true;
2566 + sendBtn.disabled = true;
3601 2567 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3602 2568 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3603 2569 </svg>`;
3604 -
2570 +
3605 2571 const response = await fetch(mxchatChat.ajax_url, {
3606 2572 method: 'POST',
3607 2573 body: formData
3608 2574 });
3609 -
2575 +
3610 2576 const data = await response.json();
3611 -
2577 +
3612 2578 if (data.success) {
3613 2579 // Hide popular questions if they exist
3614 - if (hasQuickQuestions(botId)) {
3615 - collapseQuickQuestions(botId);
2580 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2581 + if (hasQuickQuestions()) {
2582 + collapseQuickQuestions();
3616 2583 }
3617 -
2584 +
3618 2585 // Show the active Word document name
3619 - showActiveWord(data.data.filename, botId);
3620 -
3621 - appendMessage('bot', data.data.message, '', [], false, botId);
3622 - scrollToBottom(botId);
3623 - instance.activeWordFile = data.data.filename;
2586 + showActiveWord(data.data.filename);
2587 +
2588 + appendMessage('bot', data.data.message);
2589 + scrollToBottom();
2590 + activeWordFile = data.data.filename;
3624 2591 } else {
3625 2592 alert('Failed to upload Word document. Please try again.');
3626 2593 }
3627 2594 } catch (error) {
@@ -3627,25 +2594,25 @@
3627 2594 } catch (error) {
3628 2595 alert('Error uploading file. Please try again.');
3629 2596 } finally {
3630 2597 uploadBtn.disabled = false;
3631 - if (sendBtn) sendBtn.disabled = false;
2598 + sendBtn.disabled = false;
3632 2599 uploadBtn.innerHTML = originalBtnContent;
3633 2600 this.value = ''; // Reset file input
3634 2601 }
3635 2602 });
3636 2603
3637 - // Remove button click handlers - delegated, bot-aware (were bound to stale un-suffixed ids)
3638 - $(document).on('click', '.remove-pdf-btn', function(e) {
2604 + // Remove button click handlers
2605 + document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
3639 2606 e.preventDefault();
3640 2607 e.stopPropagation();
3641 - removeActivePdf(getBotIdFromElement(this));
2608 + removeActivePdf();
3642 2609 });
3643 -
3644 - $(document).on('click', '.remove-word-btn', function(e) {
2610 +
2611 + document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
3645 2612 e.preventDefault();
3646 2613 e.stopPropagation();
3647 - removeActiveWord(getBotIdFromElement(this));
2614 + removeActiveWord();
3648 2615 });
3649 2616
3650 2617 // Window resize handlers
3651 2618 $(window).on('resize orientationchange', function() {
@@ -3683,59 +2650,8 @@
3683 2650 });
3684 2651
3685 2652
3686 2653 // ====================================
3687 -// INIT LOADER & CHAT CONTAINER HELPERS
3688 -// ====================================
3689 -// These must be outside the email collection block so they're always available
3690 -// (used by persistence loading even when email collection is off)
3691 -
3692 -function showInitLoader(botId) {
3693 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3694 - if (loader) loader.style.display = 'flex';
3695 -}
3696 -
3697 -function hideInitLoader(botId) {
3698 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3699 - if (loader) loader.style.display = 'none';
3700 -}
3701 -
3702 -function showEmailFormForBot(botId) {
3703 - hideInitLoader(botId);
3704 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3705 - var chatContainer = getElementDOM(botId, 'chat-container');
3706 - if (emailBlocker) emailBlocker.style.display = 'flex';
3707 - if (chatContainer) chatContainer.style.display = 'none';
3708 -}
3709 -
3710 -function showChatContainerForBot(botId) {
3711 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3712 - var chatContainer = getElementDOM(botId, 'chat-container');
3713 - if (emailBlocker) emailBlocker.style.display = 'none';
3714 -
3715 - var instance = MxChatInstances.get(botId);
3716 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3717 -
3718 - // If persistence is on and history hasn't loaded yet, show loader
3719 - // while history loads to prevent flash of empty chat
3720 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3721 - if (chatContainer) chatContainer.style.display = 'none';
3722 - showInitLoader(botId);
3723 - loadChatHistory(botId, function() {
3724 - hideInitLoader(botId);
3725 - if (chatContainer) chatContainer.style.display = 'flex';
3726 - scrollToBottom(botId, true);
3727 - });
3728 - } else {
3729 - hideInitLoader(botId);
3730 - if (chatContainer) chatContainer.style.display = 'flex';
3731 - if (typeof loadChatHistory === 'function') {
3732 - loadChatHistory(botId);
3733 - }
3734 - }
3735 -}
3736 -
3737 -// ====================================
3738 2654 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3739 2655 // ====================================
3740 2656 // Only run email collection setup if it's enabled
3741 2657 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3771,8 +2687,28 @@
3771 2687 `;
3772 2688 document.head.appendChild(style);
3773 2689 }
3774 2690
2691 + // Helper functions for email collection (multi-instance aware)
2692 + function showEmailFormForBot(botId) {
2693 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2694 + var chatContainer = getElementDOM(botId, 'chat-container');
2695 + if (emailBlocker) emailBlocker.style.display = 'flex';
2696 + if (chatContainer) chatContainer.style.display = 'none';
2697 + }
2698 +
2699 + function showChatContainerForBot(botId) {
2700 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2701 + var chatContainer = getElementDOM(botId, 'chat-container');
2702 + if (emailBlocker) emailBlocker.style.display = 'none';
2703 + if (chatContainer) chatContainer.style.display = 'flex';
2704 +
2705 + // Load chat history for this bot
2706 + if (typeof loadChatHistory === 'function') {
2707 + loadChatHistory(botId);
2708 + }
2709 + }
2710 +
3775 2711 function isValidEmailAddress(email) {
3776 2712 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3777 2713 return emailRegex.test(email.trim()) && email.length <= 254;
3778 2714 }
@@ -3894,31 +2830,11 @@
3894 2830 existingErrors.forEach(error => error.remove());
3895 2831 }
3896 2832 }
3897 2833
3898 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3899 - function resolveEmailState(botId) {
3900 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3901 - if (mxchatChat.initial_email_state.show_email_form) {
3902 - showEmailFormForBot(botId);
3903 - } else {
3904 - showChatContainerForBot(botId);
3905 - }
3906 - } else {
3907 - checkSessionAndEmailForBot(botId);
3908 - }
3909 - }
3910 -
3911 2834 function checkSessionAndEmailForBot(botId) {
3912 - const sessionId = MxChatInstances.ensureSession(botId);
2835 + const sessionId = getChatSession(botId);
3913 2836
3914 - // Hide both panels while we check — show loader instead
3915 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3916 - var chatContainer = getElementDOM(botId, 'chat-container');
3917 - if (emailBlocker) emailBlocker.style.display = 'none';
3918 - if (chatContainer) chatContainer.style.display = 'none';
3919 - showInitLoader(botId);
3920 -
3921 2837 fetch(mxchatChat.ajax_url, {
3922 2838 method: 'POST',
3923 2839 headers: {
3924 2840 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3966,9 +2882,9 @@
3966 2882 var emailInput = getElementDOM(botId, 'user-email');
3967 2883 var nameInput = getElementDOM(botId, 'user-name');
3968 2884 var userEmail = emailInput ? emailInput.value.trim() : '';
3969 2885 var userName = nameInput ? nameInput.value.trim() : '';
3970 - var sessionId = MxChatInstances.ensureSession(botId);
2886 + var sessionId = getChatSession(botId);
3971 2887
3972 2888 // Validate email
3973 2889 if (!userEmail) {
3974 2890 showEmailError(botId, 'Please enter your email address.');
@@ -4091,27 +3007,25 @@
4091 3007 }
4092 3008 });
4093 3009
4094 3010 // Initialize email check for all bot instances
4095 - // For floating bots: defer until widget is opened (zero passive AJAX)
4096 - // For embedded bots: check immediately since the form is visible
4097 3011 $('.mxchat-chatbot-wrapper').each(function() {
4098 3012 var botId = $(this).data('bot-id') || 'default';
4099 3013 var emailBlocker = getElementDOM(botId, 'email-blocker');
4100 3014
3015 + // Only check if email blocker exists for this bot
4101 3016 if (emailBlocker) {
4102 - if (isEmbeddedBot(botId)) {
4103 - // Embedded bots are always visible — check now
4104 - resolveEmailState(botId);
3017 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3018 + if (mxchatChat.initial_email_state.show_email_form) {
3019 + showEmailFormForBot(botId);
3020 + } else {
3021 + showChatContainerForBot(botId);
3022 + }
3023 + } else {
3024 + setTimeout(function() {
3025 + checkSessionAndEmailForBot(botId);
3026 + }, 100);
4105 3027 }
4106 - // Floating bots: handled in the widget open handler
4107 - } else if (isEmbeddedBot(botId)) {
4108 - // Embedded bot, no email collection — load history with loader
4109 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
4110 - if (chatPersistenceEnabled) {
4111 - MxChatInstances.ensureSession(botId);
4112 - showChatContainerForBot(botId);
4113 - }
4114 3028 }
4115 3029 });
4116 3030 }
4117 3031
@@ -4121,32 +3035,39 @@
4121 3035 var $chatbot = getElement(botId, 'floating-chatbot');
4122 3036 if ($chatbot.hasClass('hidden')) {
4123 3037 $chatbot.removeClass('hidden').addClass('visible');
4124 3038 getElement(botId, 'floating-chatbot-button').addClass('hidden');
4125 - handlePreChatDismissal(botId);
3039 + $(this).fadeOut(250); // Hide pre-chat message
4126 3040 disableScroll(); // Disable scroll when chatbot opens
3041 + }
3042 + });
4127 3043
4128 - // Load chat history for returning visitors (persistence)
4129 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
4130 - if (chatPersistenceEnabled) {
4131 - MxChatInstances.ensureSession(botId);
4132 - }
3044 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
3045 + // This is a fallback for legacy support
3046 + $(document).on('click', '.close-pre-chat-message', function() {
3047 + var botId = getBotIdFromElement(this);
3048 + var $preChat = getElement(botId, 'pre-chat-message');
3049 + $preChat.fadeOut(200); // Hide the message
4133 3050
4134 - // Deferred email check — only on first widget open
4135 - var emailBlocker = getElementDOM(botId, 'email-blocker');
4136 - var instance = MxChatInstances.get(botId);
4137 - if (emailBlocker && !instance.emailCheckDone) {
4138 - instance.emailCheckDone = true;
4139 - resolveEmailState(botId);
4140 - } else if (!emailBlocker) {
4141 - showChatContainerForBot(botId);
3051 + // Send an AJAX request to set the transient flag for 24 hours
3052 + $.ajax({
3053 + url: mxchatChat.ajax_url,
3054 + type: 'POST',
3055 + data: {
3056 + action: 'mxchat_dismiss_pre_chat_message',
3057 + _ajax_nonce: mxchatChat.nonce
3058 + },
3059 + success: function() {
3060 + // Ensure the message is hidden after dismissal
3061 + $preChat.hide();
3062 + },
3063 + error: function() {
3064 + // Error dismissing pre-chat message - silently continue
4142 3065 }
4143 - }
3066 + });
4144 3067 });
4145 3068
4146 - // Legacy duplicate close handler removed — handled by single event delegation above
4147 3069
4148 -
4149 3070 function hasQuickQuestions(botId) {
4150 3071 botId = botId || 'default';
4151 3072 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
4152 3073 if (!questionsContainer) return false;
@@ -4282,11 +3203,18 @@
4282 3203 });
4283 3204
4284 3205 // Initialize when document is ready
4285 3206 setFullHeight();
3207 + trackOriginatingPage();
4286 3208
4287 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
4288 - // until the user's first interaction via MxChatInstances.ensureSession()
3209 + // Only load chat history if email collection is disabled
3210 + if (mxchatChat.email_collection_enabled !== 'on') {
3211 + // Load history for all instances
3212 + $('.mxchat-chatbot-wrapper').each(function() {
3213 + var botId = $(this).data('bot-id') || 'default';
3214 + loadChatHistory(botId);
3215 + });
3216 + }
4289 3217
4290 3218 // Initialize chat visibility for all instances
4291 3219 $('.mxchat-chatbot-wrapper').each(function() {
4292 3220 var botId = $(this).data('bot-id') || 'default';
@@ -4339,312 +3267,6 @@
4339 3267 }, 2000);
4340 3268 });
4341 3269 }
4342 3270 }
4343 -});
4344 -
4345 -// ============================================================================
4346 -// SATISFACTION RATING (v3.2.6)
4347 -// ============================================================================
4348 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
4349 -// inactivity following a bot reply. One prompt per session, deduped via
4350 -// localStorage. Runs ONLY when the satisfaction_rating_enabled option is on —
4351 -// the option (default off) is authoritative.
4352 -jQuery(function($) {
4353 - if (typeof mxchatChat === 'undefined') return;
4354 - // wp_localize_script stringifies scalars: a PHP boolean false arrives as
4355 - // '' and true as '1', so this must be an explicit-enable allowlist — the
4356 - // old "disabled when exactly false/'off'" check let '' through and the
4357 - // bubble rendered on sites with the option off/unset (plan-4bba64). PHP
4358 - // now emits 'on'/'off' strings; true/'1'/1 keep cached pre-fix HTML
4359 - // (boolean-true localizations) working.
4360 - // NOTE (plan-32db95): this gate reads the INLINE value at DOM ready and is
4361 - // deliberately NOT re-evaluated after the widget's dynamic-settings refresh
4362 - // merges fresh values over mxchatChat (that merge fires on first widget
4363 - // open, after this module has already decided). Re-evaluating would mean
4364 - // restructuring the whole module to late-bind its listeners — not worth it
4365 - // for a prompt that is at worst stale for one page load on a cached page.
4366 - var sre = mxchatChat.satisfaction_rating_enabled;
4367 - if (sre !== 'on' && sre !== true && sre !== '1' && sre !== 1) return;
4368 -
4369 - // wp_localize_script stringifies ints, so accept both number and numeric string.
4370 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
4371 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
4372 - if (!isFinite(idleSeconds)) idleSeconds = 60;
4373 - if (idleSeconds < 5) idleSeconds = 5;
4374 - if (idleSeconds > 600) idleSeconds = 600;
4375 - var IDLE_MS = idleSeconds * 1000;
4376 - var MIN_BOT_REPLIES = 2;
4377 - var ratingState = {};
4378 -
4379 - function getState(botId) {
4380 - if (!ratingState[botId]) {
4381 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
4382 - }
4383 - return ratingState[botId];
4384 - }
4385 -
4386 - function getSessionId(botId) {
4387 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
4388 - return MxChatInstances.getChatSession(botId);
4389 - }
4390 - return null;
4391 - }
4392 -
4393 - function isAlreadyRated(sessionId) {
4394 - if (!sessionId) return false;
4395 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
4396 - }
4397 -
4398 - function markRated(sessionId) {
4399 - if (!sessionId) return;
4400 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
4401 - }
4402 -
4403 - function esc(s) {
4404 - return String(s == null ? '' : s)
4405 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
4406 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
4407 - }
4408 -
4409 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
4410 - function ratingSkipInlineColors(botId) {
4411 - if (mxchatChat.skip_inline_colors) return true;
4412 - var botAssignments = mxchatChat.bot_theme_assignments || {};
4413 - return botAssignments.hasOwnProperty(botId);
4414 - }
4415 -
4416 - function botBubbleStyleAttr(botId) {
4417 - if (ratingSkipInlineColors(botId)) return '';
4418 - var bg = mxchatChat.bot_message_bg_color;
4419 - var fg = mxchatChat.bot_message_font_color;
4420 - if (!bg && !fg) return '';
4421 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
4422 - }
4423 -
4424 - // Reads the rating bubble's actual computed fg+bg (whatever paints it —
4425 - // the inline color pickers OR the mxchat-theme AI customizer's injected CSS)
4426 - // and paints the filled "Send" pill so it fills with the bot font color and
4427 - // labels in the bubble bg. Mirrors mxchatSyncMenuColors(~:1512) for the read.
4428 - // We paint the submit button DIRECTLY (inline longhand) rather than relying
4429 - // on the CSS rule's var()s: Chromium resolves an INHERITED custom property
4430 - // unreliably inside a descendant's `background`, so a bubble-level var would
4431 - // silently fall back to the literal (white-block bug all over again). Inline
4432 - // longhand always wins. Same transparent-guard as the menu so we never paint
4433 - // a see-through value — in that case the CSS literal fallbacks keep it legible.
4434 - function syncRatingBubbleColors(botId) {
4435 - var $chatBox = getChatBoxByBotId(botId);
4436 - if (!$chatBox || !$chatBox.length) return;
4437 - var bubbleEl = $chatBox.find('.mxchat-rating-bot-bubble').last()[0];
4438 - if (!bubbleEl) return;
4439 - var cs = window.getComputedStyle(bubbleEl);
4440 - var fg = cs.color;
4441 - var bg = cs.backgroundColor;
4442 - var hasFg = fg && fg !== 'rgba(0, 0, 0, 0)' && fg !== 'transparent';
4443 - var hasBg = bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent';
4444 - // Expose on the bubble too, for any inheriting styles / future use.
4445 - if (hasFg) bubbleEl.style.setProperty('--mxchat-bot-fg', fg);
4446 - if (hasBg) bubbleEl.style.setProperty('--mxchat-bot-bg', bg);
4447 - // Paint the Send pill directly — the part that actually fixes the bug.
4448 - var submitEl = bubbleEl.querySelector('.mxchat-rating-submit');
4449 - if (submitEl) {
4450 - if (hasFg) submitEl.style.backgroundColor = fg; // fill = bot font color
4451 - if (hasBg) submitEl.style.color = bg; // label = bubble background
4452 - }
4453 - }
4454 -
4455 - function copy(key) {
4456 - var c = mxchatChat.satisfaction_rating_copy || {};
4457 - var d = {
4458 - question: 'Was this helpful?',
4459 - helpful: 'Helpful',
4460 - not_helpful: 'Not helpful',
4461 - dismiss: 'Dismiss',
4462 - thanks: 'Thanks! Anything we should improve? (optional)',
4463 - placeholder: 'Tell us what could be better…',
4464 - send: 'Send',
4465 - skip: 'Skip',
4466 - saved: 'Thanks for the feedback.'
4467 - };
4468 - return c[key] || d[key];
4469 - }
4470 -
4471 - function thumbUpSvg() {
4472 - 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>';
4473 - }
4474 - function thumbDownSvg() {
4475 - 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>';
4476 - }
4477 -
4478 - function buildPromptHtml(botId) {
4479 - var styleAttr = botBubbleStyleAttr(botId);
4480 - return ''
4481 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4482 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
4483 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
4484 - + '<div class="mxchat-rating-actions">'
4485 - + '<span class="mxchat-rating-buttons">'
4486 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
4487 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
4488 - + '</span>'
4489 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
4490 - + '</div>'
4491 - + '</div>'
4492 - + '</div>';
4493 - }
4494 -
4495 - function buildFeedbackHtml(botId, rating) {
4496 - var styleAttr = botBubbleStyleAttr(botId);
4497 - return ''
4498 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4499 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
4500 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
4501 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
4502 - + '<div class="mxchat-rating-feedback-actions">'
4503 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
4504 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
4505 - + '</div>'
4506 - + '</div>'
4507 - + '</div>';
4508 - }
4509 -
4510 - function buildSavedHtml(botId) {
4511 - var styleAttr = botBubbleStyleAttr(botId);
4512 - return ''
4513 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4514 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
4515 - + '</div>';
4516 - }
4517 -
4518 - function getChatBoxByBotId(botId) {
4519 - var $byId = $('#chat-box-' + botId);
4520 - if ($byId.length) return $byId.first();
4521 - return $('.chat-box').first();
4522 - }
4523 -
4524 - function scrollChatBoxToBottom($chatBox) {
4525 - if (!$chatBox || !$chatBox.length) return;
4526 - $chatBox.scrollTop($chatBox[0].scrollHeight);
4527 - }
4528 -
4529 - function showPrompt(botId) {
4530 - var s = getState(botId);
4531 - if (s.promptShown || s.dismissed) return;
4532 - var sessionId = getSessionId(botId);
4533 - if (!sessionId) return;
4534 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4535 - var $chatBox = getChatBoxByBotId(botId);
4536 - if (!$chatBox.length) return;
4537 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
4538 - $chatBox.append(buildPromptHtml(botId));
4539 - syncRatingBubbleColors(botId);
4540 - s.promptShown = true;
4541 - scrollChatBoxToBottom($chatBox);
4542 - }
4543 -
4544 - function submitRating(botId, rating, feedback) {
4545 - var sessionId = getSessionId(botId);
4546 - if (!sessionId) return;
4547 - $.post(mxchatChat.ajax_url, {
4548 - action: 'mxchat_save_rating',
4549 - session_id: sessionId,
4550 - bot_id: botId,
4551 - rating: rating,
4552 - feedback: feedback || ''
4553 - });
4554 - markRated(sessionId);
4555 - }
4556 -
4557 - function onBotReply(botId) {
4558 - var s = getState(botId);
4559 - s.botReplies += 1;
4560 - if (s.promptShown || s.dismissed) return;
4561 - var sessionId = getSessionId(botId);
4562 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4563 - if (s.botReplies < MIN_BOT_REPLIES) return;
4564 - if (s.idleTimer) clearTimeout(s.idleTimer);
4565 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4566 - }
4567 -
4568 - function onUserMessage(botId) {
4569 - var s = getState(botId);
4570 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4571 - }
4572 -
4573 - function botIdFromChatBox(el) {
4574 - var id = el && el.id ? el.id : '';
4575 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4576 - }
4577 -
4578 - function setupObserver(chatBox) {
4579 - var botId = botIdFromChatBox(chatBox);
4580 - try {
4581 - var observer = new MutationObserver(function(mutations) {
4582 - mutations.forEach(function(m) {
4583 - for (var i = 0; i < m.addedNodes.length; i++) {
4584 - var node = m.addedNodes[i];
4585 - if (!node || node.nodeType !== 1) continue;
4586 - var $n = $(node);
4587 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4588 - 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)
4589 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4590 - }
4591 - });
4592 - });
4593 - observer.observe(chatBox, { childList: true });
4594 - } catch (e) { /* noop */ }
4595 - }
4596 -
4597 - $('.chat-box').each(function() { setupObserver(this); });
4598 -
4599 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4600 - e.preventDefault();
4601 - var $btn = $(this);
4602 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4603 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4604 - var botId = $prompt.data('bot-id') || 'default';
4605 - var rating = parseInt($btn.attr('data-rating'), 10);
4606 - if (rating !== 1 && rating !== -1) return;
4607 - submitRating(botId, rating, '');
4608 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4609 - syncRatingBubbleColors(botId);
4610 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4611 - });
4612 -
4613 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4614 - e.preventDefault();
4615 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4616 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4617 - var botId = $prompt.data('bot-id') || 'default';
4618 - var s = getState(botId);
4619 - s.dismissed = true;
4620 - markRated(getSessionId(botId));
4621 - ($wrap.length ? $wrap : $prompt).remove();
4622 - });
4623 -
4624 - function closeFeedback($fb) {
4625 - var botId = $fb.data('bot-id') || 'default';
4626 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4627 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4628 - syncRatingBubbleColors(botId);
4629 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4630 - }
4631 -
4632 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4633 - e.preventDefault();
4634 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4635 - });
4636 -
4637 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4638 - e.preventDefault();
4639 - var $fb = $(this).closest('.mxchat-rating-feedback');
4640 - var botId = $fb.data('bot-id') || 'default';
4641 - var rating = parseInt($fb.attr('data-rating'), 10);
4642 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4643 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4644 - if (text !== '') {
4645 - submitRating(botId, rating, text);
4646 - }
4647 - closeFeedback($fb);
4648 - });
4649 3271 });
4650 3272