PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.4
MxChat – AI Chatbot & Content Generation for WordPress v3.0.4
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 +287 -1664 3.2.133.0.4 View file →
@@ -1,166 +1,6 @@
1 1 jQuery(document).ready(function($) {
2 2
3 - // Nonce refresh — v2 (plan-6a68c9).
4 - //
5 - // The widget no longer relies on a nonce embedded in inline cached HTML.
6 - // Before each chat-send / stream-send / upload, we call the REST endpoint
7 - // GET /wp-json/mxchat/v1/nonce and use the freshly-issued value. The
8 - // endpoint creates the nonce with action `mxchat_chat_send`; the server-side
9 - // verifier ALSO still accepts the legacy `mxchat_chat_nonce` action for a
10 - // 30-day backwards-compat window so cached pages still in users' browsers
11 - // (which carry the legacy inline-localized nonce) keep working.
12 - //
13 - // Cache: a single module-scoped slot. TTL 12h conservatively (WP nonces are
14 - // 24h but we refetch at half-life so a freshly-cached-page user never sees
15 - // a borderline-stale nonce).
16 - var cachedFreshNonce = null;
17 - var cachedFreshNonceFetchedAt = 0;
18 - var NONCE_TTL_MS = 12 * 60 * 60 * 1000;
19 - var nonceRefreshState = 'idle'; // 'idle' | 'pending' | 'done'
20 - var nonceRefreshCallbacks = [];
21 -
22 - function getRestNonceUrl() {
23 - if (typeof mxchatChat !== 'undefined' && mxchatChat.rest_url) {
24 - return mxchatChat.rest_url.replace(/\/+$/, '') + '/nonce';
25 - }
26 - // Fallback: derive from current origin if mxchatChat.rest_url isn't set.
27 - return window.location.origin + '/wp-json/mxchat/v1/nonce';
28 - }
29 -
30 - function fetchFreshNonceFromRest() {
31 - return fetch(getRestNonceUrl(), {
32 - credentials: 'same-origin',
33 - headers: { 'Accept': 'application/json' }
34 - }).then(function (resp) {
35 - if (!resp.ok) {
36 - throw new Error('REST nonce fetch failed: ' + resp.status);
37 - }
38 - return resp.json();
39 - }).then(function (data) {
40 - if (data && data.nonce) {
41 - return data.nonce;
42 - }
43 - throw new Error('REST nonce response had no nonce field.');
44 - });
45 - }
46 -
47 - /**
48 - * withFreshNonce(cb) — invoke cb() after ensuring mxchatChat.nonce is fresh.
49 - * Tries REST endpoint first (cache-bypass design); falls back to the legacy
50 - * admin-ajax refresh path if REST is unavailable. Idempotent — concurrent
51 - * calls share the same in-flight refresh.
52 - */
53 - function withFreshNonce(callback) {
54 - if (typeof mxchatChat === 'undefined') {
55 - if (callback) callback();
56 - return;
57 - }
58 - var now = Date.now();
59 - if (cachedFreshNonce && (now - cachedFreshNonceFetchedAt) < NONCE_TTL_MS) {
60 - mxchatChat.nonce = cachedFreshNonce;
61 - if (callback) callback();
62 - return;
63 - }
64 - if (callback) nonceRefreshCallbacks.push(callback);
65 - if (nonceRefreshState === 'pending') return;
66 - nonceRefreshState = 'pending';
67 -
68 - var resolved = function (nonce) {
69 - if (nonce) {
70 - cachedFreshNonce = nonce;
71 - cachedFreshNonceFetchedAt = Date.now();
72 - mxchatChat.nonce = nonce;
73 - }
74 - nonceRefreshState = 'done';
75 - var pending = nonceRefreshCallbacks;
76 - nonceRefreshCallbacks = [];
77 - pending.forEach(function (cb) { try { cb(); } catch (e) {} });
78 - };
79 -
80 - fetchFreshNonceFromRest()
81 - .then(resolved)
82 - .catch(function () {
83 - // Fallback to the legacy admin-ajax refresh path (issued with the
84 - // old action `mxchat_chat_nonce`; the server still accepts both
85 - // during the compat window).
86 - if (mxchatChat.ajax_url) {
87 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' })
88 - .done(function (res) {
89 - if (res && res.success && res.data && res.data.nonce) {
90 - resolved(res.data.nonce);
91 - return;
92 - }
93 - resolved(null);
94 - })
95 - .fail(function () { resolved(null); });
96 - } else {
97 - resolved(null);
98 - }
99 - });
100 - }
101 -
102 - // Backwards-compat alias — every existing caller in this file (and any
103 - // out-of-tree consumer that hit this internal API) keeps working unchanged.
104 - function refreshNonceIfNeeded(callback) {
105 - return withFreshNonce(callback);
106 - }
107 -
108 - // Dynamic-settings refresh (plan-32db95).
109 - //
110 - // Every widget setting ships inline in cached page HTML, so behind a
111 - // full-page cache the site owner can't purge (host cache, CDN, the
112 - // browser itself) a toggled setting looks broken until the cache turns
113 - // over. Same distrust-cached-HTML reasoning as the per-request nonce:
114 - // on the FIRST widget open per page load we ask the nonce endpoint for
115 - // the current behavior-gate settings (?with_settings=1), merge them over
116 - // mxchatChat, and rebuild the header menu. Colors are NOT refreshed —
117 - // they're server-inline-styled, so a runtime swap would visibly flash.
118 - // On any failure we keep the inline values silently (nonce-fallback
119 - // posture). At most one request per page load, only if a widget opens.
120 - var dynamicSettingsState = 'idle'; // 'idle' | 'pending' | 'done'
121 -
122 - function mxchatRefreshDynamicSettings() {
123 - if (dynamicSettingsState !== 'idle') return;
124 - if (typeof mxchatChat === 'undefined') return;
125 - dynamicSettingsState = 'pending';
126 -
127 - var applied = function (data) {
128 - dynamicSettingsState = 'done';
129 - if (!data) return; // endpoint unavailable — inline values stand.
130 - if (data.nonce) {
131 - // Seed the nonce cache too: saves the first send's REST
132 - // round-trip and keeps us under the endpoint's rate limit.
133 - cachedFreshNonce = data.nonce;
134 - cachedFreshNonceFetchedAt = Date.now();
135 - mxchatChat.nonce = data.nonce;
136 - }
137 - if (data.settings && typeof data.settings === 'object') {
138 - $.extend(mxchatChat, data.settings);
139 - mxchatRebuildHeaderMenus();
140 - }
141 - };
142 -
143 - fetch(getRestNonceUrl() + '?with_settings=1', {
144 - credentials: 'same-origin',
145 - headers: { 'Accept': 'application/json' }
146 - }).then(function (resp) {
147 - if (!resp.ok) throw new Error('settings refresh failed: ' + resp.status);
148 - return resp.json();
149 - }).then(applied).catch(function () {
150 - // Fallback: legacy admin-ajax refresh path, same as withFreshNonce.
151 - if (mxchatChat.ajax_url) {
152 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce', with_settings: 1 })
153 - .done(function (res) {
154 - applied(res && res.success && res.data ? res.data : null);
155 - })
156 - .fail(function () { applied(null); });
157 - } else {
158 - applied(null);
159 - }
160 - });
161 - }
162 -
163 3 // ====================================
164 4 // MULTI-INSTANCE MANAGEMENT SYSTEM
165 5 // ====================================
166 6
@@ -170,15 +10,11 @@
170 10
171 11 // Initialize an instance for a bot
172 12 init: function(botId) {
173 13 if (!this.instances[botId]) {
174 - // When persistence is OFF, track when this session started
175 - // so the AI only sees messages from this page load
176 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
177 -
178 14 this.instances[botId] = {
179 15 botId: botId,
180 - sessionId: null,
16 + sessionId: this.getChatSession(botId),
181 17 lastSeenMessageId: '',
182 18 notificationCheckInterval: null,
183 19 pollingInterval: null,
184 20 processedMessageIds: new Set(),
@@ -184,11 +20,9 @@
184 20 processedMessageIds: new Set(),
185 21 activePdfFile: null,
186 22 activeWordFile: null,
187 23 chatHistoryLoaded: false,
188 - isStreaming: false,
189 - // Fresh context timestamp - only used when persistence is OFF
190 - sessionStartTimestamp: chatPersistenceEnabled ? 0 : Date.now()
24 + isStreaming: false
191 25 };
192 26 }
193 27 return this.instances[botId];
194 28 },
@@ -203,77 +37,23 @@
203 37 return Object.keys(this.instances);
204 38 },
205 39
206 40 // Session management per bot
207 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
208 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
209 41 getChatSession: function(botId) {
210 42 var cookieName = 'mxchat_session_id_' + botId;
211 - var storageKey = 'mxchat_session_id_' + botId;
212 43 var sessionId = getCookie(cookieName);
213 44
214 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
215 45 if (!sessionId) {
216 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
46 + sessionId = generateSessionId();
47 + this.setChatSession(botId, sessionId);
217 48 }
218 49
219 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
220 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
221 - // storage). Without this, ensureSession() can generate and store an ID that
222 - // getChatSession() then can't read back, causing null session_ids on send.
223 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
224 - sessionId = this.instances[botId].sessionId;
225 - }
226 -
227 - // Guard against stored sentinel values that indicate earlier broken writes.
228 - if (sessionId === 'null' || sessionId === 'undefined') {
229 - sessionId = null;
230 - }
231 -
232 - // Re-sync cookie from localStorage if cookie was lost
233 - if (sessionId && !getCookie(cookieName)) {
234 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
235 - }
236 -
237 - return sessionId || null;
50 + return sessionId;
238 51 },
239 52
240 - // Lazy session initializer — called on first user interaction
241 - ensureSession: function(botId) {
242 - botId = botId || 'default';
243 - var instance = this.instances[botId] || this.init(botId);
244 -
245 - if (instance.sessionId) {
246 - return instance.sessionId;
247 - }
248 -
249 - // Check for existing session from cookie or localStorage
250 - var existingSession = this.getChatSession(botId);
251 -
252 - if (existingSession) {
253 - instance.sessionId = existingSession;
254 - } else {
255 - // Brand new session
256 - var newId = generateSessionId();
257 - this.setChatSession(botId, newId);
258 - instance.sessionId = newId;
259 - }
260 -
261 - // Now that we have a session, do the deferred work
262 - refreshNonceIfNeeded();
263 - trackOriginatingPage();
264 -
265 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
266 - // so we do NOT call it here to avoid a race condition.
267 -
268 - return instance.sessionId;
269 - },
270 -
271 53 setChatSession: function(botId, sessionId) {
272 54 var cookieName = 'mxchat_session_id_' + botId;
273 - var storageKey = 'mxchat_session_id_' + botId;
274 55 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
275 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
276 56 if (this.instances[botId]) {
277 57 this.instances[botId].sessionId = sessionId;
278 58 }
279 59 },
@@ -278,10 +58,8 @@
278 58 }
279 59 },
280 60
281 61 resetChatSession: function(botId) {
282 - // Clear old session from localStorage before setting new one
283 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
284 62 var newSessionId = generateSessionId();
285 63 this.setChatSession(botId, newSessionId);
286 64 var $chatBox = getElement(botId, 'chat-box');
287 65 if ($chatBox.length) {
@@ -290,20 +68,8 @@
290 68 if (this.instances[botId]) {
291 69 this.instances[botId].chatHistoryLoaded = false;
292 70 this.instances[botId].processedMessageIds = new Set();
293 71 }
294 - },
295 -
296 - // Silent reset — new session ID without clearing the chat UI
297 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
298 - silentResetSession: function(botId) {
299 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
300 - var newSessionId = generateSessionId();
301 - this.setChatSession(botId, newSessionId);
302 - if (this.instances[botId]) {
303 - this.instances[botId].sessionId = newSessionId;
304 - }
305 - return newSessionId;
306 72 }
307 73 };
308 74
309 75 // ====================================
@@ -343,20 +109,8 @@
343 109 var id = $floating.attr('id') || '';
344 110 var match = id.match(/floating-chatbot-(.+)/);
345 111 if (match) return match[1];
346 112 }
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 113 // Fallback: check if element itself has an ID with bot suffix (e.g., floating-chatbot-button-{bot_id})
360 114 var elementId = $(element).attr('id') || '';
361 115 if (elementId) {
362 116 // Match patterns like: floating-chatbot-button-{bot_id}, pre-chat-message-{bot_id}
@@ -615,106 +369,13 @@
615 369 sendButton.disabled = false;
616 370 sendButton.style.opacity = '1';
617 371 sendButton.style.pointerEvents = 'auto';
618 372 }
619 - // Every completion path re-enables input, so this is the single restore
620 - // point for the streaming Stop affordance (no-op when not in stop mode).
621 - mxchatRestoreSendButton(botId);
622 373 }
623 374
624 -// --- Streaming Stop control -------------------------------------------------
625 -// One live stream handle per bot instance, so Stop on one widget never aborts
626 -// another bot on the same page.
627 -var mxchatActiveStreams = {};
628 -// Original send-button markup, captured once per bot the first time the Stop
629 -// state is shown (never captured while already in stop mode, so a rapid
630 -// stop-then-resend can't save the stop glyph as the "original").
631 -var mxchatSendMarkup = {};
632 -
633 -function mxchatShowStopButton(botId) {
634 - var btn = getElementDOM(botId, 'send-button');
635 - if (!btn) return;
636 - if (!btn.classList.contains('mxchat-stop-mode')) {
637 - mxchatSendMarkup[botId] = {
638 - html: btn.innerHTML,
639 - label: btn.getAttribute('aria-label')
640 - };
641 - }
642 -
643 - // Mirror the send icon's rendered size + color so the stop glyph looks
644 - // native, including custom send images/colors and theme overrides.
645 - var child = btn.querySelector('svg, img');
646 - var size = 25;
647 - var color = '';
648 - if (child) {
649 - var rect = child.getBoundingClientRect();
650 - if (rect.width) {
651 - size = Math.round(Math.min(rect.width, rect.height));
652 - }
653 - var cs = window.getComputedStyle(child);
654 - color = (child.tagName.toLowerCase() === 'svg' ? cs.fill : cs.color) || '';
655 - }
656 - var stopLabel = (typeof mxchatChat !== 'undefined' && mxchatChat.stop_button_label) || 'Stop response';
657 - 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>';
658 - // An add-on's DIRECT send-button handler (e.g. mxchat-vision's rebind) can
659 - // start a stream synchronously while the originating click is still
660 - // bubbling up to our delegated handler. Without this guard, that handler
661 - // reads the just-added stop-mode class as a user Stop press and aborts the
662 - // brand-new stream — the user's message renders but no reply ever fires
663 - // (plan-4bba64 silent message loss). The flag only spans the current event
664 - // dispatch: cleared on the next macrotask, long before a real Stop click.
665 - btn.__mxchatStopJustShown = true;
666 - setTimeout(function () { btn.__mxchatStopJustShown = false; }, 0);
667 - btn.classList.add('mxchat-stop-mode');
668 - btn.setAttribute('aria-label', stopLabel);
669 - btn.setAttribute('title', stopLabel);
670 - // disableChatInput() ran when the turn was sent; the Stop control itself
671 - // must stay clickable while the textarea remains disabled.
672 - btn.disabled = false;
673 - btn.style.opacity = '1';
674 - btn.style.pointerEvents = 'auto';
675 -}
676 -
677 -function mxchatRestoreSendButton(botId) {
678 - var btn = getElementDOM(botId, 'send-button');
679 - var saved = mxchatSendMarkup[botId];
680 - if (!btn || !btn.classList.contains('mxchat-stop-mode') || !saved) return;
681 - btn.innerHTML = saved.html;
682 - btn.classList.remove('mxchat-stop-mode');
683 - btn.removeAttribute('title');
684 - if (saved.label) {
685 - btn.setAttribute('aria-label', saved.label);
686 - }
687 -}
688 -
689 -function mxchatStopStreaming(botId) {
690 - var entry = mxchatActiveStreams[botId];
691 - if (!entry || !entry.controller) return;
692 - entry.aborted = true;
693 - try { entry.controller.abort(); } catch (e) {}
694 -}
695 -
696 -// Returns true when a stream rejection came from an intentional Stop click:
697 -// keep the partial text as the turn's answer — no error UI, no fallback resend.
698 -function mxchatHandleStreamAbort(botId, accumulatedContent, callback) {
699 - var entry = mxchatActiveStreams[botId];
700 - if (!entry || !entry.aborted) return false;
701 - delete mxchatActiveStreams[botId];
702 - if (!accumulatedContent) {
703 - // Stopped before the first chunk: drop the thinking bubble, no orphan message.
704 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
705 - }
706 - enableChatInput(botId); // also restores the send icon
707 - if (callback) {
708 - callback(accumulatedContent || '');
709 - }
710 - return true;
711 -}
712 -
713 375 // Update your existing sendMessage function
714 376 function sendMessage(botId) {
715 377 botId = botId || 'default';
716 - MxChatInstances.ensureSession(botId);
717 378 var $chatInput = getElement(botId, 'chat-input');
718 379 var message = $chatInput.val();
719 380
720 381 // ADD PROMPT HOOK HERE
@@ -722,18 +383,13 @@
722 383 message = customMxChatFilter(message, "prompt");
723 384 }
724 385
725 386 if (message) {
726 - // Don't disable input in live agent mode - let users chat freely
727 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
728 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
729 - if (!isAgentMode) {
730 - disableChatInput(botId);
731 - }
387 + // Disable input while waiting for response
388 + disableChatInput(botId);
732 389
733 390 appendMessage("user", message, '', [], false, botId);
734 391 $chatInput.val('');
735 - mxchatUpdateCharCounter($chatInput[0]); // reset the char counter after send (plan 7091a2)
736 392 $chatInput.css('height', 'auto');
737 393
738 394 if (hasQuickQuestions(botId)) {
739 395 collapseQuickQuestions(botId);
@@ -740,9 +396,9 @@
740 396 }
741 397 appendThinkingMessage(botId);
742 398 scrollToBottom(botId);
743 399
744 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
400 + const currentModel = mxchatChat.model || 'gpt-4o';
745 401
746 402 // Check if streaming is enabled AND supported for this model
747 403 if (shouldUseStreaming(currentModel)) {
748 404 callMxChatStream(message, function(response) {
@@ -758,9 +414,8 @@
758 414
759 415 // Update your existing sendMessageToChatbot function
760 416 function sendMessageToChatbot(message, botId) {
761 417 botId = botId || 'default';
762 - MxChatInstances.ensureSession(botId);
763 418
764 419 // ADD PROMPT HOOK HERE
765 420 if (typeof customMxChatFilter === 'function') {
766 421 message = customMxChatFilter(message, "prompt");
@@ -765,14 +420,10 @@
765 420 if (typeof customMxChatFilter === 'function') {
766 421 message = customMxChatFilter(message, "prompt");
767 422 }
768 423
769 - // Don't disable input in live agent mode - let users chat freely
770 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
771 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
772 - if (!isAgentMode) {
773 - disableChatInput(botId);
774 - }
424 + // Disable input while waiting for response
425 + disableChatInput(botId);
775 426
776 427 var sessionId = getChatSession(botId);
777 428
778 429 if (hasQuickQuestions(botId)) {
@@ -780,9 +431,9 @@
780 431 }
781 432 appendThinkingMessage(botId);
782 433 scrollToBottom(botId);
783 434
784 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
435 + const currentModel = mxchatChat.model || 'gpt-4o';
785 436
786 437 // Check if streaming is enabled AND supported for this model
787 438 if (shouldUseStreaming(currentModel)) {
788 439 callMxChatStream(message, function(response) {
@@ -851,15 +502,8 @@
851 502
852 503 function callMxChat(message, callback, botId) {
853 504 botId = botId || getMxChatBotId();
854 505
855 - // Streaming fallbacks land here: drop any leftover stream handle and
856 - // return the button to its send state (no-op for plain non-stream turns).
857 - if (mxchatActiveStreams[botId]) {
858 - delete mxchatActiveStreams[botId];
859 - }
860 - mxchatRestoreSendButton(botId);
861 -
862 506 // Store the message in case we need to retry after session reset
863 507 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
864 508
865 509 // Get page context if contextual awareness is enabled
@@ -864,44 +508,24 @@
864 508
865 509 // Get page context if contextual awareness is enabled
866 510 const pageContext = getPageContext();
867 511
868 - // Get instance for session start timestamp (used when persistence is OFF)
869 - var instance = MxChatInstances.get(botId);
870 -
871 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
872 - // and returns the guaranteed-present session id from the in-memory instance even when
873 - // cookie/localStorage writes are silently blocked by the browser.
874 - var sessionId = MxChatInstances.ensureSession(botId);
875 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
876 - // Last-resort generation to ensure we never POST a null marker.
877 - sessionId = generateSessionId();
878 - MxChatInstances.setChatSession(botId, sessionId);
879 - }
880 -
881 - // Wait for the page-cache nonce refresh to complete before firing the
882 - // chat-send AJAX. On cached pages the inline mxchatChat.nonce is stale
883 - // until refreshNonceIfNeeded() returns; constructing ajaxData inside the
884 - // callback guarantees we read the fresh value. See plan-c5457f.
885 - refreshNonceIfNeeded(function() {
886 512 // Prepare AJAX data
887 513 const ajaxData = {
888 514 action: 'mxchat_handle_chat_request',
889 515 message: message,
890 - session_id: sessionId,
516 + session_id: getChatSession(botId),
891 517 nonce: mxchatChat.nonce,
892 518 current_page_url: window.location.href,
893 519 current_page_title: document.title,
894 - bot_id: botId,
895 - // Pass session start timestamp so AI context matches what user sees
896 - session_start_timestamp: instance.sessionStartTimestamp || 0
520 + bot_id: botId
897 521 };
898 -
522 +
899 523 // Add page context if available
900 524 if (pageContext) {
901 525 ajaxData.page_context = JSON.stringify(pageContext);
902 526 }
903 -
527 +
904 528 // CHECK FOR VISION FLAGS AND ADD THEM
905 529 if (window.mxchatVisionProcessed) {
906 530 ajaxData.vision_processed = true;
907 531 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -910,9 +534,9 @@
910 534 window.mxchatVisionProcessed = false;
911 535 window.mxchatOriginalMessage = null;
912 536 window.mxchatVisionImagesCount = 0;
913 537 }
914 -
538 +
915 539 $.ajax({
916 540 url: mxchatChat.ajax_url,
917 541 type: 'POST',
918 542 dataType: 'json',
@@ -950,16 +574,23 @@
950 574 errorMessage = "An error occurred. Please try again or contact support.";
951 575 }
952 576
953 577 // Handle session reset action (IP changed, session expired, etc.)
954 - // Silent reset — keep chat UI intact, just get a new session and retry
955 578 if (response.data && response.data.action === 'reset_session') {
956 - MxChatInstances.silentResetSession(botId);
957 - // Re-send the original message with the new session (user message is already displayed)
579 + // Clear the old session and generate a new one
580 + resetChatSession(botId);
581 + // Remove the temporary loading message
582 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
583 + // Re-send the original message with the new session
958 584 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
959 585 if (originalMessage) {
960 586 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
961 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
587 + // Re-add the user message and thinking indicator
588 + appendMessage("user", originalMessage, '', [], false, botId);
589 + appendThinkingMessage(botId);
590 + scrollToBottom(botId);
591 + // Determine whether to use streaming
592 + const currentModel = mxchatChat.model || 'gpt-4o';
962 593 if (shouldUseStreaming(currentModel)) {
963 594 callMxChatStream(originalMessage, function(response) {
964 595 getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
965 596 }, botId);
@@ -1016,11 +647,9 @@
1016 647 }
1017 648
1018 649 // Check for live agent response
1019 650 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
1020 - removeThinkingDots(botId);
1021 651 updateChatModeIndicator('agent', botId);
1022 - enableChatInput(botId);
1023 652 return;
1024 653 }
1025 654
1026 655 // Handle the message and show notification if chat is hidden
@@ -1053,13 +682,9 @@
1053 682 $badge.show();
1054 683 }
1055 684 }
1056 685 } else {
1057 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
1058 - if (response.vectorstore_error) {
1059 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
1060 - }
1061 - replaceLastMessage("bot", emptyMsg, '', [], botId);
686 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.", '', [], botId);
1062 687 }
1063 688
1064 689 if (response.message_id) {
1065 690 var instance = MxChatInstances.get(botId);
@@ -1101,9 +726,8 @@
1101 726
1102 727 replaceLastMessage("bot", errorMessage, '', [], botId);
1103 728 }
1104 729 });
1105 - }); // refreshNonceIfNeeded
1106 730 }
1107 731
1108 732 function callMxChatStream(message, callback, botId) {
1109 733 botId = botId || getMxChatBotId();
@@ -1110,9 +734,9 @@
1110 734
1111 735 // Store the message in case we need to retry after session reset
1112 736 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
1113 737
1114 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
738 + const currentModel = mxchatChat.model || 'gpt-4o';
1115 739 if (!isStreamingSupported(currentModel)) {
1116 740 callMxChat(message, callback, botId);
1117 741 return;
1118 742 }
@@ -1119,35 +743,17 @@
1119 743
1120 744 // Get page context if contextual awareness is enabled
1121 745 const pageContext = getPageContext();
1122 746
1123 - // Get instance for session start timestamp (used when persistence is OFF)
1124 - var instance = MxChatInstances.get(botId);
1125 -
1126 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
1127 - // non-string value via String(), so passing `null` would POST the literal string "null"
1128 - // and land in the transcripts table as a ghost session. ensureSession() always returns
1129 - // a real string even when cookies/localStorage are blocked.
1130 - var streamSessionId = MxChatInstances.ensureSession(botId);
1131 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
1132 - streamSessionId = generateSessionId();
1133 - MxChatInstances.setChatSession(botId, streamSessionId);
1134 - }
1135 -
1136 - // Wait for the page-cache nonce refresh before constructing formData (which
1137 - // captures mxchatChat.nonce by value). Mirrors callMxChat's wrapping. See plan-c5457f.
1138 - refreshNonceIfNeeded(function() {
1139 747 const formData = new FormData();
1140 748 formData.append('action', 'mxchat_stream_chat');
1141 749 formData.append('message', message);
1142 - formData.append('session_id', streamSessionId);
750 + formData.append('session_id', getChatSession(botId));
1143 751 formData.append('nonce', mxchatChat.nonce);
1144 752 formData.append('current_page_url', window.location.href);
1145 753 formData.append('current_page_title', document.title);
1146 754 formData.append('bot_id', botId);
1147 - // Pass session start timestamp so AI context matches what user sees
1148 - formData.append('session_start_timestamp', instance.sessionStartTimestamp || 0);
1149 -
755 +
1150 756 // Add page context if available
1151 757 if (pageContext) {
1152 758 formData.append('page_context', JSON.stringify(pageContext));
1153 759 }
@@ -1166,20 +772,12 @@
1166 772 let accumulatedContent = '';
1167 773 let testingDataReceived = false;
1168 774 let streamingStarted = false;
1169 775
1170 - // Abortable stream: a fresh controller per turn, keyed by bot instance.
1171 - // The Stop control (send button swapped in place) aborts both the read
1172 - // loop and the underlying request.
1173 - var streamControl = { controller: new AbortController(), aborted: false };
1174 - mxchatActiveStreams[botId] = streamControl;
1175 - mxchatShowStopButton(botId);
1176 -
1177 776 fetch(mxchatChat.ajax_url, {
1178 777 method: 'POST',
1179 778 body: formData,
1180 - credentials: 'same-origin',
1181 - signal: streamControl.controller.signal
779 + credentials: 'same-origin'
1182 780 })
1183 781 .then(response => {
1184 782 // Store the response for potential fallback handling
1185 783 const responseClone = response.clone();
@@ -1248,16 +846,8 @@
1248 846
1249 847 // Re-enable chat input when stream ends with content
1250 848 enableChatInput(botId);
1251 849
1252 - // Scroll the user's last message to the top now that the
1253 - // bot's full reply has rendered (gives max reading room).
1254 - var $chatBoxDone = getElement(botId, 'chat-box');
1255 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
1256 - if ($lastUserMsgDone.length) {
1257 - scrollElementToTop($lastUserMsgDone, botId);
1258 - }
1259 -
1260 850 if (callback) {
1261 851 callback(accumulatedContent);
1262 852 }
1263 853 return;
@@ -1280,16 +870,8 @@
1280 870
1281 871 // Re-enable chat input after streaming completes
1282 872 enableChatInput(botId);
1283 873
1284 - // Scroll the user's last message to the top now
1285 - // that the bot's full reply has rendered.
1286 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1287 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1288 - if ($lastUserMsgStreamDone.length) {
1289 - scrollElementToTop($lastUserMsgStreamDone, botId);
1290 - }
1291 -
1292 874 if (callback) {
1293 875 callback(accumulatedContent);
1294 876 }
1295 877 return;
@@ -1346,9 +928,8 @@
1346 928 }
1347 929
1348 930 processStream();
1349 931 }).catch(streamError => {
1350 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1351 932 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1352 933 callMxChat(message, callback, botId);
1353 934 });
1354 935 }
@@ -1355,9 +936,8 @@
1355 936
1356 937 processStream();
1357 938 })
1358 939 .catch(error => {
1359 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1360 940 // Check if we have server error data with chat mode
1361 941 if (error && error.isServerError && error.data) {
1362 942 // Check for chat mode in error data
1363 943 if (error.data.chat_mode) {
@@ -1370,9 +950,8 @@
1370 950 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1371 951 callMxChat(message, callback, botId);
1372 952 }
1373 953 });
1374 - }); // refreshNonceIfNeeded
1375 954 }
1376 955
1377 956 // Helper function to handle non-streaming responses
1378 957 function handleNonStreamResponse(data, callback, botId) {
@@ -1411,16 +990,21 @@
1411 990 errorMessage = "An error occurred. Please try again or contact support.";
1412 991 }
1413 992
1414 993 // Handle session reset action (IP changed, session expired, etc.)
1415 - // Silent reset — keep chat UI intact, just get a new session and retry
1416 994 if (data.data && data.data.action === 'reset_session') {
1417 - MxChatInstances.silentResetSession(botId);
1418 - // Re-send the original message with the new session (user message is already displayed)
995 + // Clear the old session and generate a new one
996 + resetChatSession(botId);
997 + // Re-send the original message with the new session
1419 998 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1420 999 if (originalMessage) {
1421 1000 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1422 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1001 + // Re-add the user message and thinking indicator
1002 + appendMessage("user", originalMessage, '', [], false, botId);
1003 + appendThinkingMessage(botId);
1004 + scrollToBottom(botId);
1005 + // Determine whether to use streaming
1006 + const currentModel = mxchatChat.model || 'gpt-4o';
1423 1007 if (shouldUseStreaming(currentModel)) {
1424 1008 callMxChatStream(originalMessage, callback, botId);
1425 1009 } else {
1426 1010 callMxChat(originalMessage, callback, botId);
@@ -1442,22 +1026,8 @@
1442 1026 }
1443 1027 return; // Exit early for errors
1444 1028 }
1445 1029
1446 - // Check for live agent response
1447 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1448 - removeThinkingDots(botId);
1449 - // Also remove any leftover bot-message that lost its temporary-message class
1450 - var $chatBox = getElement(botId, 'chat-box');
1451 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1452 - updateChatModeIndicator('agent', botId);
1453 - enableChatInput(botId);
1454 - if (callback) {
1455 - callback('');
1456 - }
1457 - return;
1458 - }
1459 -
1460 1030 // Handle different response formats
1461 1031 if (data.text || data.html || data.message) {
1462 1032
1463 1033 // Apply response hooks
@@ -1502,15 +1072,19 @@
1502 1072 }
1503 1073
1504 1074 // Enhanced updateChatModeIndicator function for immediate DOM updates
1505 1075 function updateChatModeIndicator(mode, botId) {
1076 + console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1506 1077 botId = botId || 'default';
1507 1078 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1079 + console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1508 1080 if (indicator) {
1509 1081 const oldText = indicator.textContent;
1082 + console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1510 1083
1511 1084 if (mode === 'agent') {
1512 1085 indicator.textContent = 'Live Agent';
1086 + console.log('[MxChat] Mode is agent, calling startPolling...');
1513 1087 startPolling(botId);
1514 1088 } else {
1515 1089 // Everything else is AI mode
1516 1090 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
@@ -1581,21 +1155,9 @@
1581 1155 // Update the event handlers to use the correct function names (using event delegation)
1582 1156 // Use class-based selectors for multi-instance support
1583 1157 $(document).on('click', '.send-button', function() {
1584 1158 var botId = getBotIdFromElement(this);
1585 - // While a response is streaming the button is a Stop control.
1586 - if (this.classList.contains('mxchat-stop-mode')) {
1587 - // Same click that just started this stream (an add-on's direct handler
1588 - // ran before this delegated one) — not a Stop press. See
1589 - // mxchatShowStopButton for the full story (plan-4bba64).
1590 - if (this.__mxchatStopJustShown) return;
1591 - mxchatStopStreaming(botId);
1592 - return;
1593 - }
1594 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1595 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1596 - disableChatInput(botId);
1597 - }
1159 + disableChatInput(botId);
1598 1160 sendMessage(botId);
1599 1161 });
1600 1162
1601 1163 // Override enter key handler (using event delegation)
@@ -1602,378 +1164,14 @@
1602 1164 $(document).on('keypress', '.chat-input', function(e) {
1603 1165 if (e.which == 13 && !e.shiftKey) {
1604 1166 e.preventDefault();
1605 1167 var botId = getBotIdFromElement(this);
1606 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1607 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1608 - disableChatInput(botId);
1609 - }
1168 + disableChatInput(botId);
1610 1169 sendMessage(botId);
1611 1170 }
1612 1171 });
1613 1172
1614 -// Chat input character counter + soft limit feedback (plan 7091a2).
1615 -// Language-neutral: numbers + color only, no translatable strings. The counter
1616 -// reveals near the cap and ramps neutral -> amber -> red; an over-limit keystroke
1617 -// or trimmed paste produces a brief border-flash/shake so the maxlength cap (plan
1618 -// a3fae2) is never a silent "input jumps back". Per-bot scoped via .input-container.
1619 -function mxchatUpdateCharCounter(inputEl) {
1620 - if (!inputEl || !inputEl.closest) return;
1621 - var max = parseInt(inputEl.getAttribute('maxlength'), 10);
1622 - var container = inputEl.closest('.input-container');
1623 - if (!container || !max || max <= 0) return;
1624 - var counter = container.querySelector('.mxchat-char-counter');
1625 - if (!counter) return;
1626 - var len = inputEl.value.length;
1627 - var ratio = len / max;
1628 - var nearThreshold = 0.8; // start surfacing the counter at 80% of the cap
1629 - var cur = counter.querySelector('.mxchat-char-counter-current');
1630 - if (cur) cur.textContent = len;
1631 - var warn = ratio >= nearThreshold && len < max;
1632 - var full = len >= max;
1633 - counter.classList.toggle('is-visible', ratio >= nearThreshold);
1634 - counter.classList.toggle('is-warn', warn);
1635 - counter.classList.toggle('is-full', full);
1636 - container.classList.toggle('mxchat-input-near-limit', warn);
1637 - container.classList.toggle('mxchat-input-at-limit', full);
1638 -}
1639 -
1640 -function mxchatBumpInput(inputEl) {
1641 - var container = inputEl && inputEl.closest ? inputEl.closest('.input-container') : null;
1642 - if (!container) return;
1643 - container.classList.remove('mxchat-input-bump');
1644 - void container.offsetWidth; // reflow so a rapid second hit retriggers the animation
1645 - container.classList.add('mxchat-input-bump');
1646 - clearTimeout($(container).data('mxchatBumpTimeout'));
1647 - var t = setTimeout(function() { container.classList.remove('mxchat-input-bump'); }, 220);
1648 - $(container).data('mxchatBumpTimeout', t);
1649 -}
1650 -
1651 -// Live counter update on every input.
1652 -$(document).on('input', '.chat-input', function() {
1653 - mxchatUpdateCharCounter(this);
1654 -});
1655 -
1656 -// Visible "you've hit the edge" feedback when a printable keystroke is about to be
1657 -// rejected at the cap (maxlength silently swallows it otherwise).
1658 -$(document).on('keydown', '.chat-input', function(e) {
1659 - var max = parseInt(this.getAttribute('maxlength'), 10);
1660 - if (!max || max <= 0 || this.value.length < max) return;
1661 - if (e.ctrlKey || e.metaKey || e.altKey) return;
1662 - // A single printable char with no selection to overwrite WILL be rejected.
1663 - if (e.key && e.key.length === 1 && this.selectionStart === this.selectionEnd) {
1664 - mxchatBumpInput(this);
1665 - }
1666 -});
1667 -
1668 -// A paste that gets trimmed to the cap also bumps, so truncation is never silent.
1669 -$(document).on('paste', '.chat-input', function() {
1670 - var el = this;
1671 - var max = parseInt(el.getAttribute('maxlength'), 10);
1672 - if (!max || max <= 0) return;
1673 - setTimeout(function() {
1674 - mxchatUpdateCharCounter(el);
1675 - if (el.value.length >= max) mxchatBumpInput(el);
1676 - }, 0);
1677 -});
1678 -
1679 -// Builds the list of overflow-menu items for a given bot.
1680 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1681 -function mxchatGetHeaderMenuItems(botId) {
1682 - var items = [];
1683 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1684 -
1685 - // The `print_button_*` keys still gate this item for back-compat with
1686 - // existing user options. The action is now a transcript download, not print.
1687 - if (settings.print_button_enabled === 'on') {
1688 - items.push({
1689 - id: 'download-transcript',
1690 - label: settings.print_button_label || 'Download Transcript',
1691 - 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>',
1692 - action: function() {
1693 - mxchatDownloadTranscript(botId);
1694 - }
1695 - });
1696 - }
1697 -
1698 - // "Start new chat" — surfaces the EXISTING per-conversation reset
1699 - // (MxChatInstances.resetChatSession) so a visitor can start a fresh thread
1700 - // without the site owner disabling chat persistence globally. Default OFF;
1701 - // gated by the reset_chat_enabled option. plan ac2e81.
1702 - if (settings.reset_chat_enabled === 'on') {
1703 - items.push({
1704 - id: 'reset-chat',
1705 - label: settings.reset_chat_label || 'Start new chat',
1706 - 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>',
1707 - action: function() {
1708 - var confirmMsg = settings.reset_chat_confirm || 'Start a new chat? This clears the current conversation.';
1709 - if (window.confirm(confirmMsg)) {
1710 - MxChatInstances.resetChatSession(botId);
1711 - }
1712 - }
1713 - });
1714 - }
1715 -
1716 - return items;
1717 -}
1718 -
1719 -// Builds a clean markdown transcript of the current conversation and triggers
1720 -// a file download. Used by the "Download Transcript" menu item.
1721 -function mxchatDownloadTranscript(botId) {
1722 - var $chatBox = getElement(botId, 'chat-box');
1723 - if (!$chatBox || !$chatBox.length) return;
1724 -
1725 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1726 - var headerTitle = settings.print_header_title || 'Chat transcript';
1727 - var now = new Date();
1728 - var stamp = now.toLocaleString();
1729 -
1730 - var lines = [];
1731 - lines.push('# ' + headerTitle);
1732 - lines.push('');
1733 - lines.push('Exported: ' + stamp);
1734 - lines.push('');
1735 - lines.push('---');
1736 - lines.push('');
1737 -
1738 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1739 - var $msg = $(this);
1740 - // Skip thinking placeholders and any in-flight temporary messages.
1741 - if ($msg.find('.thinking-dots').length) return;
1742 - if ($msg.hasClass('temporary-message')) return;
1743 -
1744 - var sender;
1745 - if ($msg.hasClass('user-message')) sender = 'User';
1746 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1747 - else sender = 'AI Agent';
1748 -
1749 - // Strip interactive UI from the cloned message so we get the conversation text.
1750 - var $clone = $msg.clone();
1751 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1752 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1753 - if (!text) return;
1754 -
1755 - lines.push('**' + sender + '**');
1756 - lines.push('');
1757 - lines.push(text);
1758 - lines.push('');
1759 - });
1760 -
1761 - var content = lines.join('\n');
1762 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1763 - var fname = 'mxchat-transcript-' + iso + '.md';
1764 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1765 - var url = URL.createObjectURL(blob);
1766 - var a = document.createElement('a');
1767 - a.href = url;
1768 - a.download = fname;
1769 - a.style.display = 'none';
1770 - document.body.appendChild(a);
1771 - a.click();
1772 - setTimeout(function() {
1773 - if (a.parentNode) a.parentNode.removeChild(a);
1774 - URL.revokeObjectURL(url);
1775 - }, 100);
1776 -}
1777 -
1778 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1779 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1780 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1781 -function mxchatSyncMenuColors(botId, $wrap) {
1782 - if (!$wrap || !$wrap.length) return;
1783 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1784 - if (!$bot.length) return;
1785 - var cs = window.getComputedStyle($bot[0]);
1786 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1787 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1788 - }
1789 - // Bot text color usually lives on a child div, not .bot-message itself.
1790 - var $textChild = $bot.find('[style*="color"]').first();
1791 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1792 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1793 -}
1794 -
1795 -// Renders (or re-renders) the item list for one menu wrap. Split out of
1796 -// mxchatInitHeaderMenu so the dynamic-settings merge (plan-32db95) can
1797 -// rebuild items + trigger visibility WITHOUT re-binding the one-time
1798 -// open/close/keyboard wiring. closeMenu is passed in by the init closure;
1799 -// a rebuild before init (never happens, but harmless) just skips it.
1800 -function mxchatRenderHeaderMenuItems(botId, $wrap, closeMenuFn) {
1801 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1802 - var $menu = $wrap.find('.mxchat-header-menu');
1803 - var items = mxchatGetHeaderMenuItems(botId);
1804 -
1805 - $menu.empty();
1806 -
1807 - if (!items.length) {
1808 - $trigger.hide();
1809 - $menu.hide();
1810 - return;
1811 - }
1812 -
1813 - // Clear any inline display:none a previous zero-item render left behind —
1814 - // open/close visibility is governed by the hidden prop + is-open class.
1815 - $trigger.css('display', '');
1816 - $menu.css('display', '');
1817 -
1818 - items.forEach(function(item, idx) {
1819 - var $btn = $('<button>', {
1820 - type: 'button',
1821 - 'class': 'mxchat-menu-item',
1822 - 'role': 'menuitem',
1823 - 'tabindex': '-1',
1824 - 'data-menu-id': item.id,
1825 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1826 - '<span class="mxchat-menu-item-label"></span>'
1827 - });
1828 - $btn.find('.mxchat-menu-item-label').text(item.label);
1829 - $btn.on('click', function(e) {
1830 - e.preventDefault();
1831 - e.stopPropagation();
1832 - if (closeMenuFn) closeMenuFn();
1833 - try { item.action(); } catch (err) { /* no-op */ }
1834 - });
1835 - $menu.append($btn);
1836 - });
1837 -}
1838 -
1839 -// Re-render every menu on the page after a dynamic-settings merge
1840 -// (multi-bot: each wrap re-reads its items). An OPEN menu is left alone —
1841 -// swapping items under the user mid-interaction yanks focus — and the
1842 -// rebuild runs when it closes instead (closeMenu checks the pending flag).
1843 -function mxchatRebuildHeaderMenus() {
1844 - $('.mxchat-header-menu-wrap').each(function() {
1845 - var $wrap = $(this);
1846 - var botId = $wrap.data('bot-id');
1847 - if (!botId) return;
1848 - if (!$wrap.data('mxchatMenuReady')) {
1849 - mxchatInitHeaderMenu(botId);
1850 - return;
1851 - }
1852 - if ($wrap.find('.mxchat-header-menu').hasClass('is-open')) {
1853 - $wrap.data('mxchatMenuRebuildPending', true);
1854 - return;
1855 - }
1856 - mxchatRenderHeaderMenuItems(botId, $wrap, $wrap.data('mxchatMenuClose'));
1857 - });
1858 -}
1859 -
1860 -// One-time per-widget init: renders menu items, wires open/close,
1861 -// outside-click, Escape, and arrow-key navigation. If no items, hides the
1862 -// trigger. Wiring happens even when there are zero items at init, so a
1863 -// later dynamic-settings rebuild that adds items has a working trigger.
1864 -function mxchatInitHeaderMenu(botId) {
1865 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1866 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1867 -
1868 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1869 - var $menu = $wrap.find('.mxchat-header-menu');
1870 -
1871 - // Initial color sync — covers normal page load.
1872 - mxchatSyncMenuColors(botId, $wrap);
1873 -
1874 - function openMenu() {
1875 - // Re-sync each open in case the active theme changed since init.
1876 - mxchatSyncMenuColors(botId, $wrap);
1877 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1878 - $trigger.attr('aria-expanded', 'true');
1879 - // Focus the first item for keyboard users
1880 - setTimeout(function() {
1881 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1882 - }, 0);
1883 - }
1884 - function closeMenu(returnFocus) {
1885 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1886 - $trigger.attr('aria-expanded', 'false');
1887 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1888 - if (returnFocus) $trigger.trigger('focus');
1889 - // A dynamic-settings rebuild that arrived while the menu was open
1890 - // was deferred (mxchatRebuildHeaderMenus) — run it now.
1891 - if ($wrap.data('mxchatMenuRebuildPending')) {
1892 - $wrap.removeData('mxchatMenuRebuildPending');
1893 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1894 - }
1895 - }
1896 -
1897 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1898 - // click-to-collapse handler does not fire.
1899 - $trigger.on('click', function(e) {
1900 - e.preventDefault();
1901 - e.stopPropagation();
1902 - if ($menu.hasClass('is-open')) closeMenu();
1903 - else openMenu();
1904 - });
1905 -
1906 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1907 - $menu.on('click', function(e) {
1908 - e.stopPropagation();
1909 - });
1910 -
1911 - // Outside click closes the menu.
1912 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1913 - if (!$menu.hasClass('is-open')) return;
1914 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1915 - closeMenu();
1916 - });
1917 -
1918 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1919 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1920 - var $items = $menu.find('.mxchat-menu-item');
1921 - var idx = $items.index(this);
1922 - if (e.key === 'Escape') {
1923 - e.preventDefault();
1924 - closeMenu(true);
1925 - } else if (e.key === 'ArrowDown') {
1926 - e.preventDefault();
1927 - var $next = $items.eq((idx + 1) % $items.length);
1928 - $items.attr('tabindex', '-1');
1929 - $next.attr('tabindex', '0').trigger('focus');
1930 - } else if (e.key === 'ArrowUp') {
1931 - e.preventDefault();
1932 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1933 - $items.attr('tabindex', '-1');
1934 - $prev.attr('tabindex', '0').trigger('focus');
1935 - } else if (e.key === 'Enter' || e.key === ' ') {
1936 - e.preventDefault();
1937 - $(this).trigger('click');
1938 - }
1939 - });
1940 - $trigger.on('keydown', function(e) {
1941 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1942 - e.preventDefault();
1943 - closeMenu(true);
1944 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1945 - e.preventDefault();
1946 - openMenu();
1947 - }
1948 - });
1949 -
1950 - // Expose closeMenu for out-of-closure re-renders (mxchatRebuildHeaderMenus),
1951 - // then do the initial item render.
1952 - $wrap.data('mxchatMenuClose', closeMenu);
1953 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1954 -
1955 - $wrap.data('mxchatMenuReady', true);
1956 -}
1957 -
1958 -// Initialize header menus for every rendered widget on DOM ready.
1959 -$(function() {
1960 - $('.mxchat-header-menu-wrap').each(function() {
1961 - var botId = $(this).data('bot-id');
1962 - if (botId) mxchatInitHeaderMenu(botId);
1963 - });
1964 -
1965 - // Embedded (non-floating) widgets are open from the moment the page
1966 - // renders — refresh dynamic settings at init (plan-32db95). Floating
1967 - // widgets refresh on first launcher open instead.
1968 - var hasEmbeddedWidget = $('.mxchat-chatbot-wrapper').filter(function() {
1969 - return !$(this).closest('.floating-chatbot').length;
1970 - }).length > 0;
1971 - if (hasEmbeddedWidget) {
1972 - mxchatRefreshDynamicSettings();
1973 - }
1974 -});
1975 -
1173 +
1976 1174 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1977 1175 try {
1978 1176 // Determine styles based on sender type
1979 1177 let messageClass, bgColor, fontColor;
@@ -2011,12 +1209,17 @@
2011 1209 'margin-bottom': '1em'
2012 1210 });
2013 1211 }
2014 1212
2015 - // Process the message content - always run linkify to convert markdown
2016 - // links and format text. linkify() handles existing HTML safely via
2017 - // negative lookaheads that skip URLs already inside <a> tags.
2018 - let fullMessage = linkify(messageText);
1213 + // Process the message content based on sender
1214 + let fullMessage;
1215 + if (sender === "user") {
1216 + // For user messages, apply linkify after sanitization
1217 + fullMessage = linkify(messageText);
1218 + } else {
1219 + // For bot/agent messages, preserve HTML
1220 + fullMessage = messageText;
1221 + }
2019 1222
2020 1223 // Add images if provided
2021 1224 if (images && images.length > 0) {
2022 1225 fullMessage += '<div class="image-gallery" dir="auto">';
@@ -2065,12 +1268,8 @@
2065 1268 if (lastUserMessage.length) {
2066 1269 scrollElementToTop(lastUserMessage, botId);
2067 1270 }
2068 1271 }
2069 -
2070 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
2071 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2072 - }
2073 1272 });
2074 1273
2075 1274 if (messageText.id) {
2076 1275 var instance = MxChatInstances.get(botId);
@@ -2155,12 +1354,26 @@
2155 1354 bgColor = botMessageBgColor;
2156 1355 fontColor = botMessageFontColor;
2157 1356 }
2158 1357
2159 - // Always run linkify to convert markdown links and format text.
2160 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
2161 - // to avoid double-processing URLs that are already inside <a> tags).
2162 - var fullMessage = linkify(responseText);
1358 + // FIXED: Only linkify if response doesn't already contain HTML links or tags
1359 + // This prevents double-processing of URLs that are already formatted as HTML
1360 + var fullMessage;
1361 + if (sender === "user") {
1362 + // Always linkify user messages (they're plain text)
1363 + fullMessage = linkify(responseText);
1364 + } else {
1365 + // For bot/agent messages, check if HTML already exists
1366 + if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1367 + responseText.includes('<img') || responseText.includes('<div') ||
1368 + responseText.includes('<p>') || responseText.includes('<br>')) {
1369 + // Response already has HTML, don't process it
1370 + fullMessage = responseText;
1371 + } else {
1372 + // Plain text response, apply linkify
1373 + fullMessage = linkify(responseText);
1374 + }
1375 + }
2163 1376
2164 1377 if (responseHtml) {
2165 1378 // Only add line breaks if there's actual text content before the HTML
2166 1379 if (fullMessage && fullMessage.trim()) {
@@ -2217,12 +1430,8 @@
2217 1430 }
2218 1431
2219 1432 // Re-enable chat input after response is displayed
2220 1433 enableChatInput(botId);
2221 -
2222 - if (sender === "bot" || sender === "agent") {
2223 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
2224 - }
2225 1434 } else {
2226 1435 appendMessage(sender, responseText, responseHtml, images, false, botId);
2227 1436 // Re-enable chat input after response is displayed
2228 1437 enableChatInput(botId);
@@ -2231,15 +1440,8 @@
2231 1440
2232 1441
2233 1442 function appendThinkingMessage(botId) {
2234 1443 botId = botId || 'default';
2235 -
2236 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
2237 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
2238 - if (indicator && indicator.textContent === 'Live Agent') {
2239 - return;
2240 - }
2241 -
2242 1444 var $chatBox = getElement(botId, 'chat-box');
2243 1445
2244 1446 // Remove any existing thinking dots in this bot's chat first
2245 1447 $chatBox.find('.thinking-dots').remove();
@@ -2261,9 +1463,9 @@
2261 1463 '</div>' +
2262 1464 '</div>';
2263 1465
2264 1466 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
2265 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1467 + var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
2266 1468 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
2267 1469 scrollToBottom(botId);
2268 1470 }
2269 1471
@@ -2269,11 +1471,9 @@
2269 1471
2270 1472 function removeThinkingDots(botId) {
2271 1473 botId = botId || 'default';
2272 1474 var $chatBox = getElement(botId, 'chat-box');
2273 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
2274 1475 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
2275 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
2276 1476 }
2277 1477
2278 1478 // ====================================
2279 1479 // TEXT FORMATTING & PROCESSING
@@ -2307,12 +1507,9 @@
2307 1507 processedText = formatTextStyling(processedText);
2308 1508
2309 1509 // Process code blocks BEFORE processing links
2310 1510 processedText = formatCodeBlocks(processedText);
2311 -
2312 - // Process markdown tables BEFORE converting newlines to paragraphs
2313 - processedText = formatMarkdownTables(processedText);
2314 -
1511 +
2315 1512 // NOW convert to paragraphs
2316 1513 processedText = convertNewlinesToBreaks(processedText);
2317 1514
2318 1515 // IMPORTANT: Handle citation-style brackets FIRST [URL]
@@ -2325,63 +1522,37 @@
2325 1522 // Return as a proper link without the brackets
2326 1523 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2327 1524 });
2328 1525
2329 - // Process markdown links: [text](url) and [](url)
2330 - // Uses balanced parenthesis matching to handle URLs containing parens
2331 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
2332 - processedText = (function(input) {
2333 - var result = '';
2334 - var i = 0;
2335 - while (i < input.length) {
2336 - // Look for [ at current position
2337 - if (input[i] === '[') {
2338 - // Find closing ]
2339 - var closeBracket = input.indexOf(']', i + 1);
2340 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
2341 - result += input[i];
2342 - i++;
2343 - continue;
2344 - }
2345 - var linkText = input.substring(i + 1, closeBracket);
2346 - // Check if URL starts with http
2347 - var urlStart = closeBracket + 2;
2348 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
2349 - result += input[i];
2350 - i++;
2351 - continue;
2352 - }
2353 - // Find balanced closing paren
2354 - var depth = 1;
2355 - var j = urlStart;
2356 - while (j < input.length && depth > 0) {
2357 - if (input[j] === '(') depth++;
2358 - else if (input[j] === ')') depth--;
2359 - if (depth > 0) j++;
2360 - }
2361 - if (depth !== 0) {
2362 - result += input[i];
2363 - i++;
2364 - continue;
2365 - }
2366 - var url = input.substring(urlStart, j);
2367 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
2368 - var encodedUrl = safeEncodeUrl(cleanUrl);
2369 - if (!linkText || !linkText.trim()) {
2370 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
2371 - } else {
2372 - var safeText = sanitizeUserInput(linkText);
2373 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
2374 - }
2375 - i = j + 1; // Skip past the closing )
2376 - } else {
2377 - result += input[i];
2378 - i++;
2379 - }
1526 + // Process proper markdown links with text: [text](url)
1527 + // This MUST have non-empty text in the first brackets
1528 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1529 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1530 + // Make sure we have actual text (not just whitespace)
1531 + if (!text || !text.trim()) {
1532 + // If no text, treat the URL as the text
1533 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1534 + const safeUrl = safeEncodeUrl(cleanUrl);
1535 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2380 1536 }
2381 - return result;
2382 - })(processedText);
1537 +
1538 + // Clean the URL
1539 + let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1540 + const safeUrl = safeEncodeUrl(cleanUrl);
1541 + const safeText = sanitizeUserInput(text);
1542 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1543 + });
2383 1544
1545 + // Handle empty markdown links: [](url)
1546 + // This is a specific case where there's no text
1547 + const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1548 + processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1549 + let cleanUrl = url.replace(/[.,;!?]+$/, '');
1550 + const safeUrl = safeEncodeUrl(cleanUrl);
1551 + // Use the URL itself as the link text
1552 + return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1553 + });
1554 +
2384 1555 // Process phone numbers: [text](tel:number)
2385 1556 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
2386 1557 processedText = processedText.replace(phonePattern, (match, text, phone) => {
2387 1558 const safePhone = safeEncodeUrl(phone);
@@ -2533,78 +1704,9 @@
2533 1704 });
2534 1705
2535 1706 return text;
2536 1707 }
2537 -
2538 - function formatMarkdownTables(text) {
2539 - var lines = text.split('\n');
2540 - var result = [];
2541 - var i = 0;
2542 -
2543 - while (i < lines.length) {
2544 - // Check for a table: current line has pipes AND next line is a separator row
2545 - if (i + 1 < lines.length &&
2546 - lines[i].indexOf('|') !== -1 &&
2547 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2548 -
2549 - var tableLines = [];
2550 - var headerLine = lines[i];
2551 - var separatorLine = lines[i + 1];
2552 - tableLines.push(headerLine);
2553 - tableLines.push(separatorLine);
2554 -
2555 - // Collect remaining table rows
2556 - var j = i + 2;
2557 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2558 - tableLines.push(lines[j]);
2559 - j++;
2560 - }
2561 -
2562 - // Parse alignment from separator row
2563 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2564 - var alignments = sepCells.map(function(cell) {
2565 - var trimmed = cell.trim();
2566 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2567 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2568 - return 'left';
2569 - });
2570 -
2571 - // Build HTML table
2572 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2573 -
2574 - // Header row
2575 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2576 - html += '<thead><tr>';
2577 - headerCells.forEach(function(cell, idx) {
2578 - var align = alignments[idx] || 'left';
2579 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2580 - });
2581 - html += '</tr></thead>';
2582 -
2583 - // Body rows
2584 - html += '<tbody>';
2585 - for (var r = 2; r < tableLines.length; r++) {
2586 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2587 - html += '<tr>';
2588 - rowCells.forEach(function(cell, idx) {
2589 - var align = alignments[idx] || 'left';
2590 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2591 - });
2592 - html += '</tr>';
2593 - }
2594 - html += '</tbody></table></div>';
2595 -
2596 - result.push(html);
2597 - i = j;
2598 - } else {
2599 - result.push(lines[i]);
2600 - i++;
2601 - }
2602 - }
2603 -
2604 - return result.join('\n');
2605 - }
2606 -
1708 +
2607 1709 function sanitizeUserInput(text) {
2608 1710 const div = document.createElement('div');
2609 1711 div.textContent = text;
2610 1712 return div.innerHTML;
@@ -2675,14 +1777,13 @@
2675 1777 requestAnimationFrame(smoothScroll);
2676 1778 }
2677 1779 }
2678 1780
2679 - function scrollElementToTop(element, botId, topOffset) {
1781 + function scrollElementToTop(element, botId) {
2680 1782 botId = botId || 'default';
2681 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2682 1783 var chatBox = getElement(botId, 'chat-box');
2683 1784 var elementTop = element.position().top + chatBox.scrollTop();
2684 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1785 + chatBox.animate({ scrollTop: elementTop }, 500);
2685 1786 }
2686 1787
2687 1788 function showChatWidget(botId) {
2688 1789 botId = botId || 'default';
@@ -2826,12 +1927,15 @@
2826 1927 // LIVE AGENT FUNCTIONALITY
2827 1928 // ====================================
2828 1929
2829 1930 function startPolling(botId) {
1931 + console.log('[MxChat] startPolling called for botId:', botId);
2830 1932 botId = botId || 'default';
2831 1933 var instance = MxChatInstances.get(botId);
2832 1934 // Clear any existing interval first
2833 1935 stopPolling(botId);
1936 + // Start new polling interval
1937 + console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
2834 1938 instance.pollingInterval = setInterval(function() {
2835 1939 checkForAgentMessages(botId);
2836 1940 }, 5000);
2837 1941 }
@@ -2836,17 +1940,20 @@
2836 1940 }, 5000);
2837 1941 }
2838 1942
2839 1943 function stopPolling(botId) {
1944 + console.log('[MxChat] stopPolling called for botId:', botId);
2840 1945 botId = botId || 'default';
2841 1946 var instance = MxChatInstances.get(botId);
2842 1947 if (instance.pollingInterval) {
2843 1948 clearInterval(instance.pollingInterval);
2844 1949 instance.pollingInterval = null;
1950 + console.log('[MxChat] Polling stopped for botId:', botId);
2845 1951 }
2846 1952 }
2847 1953
2848 1954 function checkForAgentMessages(botId) {
1955 + console.log('[MxChat] checkForAgentMessages called for botId:', botId);
2849 1956 botId = botId || 'default';
2850 1957 var instance = MxChatInstances.get(botId);
2851 1958 const sessionId = getChatSession(botId);
2852 1959 $.ajax({
@@ -2872,12 +1979,8 @@
2872 1979 instance.processedMessageIds.add(message.id);
2873 1980 }
2874 1981 });
2875 1982
2876 - if (hasNewMessage) {
2877 - enableChatInput(botId);
2878 - }
2879 -
2880 1983 var $floatingChatbot = getElement(botId, 'floating-chatbot');
2881 1984 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2882 1985 showNotification(botId);
2883 1986 }
@@ -2883,13 +1986,8 @@
2883 1986 }
2884 1987
2885 1988 scrollToBottom(botId, true);
2886 1989 }
2887 -
2888 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2889 - if (response.success && response.data?.chat_mode) {
2890 - updateChatModeIndicator(response.data.chat_mode, botId);
2891 - }
2892 1990 },
2893 1991 error: function (xhr, status, error) {
2894 1992 // Polling error - silently continue
2895 1993 }
@@ -2899,29 +1997,20 @@
2899 1997 // ====================================
2900 1998 // CHAT HISTORY & PERSISTENCE
2901 1999 // ====================================
2902 2000
2903 -function loadChatHistory(botId, onComplete) {
2001 +function loadChatHistory(botId) {
2904 2002 botId = botId || 'default';
2905 2003 var instance = MxChatInstances.get(botId);
2906 2004
2907 2005 // Prevent duplicate loading
2908 2006 if (instance.chatHistoryLoaded) {
2909 - if (onComplete) onComplete();
2910 2007 return;
2911 2008 }
2912 2009
2913 - // Use getChatSession which returns null if no session exists (does NOT create one)
2914 2010 var sessionId = getChatSession(botId);
2915 2011 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2916 2012
2917 - // No session yet — nothing to load. History will load after first message via ensureSession.
2918 - if (!sessionId) {
2919 - instance.chatHistoryLoaded = true;
2920 - if (onComplete) onComplete();
2921 - return;
2922 - }
2923 -
2924 2013 if (chatPersistenceEnabled && sessionId) {
2925 2014 $.ajax({
2926 2015 url: mxchatChat.ajax_url,
2927 2016 type: 'POST',
@@ -2932,12 +2021,11 @@
2932 2021 },
2933 2022 success: function(response) {
2934 2023 // Handle session reset (IP changed while user was away)
2935 2024 if (response.success === false && response.data && response.data.action === 'reset_session') {
2936 - // Silent reset — new session but don't clear UI
2937 - MxChatInstances.silentResetSession(botId);
2025 + // Silently reset session - user will start fresh
2026 + resetChatSession(botId);
2938 2027 instance.chatHistoryLoaded = true; // Prevent retry loop
2939 - if (onComplete) onComplete();
2940 2028 return;
2941 2029 }
2942 2030
2943 2031 // Check if the response indicates success
@@ -2993,19 +2081,9 @@
2993 2081 var content = message.content;
2994 2082 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2995 2083 content = decodeHTMLEntities(content);
2996 2084
2997 - // Skip linkify for messages containing structured HTML
2998 - // (forms, product cards, galleries, etc.) to avoid
2999 - // markdown formatting corrupting HTML attributes
3000 - // (e.g. underscores in name="field_name" becoming <em> tags)
3001 - if (content.includes("mxchat-product-card") ||
3002 - content.includes("mxchat-image-gallery") ||
3003 - content.includes("mxchat-featured-products") ||
3004 - content.includes("<form") ||
3005 - content.includes("<input") ||
3006 - content.includes("<select") ||
3007 - content.includes("<textarea")) {
2085 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
3008 2086 messageElement.html(content);
3009 2087 } else {
3010 2088 var formattedContent = linkify(content);
3011 2089 messageElement.html(formattedContent);
@@ -3045,17 +2123,13 @@
3045 2123 instance.chatHistoryLoaded = true;
3046 2124 }
3047 2125 }
3048 2126 }
3049 - if (onComplete) onComplete();
3050 2127 },
3051 2128 error: function(xhr, status, error) {
3052 2129 // Error loading chat history - silently continue
3053 - if (onComplete) onComplete();
3054 2130 }
3055 2131 });
3056 - } else {
3057 - if (onComplete) onComplete();
3058 2132 }
3059 2133 }
3060 2134
3061 2135
@@ -3119,10 +2193,10 @@
3119 2193 .then(data => {
3120 2194 if (data.success) {
3121 2195 container.style.display = 'none';
3122 2196 nameElement.textContent = '';
3123 - instance.activePdfFile = null;
3124 - appendMessage('bot', 'PDF removed.', '', [], false, botId);
2197 + activePdfFile = null;
2198 + appendMessage('bot', 'PDF removed.');
3125 2199 }
3126 2200 })
3127 2201 .catch(error => {
3128 2202 // Error removing PDF - silently continue
@@ -3128,16 +2202,14 @@
3128 2202 // Error removing PDF - silently continue
3129 2203 });
3130 2204 }
3131 2205
3132 - function removeActiveWord(botId) {
3133 - botId = botId || 'default';
3134 - var instance = MxChatInstances.get(botId);
3135 - const container = getElementDOM(botId, 'active-word-container');
3136 - const nameElement = getElementDOM(botId, 'active-word-name');
3137 -
3138 - if (!container || !nameElement || !instance.activeWordFile) return;
3139 -
2206 + function removeActiveWord() {
2207 + const container = document.getElementById('active-word-container');
2208 + const nameElement = document.getElementById('active-word-name');
2209 +
2210 + if (!container || !nameElement || !activeWordFile) return;
2211 +
3140 2212 fetch(mxchatChat.ajax_url, {
3141 2213 method: 'POST',
3142 2214 headers: {
3143 2215 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3143,9 +2215,9 @@
3143 2215 'Content-Type': 'application/x-www-form-urlencoded',
3144 2216 },
3145 2217 body: new URLSearchParams({
3146 2218 'action': 'mxchat_remove_word',
3147 - 'session_id': getChatSession(botId),
2219 + 'session_id': sessionId,
3148 2220 'nonce': mxchatChat.nonce
3149 2221 })
3150 2222 })
3151 2223 .then(response => response.json())
@@ -3152,10 +2224,10 @@
3152 2224 .then(data => {
3153 2225 if (data.success) {
3154 2226 container.style.display = 'none';
3155 2227 nameElement.textContent = '';
3156 - instance.activeWordFile = null;
3157 - appendMessage('bot', 'Word document removed.', '', [], false, botId);
2228 + activeWordFile = null;
2229 + appendMessage('bot', 'Word document removed.');
3158 2230 }
3159 2231 })
3160 2232 .catch(error => {
3161 2233 // Error removing Word document - silently continue
@@ -3233,35 +2305,45 @@
3233 2305 // ====================================
3234 2306
3235 2307 function checkPreChatDismissal(botId) {
3236 2308 botId = botId || 'default';
3237 - try {
3238 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
3239 - if (dismissedAt) {
3240 - // Re-show after 24 hours
3241 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
3242 - if (elapsed < 86400000) {
2309 + $.ajax({
2310 + url: mxchatChat.ajax_url,
2311 + type: 'POST',
2312 + data: {
2313 + action: 'mxchat_check_pre_chat_message_status',
2314 + _ajax_nonce: mxchatChat.nonce
2315 + },
2316 + success: function(response) {
2317 + if (response.success && !response.data.dismissed) {
2318 + getElement(botId, 'pre-chat-message').fadeIn(250);
2319 + } else {
3243 2320 getElement(botId, 'pre-chat-message').hide();
3244 - return;
3245 2321 }
3246 - // Expired — clear and show again
3247 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
2322 + },
2323 + error: function() {
2324 + // Error checking pre-chat dismissal - silently continue
3248 2325 }
3249 - getElement(botId, 'pre-chat-message').fadeIn(250);
3250 - } catch (e) {
3251 - // localStorage unavailable — show the message
3252 - getElement(botId, 'pre-chat-message').fadeIn(250);
3253 - }
2326 + });
3254 2327 }
3255 2328
3256 2329 function handlePreChatDismissal(botId) {
3257 2330 botId = botId || 'default';
3258 2331 getElement(botId, 'pre-chat-message').fadeOut(200);
3259 - try {
3260 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
3261 - } catch (e) {
3262 - // localStorage unavailable — dismissal won't persist
3263 - }
2332 + $.ajax({
2333 + url: mxchatChat.ajax_url,
2334 + type: 'POST',
2335 + data: {
2336 + action: 'mxchat_dismiss_pre_chat_message',
2337 + _ajax_nonce: mxchatChat.nonce
2338 + },
2339 + success: function() {
2340 + $('#pre-chat-message').hide();
2341 + },
2342 + error: function() {
2343 + // Error dismissing pre-chat message - silently continue
2344 + }
2345 + });
3264 2346 }
3265 2347
3266 2348
3267 2349 // ====================================
@@ -3316,14 +2398,9 @@
3316 2398 collapseQuickQuestions(botId);
3317 2399 });
3318 2400
3319 2401 // Chatbot visibility toggle handlers - use class selector for multi-instance support
3320 - // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
3321 - $(document).on('click keydown', '.floating-chatbot-button', function(e) {
3322 - if (e.type === 'keydown') {
3323 - if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
3324 - e.preventDefault();
3325 - }
2402 + $(document).on('click', '.floating-chatbot-button', function() {
3326 2403 var botId = getBotIdFromElement(this);
3327 2404 var $chatbot = getElement(botId, 'floating-chatbot');
3328 2405 var $badge = getElement(botId, 'chat-notification-badge');
3329 2406 var $preChat = getElement(botId, 'pre-chat-message');
@@ -3328,88 +2405,35 @@
3328 2405 var $badge = getElement(botId, 'chat-notification-badge');
3329 2406 var $preChat = getElement(botId, 'pre-chat-message');
3330 2407
3331 2408 if ($chatbot.hasClass('hidden')) {
3332 - $chatbot.removeClass('hidden').addClass('visible')
3333 - .attr('aria-modal', 'true').attr('role', 'dialog');
3334 - $(this).addClass('hidden').attr('aria-expanded', 'true');
2409 + $chatbot.removeClass('hidden').addClass('visible');
2410 + $(this).addClass('hidden');
3335 2411 $badge.hide(); // Hide notification when opening chat
3336 2412 disableScroll();
3337 2413 $preChat.fadeOut(250);
3338 -
3339 - // First open per page load: re-fetch behavior settings in case
3340 - // this page's inline values came from a stale full-page cache
3341 - // (plan-32db95). Idempotent — later opens are a no-op.
3342 - mxchatRefreshDynamicSettings();
3343 -
3344 - // Load chat history for returning visitors (persistence)
3345 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3346 - if (chatPersistenceEnabled) {
3347 - MxChatInstances.ensureSession(botId);
3348 - }
3349 -
3350 - // Deferred email check — only on first widget open
3351 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3352 - var instance = MxChatInstances.get(botId);
3353 - if (emailBlocker && !instance.emailCheckDone) {
3354 - instance.emailCheckDone = true;
3355 - resolveEmailState(botId);
3356 - } else if (!emailBlocker) {
3357 - // No email collection — still route through showChatContainerForBot
3358 - // so the loader is shown while chat history loads
3359 - showChatContainerForBot(botId);
3360 - }
3361 -
3362 - // Move keyboard focus into the message input after the open transition.
3363 - setTimeout(function() {
3364 - var chatInput = getElementDOM(botId, 'chat-input');
3365 - if (chatInput && !chatInput.disabled) {
3366 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
3367 - }
3368 - }, 300);
3369 2414 } else {
3370 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3371 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2415 + $chatbot.removeClass('visible').addClass('hidden');
2416 + $(this).removeClass('hidden');
3372 2417 enableScroll();
3373 2418 checkPreChatDismissal(botId);
3374 2419 }
3375 2420 });
3376 2421
3377 - // Allow clicking anywhere on the title bar to close the chatbot.
3378 - // Returns keyboard focus to the launcher so keyboard users don't get
3379 - // stranded at <body> (WCAG SC 2.4.3 Focus Order). :focus-visible is
3380 - // heuristic-based so mouse-triggered close won't show a focus ring.
2422 + // Allow clicking anywhere on the title bar to close the chatbot
3381 2423 $(document).on('click', '.chatbot-top-bar', function() {
3382 2424 var botId = getBotIdFromElement(this);
3383 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3384 - var $launcher = getElement(botId, 'floating-chatbot-button');
3385 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
2425 + getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2426 + getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3386 2427 enableScroll();
3387 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3388 2428 });
3389 2429
3390 - // Global Escape-key handler — closes any visible chat widget and
3391 - // returns focus to its launcher. Standard modal-dismissal pattern;
3392 - // pairs with aria-modal="true" set on the widget when it opens.
3393 - $(document).on('keydown', function(e) {
3394 - if (e.key !== 'Escape' && e.key !== 'Esc') return;
3395 - var $visible = $('.floating-chatbot.visible');
3396 - if (!$visible.length) return;
3397 - e.preventDefault();
3398 - $visible.each(function() {
3399 - var botId = getBotIdFromElement(this);
3400 - $(this).addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3401 - var $launcher = getElement(botId, 'floating-chatbot-button');
3402 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
3403 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3404 - });
3405 - enableScroll();
3406 - });
3407 -
3408 2430 $(document).on('click', '.close-pre-chat-message', function(e) {
3409 2431 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3410 2432 var botId = getBotIdFromElement(this);
3411 - handlePreChatDismissal(botId);
2433 + getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2434 + $(this).remove();
2435 + });
3412 2436 });
3413 2437
3414 2438
3415 2439 // PDF upload button handlers - use class selector
@@ -3425,20 +2449,17 @@
3425 2449 var wordInput = getElementDOM(botId, 'word-upload');
3426 2450 if (wordInput) wordInput.click();
3427 2451 });
3428 2452
3429 - // PDF file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'pdf-upload')
3430 - $(document).on('change', '.pdf-upload', async function(e) {
3431 - var botId = getBotIdFromElement(this);
3432 - var instance = MxChatInstances.get(botId);
3433 - const file = this.files[0];
3434 - const sessionId = MxChatInstances.ensureSession(botId);
3435 -
2453 + // PDF file input change handler
2454 + addSafeEventListener('pdf-upload', 'change', async function(e) {
2455 + const file = e.target.files[0];
2456 +
3436 2457 if (!file || file.type !== 'application/pdf') {
3437 2458 alert('Please select a valid PDF file.');
3438 2459 return;
3439 2460 }
3440 -
2461 +
3441 2462 if (!sessionId) {
3442 2463 alert('Error: No session ID found');
3443 2464 return;
3444 2465 }
@@ -3446,49 +2467,47 @@
3446 2467 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3447 2468 alert('Error: Ajax configuration missing');
3448 2469 return;
3449 2470 }
3450 -
2471 +
3451 2472 // Disable buttons and show loading state
3452 - const uploadBtn = getElementDOM(botId, 'pdf-upload-btn');
3453 - const sendBtn = getElementDOM(botId, 'send-button');
3454 - if (!uploadBtn) return;
2473 + const uploadBtn = document.getElementById('pdf-upload-btn');
2474 + const sendBtn = document.getElementById('send-button');
3455 2475 const originalBtnContent = uploadBtn.innerHTML;
3456 -
2476 +
3457 2477 try {
3458 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3459 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3460 2478 const formData = new FormData();
3461 2479 formData.append('action', 'mxchat_upload_pdf');
3462 2480 formData.append('pdf_file', file);
3463 2481 formData.append('session_id', sessionId);
3464 2482 formData.append('nonce', mxchatChat.nonce);
3465 -
2483 +
3466 2484 uploadBtn.disabled = true;
3467 - if (sendBtn) sendBtn.disabled = true;
2485 + sendBtn.disabled = true;
3468 2486 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3469 2487 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3470 2488 </svg>`;
3471 -
2489 +
3472 2490 const response = await fetch(mxchatChat.ajax_url, {
3473 2491 method: 'POST',
3474 2492 body: formData
3475 2493 });
3476 -
2494 +
3477 2495 const data = await response.json();
3478 -
2496 +
3479 2497 if (data.success) {
3480 2498 // Hide popular questions if they exist
3481 - if (hasQuickQuestions(botId)) {
3482 - collapseQuickQuestions(botId);
2499 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2500 + if (hasQuickQuestions()) {
2501 + collapseQuickQuestions();
3483 2502 }
3484 -
2503 +
3485 2504 // Show the active PDF name
3486 - showActivePdf(data.data.filename, botId);
3487 -
3488 - appendMessage('bot', data.data.message, '', [], false, botId);
3489 - scrollToBottom(botId);
3490 - instance.activePdfFile = data.data.filename;
2505 + showActivePdf(data.data.filename);
2506 +
2507 + appendMessage('bot', data.data.message);
2508 + scrollToBottom();
2509 + activePdfFile = data.data.filename;
3491 2510 } else {
3492 2511 alert('Failed to upload PDF. Please try again.');
3493 2512 }
3494 2513 } catch (error) {
@@ -3494,76 +2513,66 @@
3494 2513 } catch (error) {
3495 2514 alert('Error uploading file. Please try again.');
3496 2515 } finally {
3497 2516 uploadBtn.disabled = false;
3498 - if (sendBtn) sendBtn.disabled = false;
2517 + sendBtn.disabled = false;
3499 2518 uploadBtn.innerHTML = originalBtnContent;
3500 2519 this.value = ''; // Reset file input
3501 2520 }
3502 2521 });
3503 2522
3504 - // Word file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'word-upload')
3505 - $(document).on('change', '.word-upload', async function(e) {
3506 - var botId = getBotIdFromElement(this);
3507 - var instance = MxChatInstances.get(botId);
3508 - const file = this.files[0];
3509 - const sessionId = MxChatInstances.ensureSession(botId);
3510 -
2523 + // Word file input change handler
2524 + addSafeEventListener('word-upload', 'change', async function(e) {
2525 + const file = e.target.files[0];
2526 +
3511 2527 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
3512 2528 alert('Please select a valid Word document (.docx).');
3513 2529 return;
3514 2530 }
3515 -
2531 +
3516 2532 if (!sessionId) {
3517 2533 alert('Error: No session ID found');
3518 2534 return;
3519 2535 }
3520 2536
3521 - if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3522 - alert('Error: Ajax configuration missing');
3523 - return;
3524 - }
3525 -
3526 2537 // Disable buttons and show loading state
3527 - const uploadBtn = getElementDOM(botId, 'word-upload-btn');
3528 - const sendBtn = getElementDOM(botId, 'send-button');
3529 - if (!uploadBtn) return;
2538 + const uploadBtn = document.getElementById('word-upload-btn');
2539 + const sendBtn = document.getElementById('send-button');
3530 2540 const originalBtnContent = uploadBtn.innerHTML;
3531 -
2541 +
3532 2542 try {
3533 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3534 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3535 2543 const formData = new FormData();
3536 2544 formData.append('action', 'mxchat_upload_word');
3537 2545 formData.append('word_file', file);
3538 2546 formData.append('session_id', sessionId);
3539 2547 formData.append('nonce', mxchatChat.nonce);
3540 -
2548 +
3541 2549 uploadBtn.disabled = true;
3542 - if (sendBtn) sendBtn.disabled = true;
2550 + sendBtn.disabled = true;
3543 2551 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3544 2552 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3545 2553 </svg>`;
3546 -
2554 +
3547 2555 const response = await fetch(mxchatChat.ajax_url, {
3548 2556 method: 'POST',
3549 2557 body: formData
3550 2558 });
3551 -
2559 +
3552 2560 const data = await response.json();
3553 -
2561 +
3554 2562 if (data.success) {
3555 2563 // Hide popular questions if they exist
3556 - if (hasQuickQuestions(botId)) {
3557 - collapseQuickQuestions(botId);
2564 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2565 + if (hasQuickQuestions()) {
2566 + collapseQuickQuestions();
3558 2567 }
3559 -
2568 +
3560 2569 // Show the active Word document name
3561 - showActiveWord(data.data.filename, botId);
3562 -
3563 - appendMessage('bot', data.data.message, '', [], false, botId);
3564 - scrollToBottom(botId);
3565 - instance.activeWordFile = data.data.filename;
2570 + showActiveWord(data.data.filename);
2571 +
2572 + appendMessage('bot', data.data.message);
2573 + scrollToBottom();
2574 + activeWordFile = data.data.filename;
3566 2575 } else {
3567 2576 alert('Failed to upload Word document. Please try again.');
3568 2577 }
3569 2578 } catch (error) {
@@ -3569,25 +2578,25 @@
3569 2578 } catch (error) {
3570 2579 alert('Error uploading file. Please try again.');
3571 2580 } finally {
3572 2581 uploadBtn.disabled = false;
3573 - if (sendBtn) sendBtn.disabled = false;
2582 + sendBtn.disabled = false;
3574 2583 uploadBtn.innerHTML = originalBtnContent;
3575 2584 this.value = ''; // Reset file input
3576 2585 }
3577 2586 });
3578 2587
3579 - // Remove button click handlers - delegated, bot-aware (were bound to stale un-suffixed ids)
3580 - $(document).on('click', '.remove-pdf-btn', function(e) {
2588 + // Remove button click handlers
2589 + document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
3581 2590 e.preventDefault();
3582 2591 e.stopPropagation();
3583 - removeActivePdf(getBotIdFromElement(this));
2592 + removeActivePdf();
3584 2593 });
3585 -
3586 - $(document).on('click', '.remove-word-btn', function(e) {
2594 +
2595 + document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
3587 2596 e.preventDefault();
3588 2597 e.stopPropagation();
3589 - removeActiveWord(getBotIdFromElement(this));
2598 + removeActiveWord();
3590 2599 });
3591 2600
3592 2601 // Window resize handlers
3593 2602 $(window).on('resize orientationchange', function() {
@@ -3625,59 +2634,8 @@
3625 2634 });
3626 2635
3627 2636
3628 2637 // ====================================
3629 -// INIT LOADER & CHAT CONTAINER HELPERS
3630 -// ====================================
3631 -// These must be outside the email collection block so they're always available
3632 -// (used by persistence loading even when email collection is off)
3633 -
3634 -function showInitLoader(botId) {
3635 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3636 - if (loader) loader.style.display = 'flex';
3637 -}
3638 -
3639 -function hideInitLoader(botId) {
3640 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3641 - if (loader) loader.style.display = 'none';
3642 -}
3643 -
3644 -function showEmailFormForBot(botId) {
3645 - hideInitLoader(botId);
3646 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3647 - var chatContainer = getElementDOM(botId, 'chat-container');
3648 - if (emailBlocker) emailBlocker.style.display = 'flex';
3649 - if (chatContainer) chatContainer.style.display = 'none';
3650 -}
3651 -
3652 -function showChatContainerForBot(botId) {
3653 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3654 - var chatContainer = getElementDOM(botId, 'chat-container');
3655 - if (emailBlocker) emailBlocker.style.display = 'none';
3656 -
3657 - var instance = MxChatInstances.get(botId);
3658 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3659 -
3660 - // If persistence is on and history hasn't loaded yet, show loader
3661 - // while history loads to prevent flash of empty chat
3662 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3663 - if (chatContainer) chatContainer.style.display = 'none';
3664 - showInitLoader(botId);
3665 - loadChatHistory(botId, function() {
3666 - hideInitLoader(botId);
3667 - if (chatContainer) chatContainer.style.display = 'flex';
3668 - scrollToBottom(botId, true);
3669 - });
3670 - } else {
3671 - hideInitLoader(botId);
3672 - if (chatContainer) chatContainer.style.display = 'flex';
3673 - if (typeof loadChatHistory === 'function') {
3674 - loadChatHistory(botId);
3675 - }
3676 - }
3677 -}
3678 -
3679 -// ====================================
3680 2638 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3681 2639 // ====================================
3682 2640 // Only run email collection setup if it's enabled
3683 2641 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3713,8 +2671,28 @@
3713 2671 `;
3714 2672 document.head.appendChild(style);
3715 2673 }
3716 2674
2675 + // Helper functions for email collection (multi-instance aware)
2676 + function showEmailFormForBot(botId) {
2677 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2678 + var chatContainer = getElementDOM(botId, 'chat-container');
2679 + if (emailBlocker) emailBlocker.style.display = 'flex';
2680 + if (chatContainer) chatContainer.style.display = 'none';
2681 + }
2682 +
2683 + function showChatContainerForBot(botId) {
2684 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2685 + var chatContainer = getElementDOM(botId, 'chat-container');
2686 + if (emailBlocker) emailBlocker.style.display = 'none';
2687 + if (chatContainer) chatContainer.style.display = 'flex';
2688 +
2689 + // Load chat history for this bot
2690 + if (typeof loadChatHistory === 'function') {
2691 + loadChatHistory(botId);
2692 + }
2693 + }
2694 +
3717 2695 function isValidEmailAddress(email) {
3718 2696 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3719 2697 return emailRegex.test(email.trim()) && email.length <= 254;
3720 2698 }
@@ -3722,41 +2700,8 @@
3722 2700 function isValidNameInput(name) {
3723 2701 return name && name.trim().length >= 2 && name.trim().length <= 100;
3724 2702 }
3725 2703
3726 - /**
3727 - * Replace {visitor_name} placeholder in intro message with actual visitor name
3728 - * @param {string} botId - The bot instance ID
3729 - * @param {string} visitorName - The visitor's name to insert
3730 - */
3731 - function replaceVisitorNamePlaceholder(botId, visitorName) {
3732 - var chatBox = getElementDOM(botId, 'chat-box');
3733 - if (!chatBox) return;
3734 -
3735 - // Find the first bot message (intro message)
3736 - var introMessage = chatBox.querySelector('.bot-message');
3737 - if (!introMessage) return;
3738 -
3739 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3740 - if (!messageContent) return;
3741 -
3742 - var html = messageContent.innerHTML;
3743 -
3744 - // Replace {visitor_name} placeholder (case-insensitive)
3745 - if (visitorName && visitorName.trim()) {
3746 - // Escape HTML to prevent XSS
3747 - var safeName = $('<div>').text(visitorName.trim()).html();
3748 - html = html.replace(/\{visitor_name\}/gi, safeName);
3749 - } else {
3750 - // Remove placeholder and clean up spacing if no name provided
3751 - html = html.replace(/\{visitor_name\}/gi, '');
3752 - // Clean up any double spaces that might result
3753 - html = html.replace(/\s{2,}/g, ' ').trim();
3754 - }
3755 -
3756 - messageContent.innerHTML = html;
3757 - }
3758 -
3759 2704 function setEmailSubmissionState(botId, loading) {
3760 2705 var submitButton = getElementDOM(botId, 'email-submit-button');
3761 2706 var emailInput = getElementDOM(botId, 'user-email');
3762 2707 var nameInput = getElementDOM(botId, 'user-name');
@@ -3836,31 +2781,11 @@
3836 2781 existingErrors.forEach(error => error.remove());
3837 2782 }
3838 2783 }
3839 2784
3840 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3841 - function resolveEmailState(botId) {
3842 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3843 - if (mxchatChat.initial_email_state.show_email_form) {
3844 - showEmailFormForBot(botId);
3845 - } else {
3846 - showChatContainerForBot(botId);
3847 - }
3848 - } else {
3849 - checkSessionAndEmailForBot(botId);
3850 - }
3851 - }
3852 -
3853 2785 function checkSessionAndEmailForBot(botId) {
3854 - const sessionId = MxChatInstances.ensureSession(botId);
2786 + const sessionId = getChatSession(botId);
3855 2787
3856 - // Hide both panels while we check — show loader instead
3857 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3858 - var chatContainer = getElementDOM(botId, 'chat-container');
3859 - if (emailBlocker) emailBlocker.style.display = 'none';
3860 - if (chatContainer) chatContainer.style.display = 'none';
3861 - showInitLoader(botId);
3862 -
3863 2788 fetch(mxchatChat.ajax_url, {
3864 2789 method: 'POST',
3865 2790 headers: {
3866 2791 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3908,9 +2833,9 @@
3908 2833 var emailInput = getElementDOM(botId, 'user-email');
3909 2834 var nameInput = getElementDOM(botId, 'user-name');
3910 2835 var userEmail = emailInput ? emailInput.value.trim() : '';
3911 2836 var userName = nameInput ? nameInput.value.trim() : '';
3912 - var sessionId = MxChatInstances.ensureSession(botId);
2837 + var sessionId = getChatSession(botId);
3913 2838
3914 2839 // Validate email
3915 2840 if (!userEmail) {
3916 2841 showEmailError(botId, 'Please enter your email address.');
@@ -3961,16 +2886,8 @@
3961 2886
3962 2887 if (data.success) {
3963 2888 showChatContainerForBot(botId);
3964 2889
3965 - // Replace {visitor_name} placeholder in intro message with actual name
3966 - if (userName) {
3967 - replaceVisitorNamePlaceholder(botId, userName);
3968 - } else {
3969 - // Remove placeholder if no name provided
3970 - replaceVisitorNamePlaceholder(botId, '');
3971 - }
3972 -
3973 2890 if (data.message && typeof appendMessage === 'function') {
3974 2891 setTimeout(() => {
3975 2892 appendMessage('bot', data.message, '', [], false, botId);
3976 2893 if (typeof scrollToBottom === 'function') {
@@ -4033,27 +2950,25 @@
4033 2950 }
4034 2951 });
4035 2952
4036 2953 // Initialize email check for all bot instances
4037 - // For floating bots: defer until widget is opened (zero passive AJAX)
4038 - // For embedded bots: check immediately since the form is visible
4039 2954 $('.mxchat-chatbot-wrapper').each(function() {
4040 2955 var botId = $(this).data('bot-id') || 'default';
4041 2956 var emailBlocker = getElementDOM(botId, 'email-blocker');
4042 2957
2958 + // Only check if email blocker exists for this bot
4043 2959 if (emailBlocker) {
4044 - if (isEmbeddedBot(botId)) {
4045 - // Embedded bots are always visible — check now
4046 - resolveEmailState(botId);
2960 + if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2961 + if (mxchatChat.initial_email_state.show_email_form) {
2962 + showEmailFormForBot(botId);
2963 + } else {
2964 + showChatContainerForBot(botId);
2965 + }
2966 + } else {
2967 + setTimeout(function() {
2968 + checkSessionAndEmailForBot(botId);
2969 + }, 100);
4047 2970 }
4048 - // Floating bots: handled in the widget open handler
4049 - } else if (isEmbeddedBot(botId)) {
4050 - // Embedded bot, no email collection — load history with loader
4051 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
4052 - if (chatPersistenceEnabled) {
4053 - MxChatInstances.ensureSession(botId);
4054 - showChatContainerForBot(botId);
4055 - }
4056 2971 }
4057 2972 });
4058 2973 }
4059 2974
@@ -4063,32 +2978,39 @@
4063 2978 var $chatbot = getElement(botId, 'floating-chatbot');
4064 2979 if ($chatbot.hasClass('hidden')) {
4065 2980 $chatbot.removeClass('hidden').addClass('visible');
4066 2981 getElement(botId, 'floating-chatbot-button').addClass('hidden');
4067 - handlePreChatDismissal(botId);
2982 + $(this).fadeOut(250); // Hide pre-chat message
4068 2983 disableScroll(); // Disable scroll when chatbot opens
2984 + }
2985 + });
4069 2986
4070 - // Load chat history for returning visitors (persistence)
4071 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
4072 - if (chatPersistenceEnabled) {
4073 - MxChatInstances.ensureSession(botId);
4074 - }
2987 + // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
2988 + // This is a fallback for legacy support
2989 + $(document).on('click', '.close-pre-chat-message', function() {
2990 + var botId = getBotIdFromElement(this);
2991 + var $preChat = getElement(botId, 'pre-chat-message');
2992 + $preChat.fadeOut(200); // Hide the message
4075 2993
4076 - // Deferred email check — only on first widget open
4077 - var emailBlocker = getElementDOM(botId, 'email-blocker');
4078 - var instance = MxChatInstances.get(botId);
4079 - if (emailBlocker && !instance.emailCheckDone) {
4080 - instance.emailCheckDone = true;
4081 - resolveEmailState(botId);
4082 - } else if (!emailBlocker) {
4083 - showChatContainerForBot(botId);
2994 + // Send an AJAX request to set the transient flag for 24 hours
2995 + $.ajax({
2996 + url: mxchatChat.ajax_url,
2997 + type: 'POST',
2998 + data: {
2999 + action: 'mxchat_dismiss_pre_chat_message',
3000 + _ajax_nonce: mxchatChat.nonce
3001 + },
3002 + success: function() {
3003 + // Ensure the message is hidden after dismissal
3004 + $preChat.hide();
3005 + },
3006 + error: function() {
3007 + // Error dismissing pre-chat message - silently continue
4084 3008 }
4085 - }
3009 + });
4086 3010 });
4087 3011
4088 - // Legacy duplicate close handler removed — handled by single event delegation above
4089 3012
4090 -
4091 3013 function hasQuickQuestions(botId) {
4092 3014 botId = botId || 'default';
4093 3015 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
4094 3016 if (!questionsContainer) return false;
@@ -4224,11 +3146,18 @@
4224 3146 });
4225 3147
4226 3148 // Initialize when document is ready
4227 3149 setFullHeight();
3150 + trackOriginatingPage();
4228 3151
4229 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
4230 - // until the user's first interaction via MxChatInstances.ensureSession()
3152 + // Only load chat history if email collection is disabled
3153 + if (mxchatChat.email_collection_enabled !== 'on') {
3154 + // Load history for all instances
3155 + $('.mxchat-chatbot-wrapper').each(function() {
3156 + var botId = $(this).data('bot-id') || 'default';
3157 + loadChatHistory(botId);
3158 + });
3159 + }
4231 3160
4232 3161 // Initialize chat visibility for all instances
4233 3162 $('.mxchat-chatbot-wrapper').each(function() {
4234 3163 var botId = $(this).data('bot-id') || 'default';
@@ -4281,312 +3210,6 @@
4281 3210 }, 2000);
4282 3211 });
4283 3212 }
4284 3213 }
4285 -});
4286 -
4287 -// ============================================================================
4288 -// SATISFACTION RATING (v3.2.6)
4289 -// ============================================================================
4290 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
4291 -// inactivity following a bot reply. One prompt per session, deduped via
4292 -// localStorage. Runs ONLY when the satisfaction_rating_enabled option is on —
4293 -// the option (default off) is authoritative.
4294 -jQuery(function($) {
4295 - if (typeof mxchatChat === 'undefined') return;
4296 - // wp_localize_script stringifies scalars: a PHP boolean false arrives as
4297 - // '' and true as '1', so this must be an explicit-enable allowlist — the
4298 - // old "disabled when exactly false/'off'" check let '' through and the
4299 - // bubble rendered on sites with the option off/unset (plan-4bba64). PHP
4300 - // now emits 'on'/'off' strings; true/'1'/1 keep cached pre-fix HTML
4301 - // (boolean-true localizations) working.
4302 - // NOTE (plan-32db95): this gate reads the INLINE value at DOM ready and is
4303 - // deliberately NOT re-evaluated after the widget's dynamic-settings refresh
4304 - // merges fresh values over mxchatChat (that merge fires on first widget
4305 - // open, after this module has already decided). Re-evaluating would mean
4306 - // restructuring the whole module to late-bind its listeners — not worth it
4307 - // for a prompt that is at worst stale for one page load on a cached page.
4308 - var sre = mxchatChat.satisfaction_rating_enabled;
4309 - if (sre !== 'on' && sre !== true && sre !== '1' && sre !== 1) return;
4310 -
4311 - // wp_localize_script stringifies ints, so accept both number and numeric string.
4312 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
4313 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
4314 - if (!isFinite(idleSeconds)) idleSeconds = 60;
4315 - if (idleSeconds < 5) idleSeconds = 5;
4316 - if (idleSeconds > 600) idleSeconds = 600;
4317 - var IDLE_MS = idleSeconds * 1000;
4318 - var MIN_BOT_REPLIES = 2;
4319 - var ratingState = {};
4320 -
4321 - function getState(botId) {
4322 - if (!ratingState[botId]) {
4323 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
4324 - }
4325 - return ratingState[botId];
4326 - }
4327 -
4328 - function getSessionId(botId) {
4329 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
4330 - return MxChatInstances.getChatSession(botId);
4331 - }
4332 - return null;
4333 - }
4334 -
4335 - function isAlreadyRated(sessionId) {
4336 - if (!sessionId) return false;
4337 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
4338 - }
4339 -
4340 - function markRated(sessionId) {
4341 - if (!sessionId) return;
4342 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
4343 - }
4344 -
4345 - function esc(s) {
4346 - return String(s == null ? '' : s)
4347 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
4348 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
4349 - }
4350 -
4351 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
4352 - function ratingSkipInlineColors(botId) {
4353 - if (mxchatChat.skip_inline_colors) return true;
4354 - var botAssignments = mxchatChat.bot_theme_assignments || {};
4355 - return botAssignments.hasOwnProperty(botId);
4356 - }
4357 -
4358 - function botBubbleStyleAttr(botId) {
4359 - if (ratingSkipInlineColors(botId)) return '';
4360 - var bg = mxchatChat.bot_message_bg_color;
4361 - var fg = mxchatChat.bot_message_font_color;
4362 - if (!bg && !fg) return '';
4363 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
4364 - }
4365 -
4366 - // Reads the rating bubble's actual computed fg+bg (whatever paints it —
4367 - // the inline color pickers OR the mxchat-theme AI customizer's injected CSS)
4368 - // and paints the filled "Send" pill so it fills with the bot font color and
4369 - // labels in the bubble bg. Mirrors mxchatSyncMenuColors(~:1512) for the read.
4370 - // We paint the submit button DIRECTLY (inline longhand) rather than relying
4371 - // on the CSS rule's var()s: Chromium resolves an INHERITED custom property
4372 - // unreliably inside a descendant's `background`, so a bubble-level var would
4373 - // silently fall back to the literal (white-block bug all over again). Inline
4374 - // longhand always wins. Same transparent-guard as the menu so we never paint
4375 - // a see-through value — in that case the CSS literal fallbacks keep it legible.
4376 - function syncRatingBubbleColors(botId) {
4377 - var $chatBox = getChatBoxByBotId(botId);
4378 - if (!$chatBox || !$chatBox.length) return;
4379 - var bubbleEl = $chatBox.find('.mxchat-rating-bot-bubble').last()[0];
4380 - if (!bubbleEl) return;
4381 - var cs = window.getComputedStyle(bubbleEl);
4382 - var fg = cs.color;
4383 - var bg = cs.backgroundColor;
4384 - var hasFg = fg && fg !== 'rgba(0, 0, 0, 0)' && fg !== 'transparent';
4385 - var hasBg = bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent';
4386 - // Expose on the bubble too, for any inheriting styles / future use.
4387 - if (hasFg) bubbleEl.style.setProperty('--mxchat-bot-fg', fg);
4388 - if (hasBg) bubbleEl.style.setProperty('--mxchat-bot-bg', bg);
4389 - // Paint the Send pill directly — the part that actually fixes the bug.
4390 - var submitEl = bubbleEl.querySelector('.mxchat-rating-submit');
4391 - if (submitEl) {
4392 - if (hasFg) submitEl.style.backgroundColor = fg; // fill = bot font color
4393 - if (hasBg) submitEl.style.color = bg; // label = bubble background
4394 - }
4395 - }
4396 -
4397 - function copy(key) {
4398 - var c = mxchatChat.satisfaction_rating_copy || {};
4399 - var d = {
4400 - question: 'Was this helpful?',
4401 - helpful: 'Helpful',
4402 - not_helpful: 'Not helpful',
4403 - dismiss: 'Dismiss',
4404 - thanks: 'Thanks! Anything we should improve? (optional)',
4405 - placeholder: 'Tell us what could be better…',
4406 - send: 'Send',
4407 - skip: 'Skip',
4408 - saved: 'Thanks for the feedback.'
4409 - };
4410 - return c[key] || d[key];
4411 - }
4412 -
4413 - function thumbUpSvg() {
4414 - 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>';
4415 - }
4416 - function thumbDownSvg() {
4417 - 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>';
4418 - }
4419 -
4420 - function buildPromptHtml(botId) {
4421 - var styleAttr = botBubbleStyleAttr(botId);
4422 - return ''
4423 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4424 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
4425 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
4426 - + '<div class="mxchat-rating-actions">'
4427 - + '<span class="mxchat-rating-buttons">'
4428 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
4429 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
4430 - + '</span>'
4431 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
4432 - + '</div>'
4433 - + '</div>'
4434 - + '</div>';
4435 - }
4436 -
4437 - function buildFeedbackHtml(botId, rating) {
4438 - var styleAttr = botBubbleStyleAttr(botId);
4439 - return ''
4440 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4441 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
4442 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
4443 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
4444 - + '<div class="mxchat-rating-feedback-actions">'
4445 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
4446 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
4447 - + '</div>'
4448 - + '</div>'
4449 - + '</div>';
4450 - }
4451 -
4452 - function buildSavedHtml(botId) {
4453 - var styleAttr = botBubbleStyleAttr(botId);
4454 - return ''
4455 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4456 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
4457 - + '</div>';
4458 - }
4459 -
4460 - function getChatBoxByBotId(botId) {
4461 - var $byId = $('#chat-box-' + botId);
4462 - if ($byId.length) return $byId.first();
4463 - return $('.chat-box').first();
4464 - }
4465 -
4466 - function scrollChatBoxToBottom($chatBox) {
4467 - if (!$chatBox || !$chatBox.length) return;
4468 - $chatBox.scrollTop($chatBox[0].scrollHeight);
4469 - }
4470 -
4471 - function showPrompt(botId) {
4472 - var s = getState(botId);
4473 - if (s.promptShown || s.dismissed) return;
4474 - var sessionId = getSessionId(botId);
4475 - if (!sessionId) return;
4476 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4477 - var $chatBox = getChatBoxByBotId(botId);
4478 - if (!$chatBox.length) return;
4479 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
4480 - $chatBox.append(buildPromptHtml(botId));
4481 - syncRatingBubbleColors(botId);
4482 - s.promptShown = true;
4483 - scrollChatBoxToBottom($chatBox);
4484 - }
4485 -
4486 - function submitRating(botId, rating, feedback) {
4487 - var sessionId = getSessionId(botId);
4488 - if (!sessionId) return;
4489 - $.post(mxchatChat.ajax_url, {
4490 - action: 'mxchat_save_rating',
4491 - session_id: sessionId,
4492 - bot_id: botId,
4493 - rating: rating,
4494 - feedback: feedback || ''
4495 - });
4496 - markRated(sessionId);
4497 - }
4498 -
4499 - function onBotReply(botId) {
4500 - var s = getState(botId);
4501 - s.botReplies += 1;
4502 - if (s.promptShown || s.dismissed) return;
4503 - var sessionId = getSessionId(botId);
4504 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4505 - if (s.botReplies < MIN_BOT_REPLIES) return;
4506 - if (s.idleTimer) clearTimeout(s.idleTimer);
4507 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4508 - }
4509 -
4510 - function onUserMessage(botId) {
4511 - var s = getState(botId);
4512 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4513 - }
4514 -
4515 - function botIdFromChatBox(el) {
4516 - var id = el && el.id ? el.id : '';
4517 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4518 - }
4519 -
4520 - function setupObserver(chatBox) {
4521 - var botId = botIdFromChatBox(chatBox);
4522 - try {
4523 - var observer = new MutationObserver(function(mutations) {
4524 - mutations.forEach(function(m) {
4525 - for (var i = 0; i < m.addedNodes.length; i++) {
4526 - var node = m.addedNodes[i];
4527 - if (!node || node.nodeType !== 1) continue;
4528 - var $n = $(node);
4529 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4530 - 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)
4531 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4532 - }
4533 - });
4534 - });
4535 - observer.observe(chatBox, { childList: true });
4536 - } catch (e) { /* noop */ }
4537 - }
4538 -
4539 - $('.chat-box').each(function() { setupObserver(this); });
4540 -
4541 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4542 - e.preventDefault();
4543 - var $btn = $(this);
4544 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4545 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4546 - var botId = $prompt.data('bot-id') || 'default';
4547 - var rating = parseInt($btn.attr('data-rating'), 10);
4548 - if (rating !== 1 && rating !== -1) return;
4549 - submitRating(botId, rating, '');
4550 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4551 - syncRatingBubbleColors(botId);
4552 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4553 - });
4554 -
4555 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4556 - e.preventDefault();
4557 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4558 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4559 - var botId = $prompt.data('bot-id') || 'default';
4560 - var s = getState(botId);
4561 - s.dismissed = true;
4562 - markRated(getSessionId(botId));
4563 - ($wrap.length ? $wrap : $prompt).remove();
4564 - });
4565 -
4566 - function closeFeedback($fb) {
4567 - var botId = $fb.data('bot-id') || 'default';
4568 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4569 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4570 - syncRatingBubbleColors(botId);
4571 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4572 - }
4573 -
4574 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4575 - e.preventDefault();
4576 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4577 - });
4578 -
4579 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4580 - e.preventDefault();
4581 - var $fb = $(this).closest('.mxchat-rating-feedback');
4582 - var botId = $fb.data('bot-id') || 'default';
4583 - var rating = parseInt($fb.attr('data-rating'), 10);
4584 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4585 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4586 - if (text !== '') {
4587 - submitRating(botId, rating, text);
4588 - }
4589 - closeFeedback($fb);
4590 - });
4591 3214 });
4592 3215