PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.8
MxChat – AI Chatbot & Content Generation for WordPress v3.2.8
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 +102 -637 3.2.213.2.8 View file →
@@ -104,63 +104,8 @@
104 104 function refreshNonceIfNeeded(callback) {
105 105 return withFreshNonce(callback);
106 106 }
107 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 108 // ====================================
164 109 // MULTI-INSTANCE MANAGEMENT SYSTEM
165 110 // ====================================
166 111
@@ -284,20 +229,9 @@
284 229 var newSessionId = generateSessionId();
285 230 this.setChatSession(botId, newSessionId);
286 231 var $chatBox = getElement(botId, 'chat-box');
287 232 if ($chatBox.length) {
288 - // Keep the greeting, drop everything else. Identify the greeting
289 - // by its marker class, NOT by position (plan a1a79b): after a
290 - // chat-persistence restore the first .bot-message is a real
291 - // reply, so ":not(:first)" left a stale answer sitting at the
292 - // top of an otherwise empty box. The positional fallback only
293 - // runs when the marker is absent — a page served from HTML cache
294 - // that predates this release — and behaves exactly as before.
295 - if ($chatBox.find('.mxchat-intro-message').length) {
296 - $chatBox.find('.user-message, .bot-message:not(.mxchat-intro-message), .agent-message').remove();
297 - } else {
298 - $chatBox.find('.user-message, .bot-message:not(:first), .agent-message').remove();
299 - }
233 + $chatBox.find('.user-message, .bot-message:not(:first), .agent-message').remove();
300 234 }
301 235 if (this.instances[botId]) {
302 236 this.instances[botId].chatHistoryLoaded = false;
303 237 this.instances[botId].processedMessageIds = new Set();
@@ -354,20 +288,8 @@
354 288 var id = $floating.attr('id') || '';
355 289 var match = id.match(/floating-chatbot-(.+)/);
356 290 if (match) return match[1];
357 291 }
358 - // Fallback: the pre-chat teaser bubble (#pre-chat-message-{bot_id}) is a SIBLING
359 - // outside .mxchat-chatbot-wrapper / .floating-chatbot, so its children — e.g. the
360 - // .close-pre-chat-message button, which carries only a class and no id — miss both
361 - // branches above. Walk to the nearest ancestor whose id is pre-chat-message-{bot_id}
362 - // and read the suffix. (closest() includes the element itself, so a click directly on
363 - // #pre-chat-message-{bot_id} resolves here too.)
364 - var $preChat = $(element).closest('[id^="pre-chat-message-"]');
365 - if ($preChat.length) {
366 - var preId = $preChat.attr('id') || '';
367 - var preMatch = preId.match(/^pre-chat-message-(.+)$/);
368 - if (preMatch) return preMatch[1];
369 - }
370 292 // Fallback: check if element itself has an ID with bot suffix (e.g., floating-chatbot-button-{bot_id})
371 293 var elementId = $(element).attr('id') || '';
372 294 if (elementId) {
373 295 // Match patterns like: floating-chatbot-button-{bot_id}, pre-chat-message-{bot_id}
@@ -407,27 +329,9 @@
407 329 if (parts.length == 2) return parts.pop().split(";").shift();
408 330 }
409 331
410 332 function generateSessionId() {
411 - // Session IDs function as the de-facto bearer token for an anonymous
412 - // chat, so generate them with a CSPRNG when available. Math.random is a
413 - // legacy fallback for ancient/sandboxed environments that lack
414 - // window.crypto. The 'mxchat_chat_' prefix is preserved exactly (other
415 - // code pattern-matches on it). (plan-0c17b5)
416 - var rand;
417 - try {
418 - if (window.crypto && window.crypto.getRandomValues) {
419 - var buf = new Uint8Array(16); // 128 bits
420 - window.crypto.getRandomValues(buf);
421 - rand = Array.prototype.map.call(buf, function (b) {
422 - return ('0' + b.toString(16)).slice(-2);
423 - }).join('');
424 - }
425 - } catch (e) {}
426 - if (!rand) {
427 - rand = Math.random().toString(36).substr(2, 9); // legacy fallback
428 - }
429 - return 'mxchat_chat_' + rand;
333 + return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
430 334 }
431 335
432 336 // Legacy function - now delegates to instance manager
433 337 function getChatSession(botId) {
@@ -630,26 +534,8 @@
630 534 sendButton.style.pointerEvents = 'none';
631 535 }
632 536 }
633 537
634 -// Whether the input may grab focus after a completed reply (plan 03799f).
635 -// On coarse-pointer devices focusing a text input summons the on-screen
636 -// keyboard over the answer the visitor is trying to read, so 'auto' (the
637 -// default) focuses only on fine-pointer devices. The site-wide
638 -// mxchat_autofocus_after_reply PHP filter can force 'on'/'off'.
639 -// NOT used on widget open (:~3424) — that focus is a deliberate act and is
640 -// what makes the widget keyboard-accessible.
641 -function mxchatShouldAutofocusAfterReply() {
642 - var pref = (typeof mxchatChat !== 'undefined' && mxchatChat.autofocus_after_reply) || 'auto';
643 - if (pref === 'on') return true;
644 - if (pref === 'off') return false;
645 - try {
646 - return !window.matchMedia('(pointer: coarse)').matches;
647 - } catch (err) {
648 - return true;
649 - }
650 -}
651 -
652 538 function enableChatInput(botId) {
653 539 botId = botId || 'default';
654 540 var chatInput = getElementDOM(botId, 'chat-input');
655 541 var sendButton = getElementDOM(botId, 'send-button');
@@ -655,11 +541,9 @@
655 541 var sendButton = getElementDOM(botId, 'send-button');
656 542 if (chatInput) {
657 543 chatInput.disabled = false;
658 544 chatInput.style.opacity = '1';
659 - if (mxchatShouldAutofocusAfterReply()) {
660 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
661 - }
545 + chatInput.focus();
662 546 }
663 547 if (sendButton) {
664 548 sendButton.disabled = false;
665 549 sendButton.style.opacity = '1';
@@ -664,102 +548,10 @@
664 548 sendButton.disabled = false;
665 549 sendButton.style.opacity = '1';
666 550 sendButton.style.pointerEvents = 'auto';
667 551 }
668 - // Every completion path re-enables input, so this is the single restore
669 - // point for the streaming Stop affordance (no-op when not in stop mode).
670 - mxchatRestoreSendButton(botId);
671 552 }
672 553
673 -// --- Streaming Stop control -------------------------------------------------
674 -// One live stream handle per bot instance, so Stop on one widget never aborts
675 -// another bot on the same page.
676 -var mxchatActiveStreams = {};
677 -// Original send-button markup, captured once per bot the first time the Stop
678 -// state is shown (never captured while already in stop mode, so a rapid
679 -// stop-then-resend can't save the stop glyph as the "original").
680 -var mxchatSendMarkup = {};
681 -
682 -function mxchatShowStopButton(botId) {
683 - var btn = getElementDOM(botId, 'send-button');
684 - if (!btn) return;
685 - if (!btn.classList.contains('mxchat-stop-mode')) {
686 - mxchatSendMarkup[botId] = {
687 - html: btn.innerHTML,
688 - label: btn.getAttribute('aria-label')
689 - };
690 - }
691 -
692 - // Mirror the send icon's rendered size + color so the stop glyph looks
693 - // native, including custom send images/colors and theme overrides.
694 - var child = btn.querySelector('svg, img');
695 - var size = 25;
696 - var color = '';
697 - if (child) {
698 - var rect = child.getBoundingClientRect();
699 - if (rect.width) {
700 - size = Math.round(Math.min(rect.width, rect.height));
701 - }
702 - var cs = window.getComputedStyle(child);
703 - color = (child.tagName.toLowerCase() === 'svg' ? cs.fill : cs.color) || '';
704 - }
705 - var stopLabel = (typeof mxchatChat !== 'undefined' && mxchatChat.stop_button_label) || 'Stop response';
706 - 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>';
707 - // An add-on's DIRECT send-button handler (e.g. mxchat-vision's rebind) can
708 - // start a stream synchronously while the originating click is still
709 - // bubbling up to our delegated handler. Without this guard, that handler
710 - // reads the just-added stop-mode class as a user Stop press and aborts the
711 - // brand-new stream — the user's message renders but no reply ever fires
712 - // (plan-4bba64 silent message loss). The flag only spans the current event
713 - // dispatch: cleared on the next macrotask, long before a real Stop click.
714 - btn.__mxchatStopJustShown = true;
715 - setTimeout(function () { btn.__mxchatStopJustShown = false; }, 0);
716 - btn.classList.add('mxchat-stop-mode');
717 - btn.setAttribute('aria-label', stopLabel);
718 - btn.setAttribute('title', stopLabel);
719 - // disableChatInput() ran when the turn was sent; the Stop control itself
720 - // must stay clickable while the textarea remains disabled.
721 - btn.disabled = false;
722 - btn.style.opacity = '1';
723 - btn.style.pointerEvents = 'auto';
724 -}
725 -
726 -function mxchatRestoreSendButton(botId) {
727 - var btn = getElementDOM(botId, 'send-button');
728 - var saved = mxchatSendMarkup[botId];
729 - if (!btn || !btn.classList.contains('mxchat-stop-mode') || !saved) return;
730 - btn.innerHTML = saved.html;
731 - btn.classList.remove('mxchat-stop-mode');
732 - btn.removeAttribute('title');
733 - if (saved.label) {
734 - btn.setAttribute('aria-label', saved.label);
735 - }
736 -}
737 -
738 -function mxchatStopStreaming(botId) {
739 - var entry = mxchatActiveStreams[botId];
740 - if (!entry || !entry.controller) return;
741 - entry.aborted = true;
742 - try { entry.controller.abort(); } catch (e) {}
743 -}
744 -
745 -// Returns true when a stream rejection came from an intentional Stop click:
746 -// keep the partial text as the turn's answer — no error UI, no fallback resend.
747 -function mxchatHandleStreamAbort(botId, accumulatedContent, callback) {
748 - var entry = mxchatActiveStreams[botId];
749 - if (!entry || !entry.aborted) return false;
750 - delete mxchatActiveStreams[botId];
751 - if (!accumulatedContent) {
752 - // Stopped before the first chunk: drop the thinking bubble, no orphan message.
753 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
754 - }
755 - enableChatInput(botId); // also restores the send icon
756 - if (callback) {
757 - callback(accumulatedContent || '');
758 - }
759 - return true;
760 -}
761 -
762 554 // Update your existing sendMessage function
763 555 function sendMessage(botId) {
764 556 botId = botId || 'default';
765 557 MxChatInstances.ensureSession(botId);
@@ -780,9 +572,8 @@
780 572 }
781 573
782 574 appendMessage("user", message, '', [], false, botId);
783 575 $chatInput.val('');
784 - mxchatUpdateCharCounter($chatInput[0]); // reset the char counter after send (plan 7091a2)
785 576 $chatInput.css('height', 'auto');
786 577
787 578 if (hasQuickQuestions(botId)) {
788 579 collapseQuickQuestions(botId);
@@ -789,16 +580,14 @@
789 580 }
790 581 appendThinkingMessage(botId);
791 582 scrollToBottom(botId);
792 583
793 - const currentModel = mxchatChat.model || 'gpt-5.6-sol';
584 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
794 585
795 586 // Check if streaming is enabled AND supported for this model
796 587 if (shouldUseStreaming(currentModel)) {
797 588 callMxChatStream(message, function(response) {
798 - // Content is final: releasing aria-busy lets the live region
799 - // announce the completed reply once (plan 67f126).
800 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message').attr('aria-busy', 'false');
589 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
801 590 }, botId);
802 591 } else {
803 592 callMxChat(message, function(response) {
804 593 replaceLastMessage("bot", response, '', [], botId);
@@ -831,15 +620,14 @@
831 620 }
832 621 appendThinkingMessage(botId);
833 622 scrollToBottom(botId);
834 623
835 - const currentModel = mxchatChat.model || 'gpt-5.6-sol';
624 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
836 625
837 626 // Check if streaming is enabled AND supported for this model
838 627 if (shouldUseStreaming(currentModel)) {
839 628 callMxChatStream(message, function(response) {
840 - // Final content — release aria-busy so the reply announces once (67f126).
841 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message').attr('aria-busy', 'false');
629 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
842 630 }, botId);
843 631 } else {
844 632 callMxChat(message, function(response) {
845 633 getElement(botId, 'chat-box').find('.temporary-message').remove();
@@ -903,15 +691,8 @@
903 691
904 692 function callMxChat(message, callback, botId) {
905 693 botId = botId || getMxChatBotId();
906 694
907 - // Streaming fallbacks land here: drop any leftover stream handle and
908 - // return the button to its send state (no-op for plain non-stream turns).
909 - if (mxchatActiveStreams[botId]) {
910 - delete mxchatActiveStreams[botId];
911 - }
912 - mxchatRestoreSendButton(botId);
913 -
914 695 // Store the message in case we need to retry after session reset
915 696 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
916 697
917 698 // Get page context if contextual awareness is enabled
@@ -1009,13 +790,12 @@
1009 790 // Re-send the original message with the new session (user message is already displayed)
1010 791 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1011 792 if (originalMessage) {
1012 793 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1013 - var currentModel = mxchatChat.model || 'gpt-5.6-sol';
794 + var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1014 795 if (shouldUseStreaming(currentModel)) {
1015 796 callMxChatStream(originalMessage, function(response) {
1016 - // Final content — release aria-busy so the reply announces once (67f126).
1017 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message').attr('aria-busy', 'false');
797 + getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
1018 798 }, botId);
1019 799 } else {
1020 800 callMxChat(originalMessage, function(response) {
1021 801 replaceLastMessage("bot", response, '', [], botId);
@@ -1163,9 +943,9 @@
1163 943
1164 944 // Store the message in case we need to retry after session reset
1165 945 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
1166 946
1167 - const currentModel = mxchatChat.model || 'gpt-5.6-sol';
947 + const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1168 948 if (!isStreamingSupported(currentModel)) {
1169 949 callMxChat(message, callback, botId);
1170 950 return;
1171 951 }
@@ -1218,25 +998,13 @@
1218 998
1219 999 let accumulatedContent = '';
1220 1000 let testingDataReceived = false;
1221 1001 let streamingStarted = false;
1222 - // Server-pushed html to append as its OWN bot bubble once the stream
1223 - // finishes (e.g. the consent-safe YouTube embed, plan 03ba33). Rendering is
1224 - // deferred to [DONE] so the embed always lands BELOW the streamed text.
1225 - let pendingAppendHtml = '';
1226 1002
1227 - // Abortable stream: a fresh controller per turn, keyed by bot instance.
1228 - // The Stop control (send button swapped in place) aborts both the read
1229 - // loop and the underlying request.
1230 - var streamControl = { controller: new AbortController(), aborted: false };
1231 - mxchatActiveStreams[botId] = streamControl;
1232 - mxchatShowStopButton(botId);
1233 -
1234 1003 fetch(mxchatChat.ajax_url, {
1235 1004 method: 'POST',
1236 1005 body: formData,
1237 - credentials: 'same-origin',
1238 - signal: streamControl.controller.signal
1006 + credentials: 'same-origin'
1239 1007 })
1240 1008 .then(response => {
1241 1009 // Store the response for potential fallback handling
1242 1010 const responseClone = response.clone();
@@ -1337,17 +1105,8 @@
1337 1105
1338 1106 // Re-enable chat input after streaming completes
1339 1107 enableChatInput(botId);
1340 1108
1341 - // Render any server-pushed appendix html (e.g. the
1342 - // YouTube embed) as its own bot bubble below the
1343 - // streamed text — mirrors how it is saved in the
1344 - // transcript, so history replays identically.
1345 - if (pendingAppendHtml) {
1346 - appendMessage("bot", "", pendingAppendHtml, [], false, botId);
1347 - pendingAppendHtml = '';
1348 - }
1349 -
1350 1109 // Scroll the user's last message to the top now
1351 1110 // that the bot's full reply has rendered.
1352 1111 var $chatBoxStreamDone = getElement(botId, 'chat-box');
1353 1112 var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
@@ -1381,21 +1140,8 @@
1381 1140 streamingStarted = true;
1382 1141 accumulatedContent += json.content;
1383 1142 updateStreamingMessage(accumulatedContent, botId);
1384 1143 }
1385 - // Stash appendix html (e.g. video embed) for [DONE]
1386 - else if (json.append_html) {
1387 - pendingAppendHtml = json.append_html;
1388 - }
1389 - // Server-side final pass changed the assembled text
1390 - // (ffef6f: dead-link stripping) — swap the rendered
1391 - // bubble for the validated version. Arrives at most
1392 - // once, just before [DONE].
1393 - else if (json.replace_content) {
1394 - streamingStarted = true;
1395 - accumulatedContent = json.replace_content;
1396 - updateStreamingMessage(accumulatedContent, botId);
1397 - }
1398 1144 // Handle complete response in stream (fallback response)
1399 1145 else if (json.text || json.message || json.html) {
1400 1146 handleNonStreamResponse(json, callback, botId);
1401 1147 return;
@@ -1425,9 +1171,8 @@
1425 1171 }
1426 1172
1427 1173 processStream();
1428 1174 }).catch(streamError => {
1429 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1430 1175 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1431 1176 callMxChat(message, callback, botId);
1432 1177 });
1433 1178 }
@@ -1434,9 +1179,8 @@
1434 1179
1435 1180 processStream();
1436 1181 })
1437 1182 .catch(error => {
1438 - if (mxchatHandleStreamAbort(botId, accumulatedContent, callback)) return;
1439 1183 // Check if we have server error data with chat mode
1440 1184 if (error && error.isServerError && error.data) {
1441 1185 // Check for chat mode in error data
1442 1186 if (error.data.chat_mode) {
@@ -1497,9 +1241,9 @@
1497 1241 // Re-send the original message with the new session (user message is already displayed)
1498 1242 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1499 1243 if (originalMessage) {
1500 1244 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1501 - var currentModel = mxchatChat.model || 'gpt-5.6-sol';
1245 + var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1502 1246 if (shouldUseStreaming(currentModel)) {
1503 1247 callMxChatStream(originalMessage, callback, botId);
1504 1248 } else {
1505 1249 callMxChat(originalMessage, callback, botId);
@@ -1632,15 +1376,8 @@
1632 1376 var $chatBox = getElement(botId, 'chat-box');
1633 1377 const tempMessage = $chatBox.find('.bot-message.temporary-message').last();
1634 1378
1635 1379 if (tempMessage.length) {
1636 - // aria-busy=true for the whole stream: the bubble is rewritten on
1637 - // every chunk, and without busy a polite live region announces those
1638 - // rewrites continuously. Flipped false once the reply is final, so
1639 - // assistive tech announces the completed message ONCE (plan 67f126).
1640 - if (tempMessage.attr('aria-busy') !== 'true') {
1641 - tempMessage.attr('aria-busy', 'true');
1642 - }
1643 1380 // Update existing message
1644 1381 tempMessage.html(formattedContent);
1645 1382 } else {
1646 1383 // Create new temporary message if it doesn't exist
@@ -1667,17 +1404,8 @@
1667 1404 // Update the event handlers to use the correct function names (using event delegation)
1668 1405 // Use class-based selectors for multi-instance support
1669 1406 $(document).on('click', '.send-button', function() {
1670 1407 var botId = getBotIdFromElement(this);
1671 - // While a response is streaming the button is a Stop control.
1672 - if (this.classList.contains('mxchat-stop-mode')) {
1673 - // Same click that just started this stream (an add-on's direct handler
1674 - // ran before this delegated one) — not a Stop press. See
1675 - // mxchatShowStopButton for the full story (plan-4bba64).
1676 - if (this.__mxchatStopJustShown) return;
1677 - mxchatStopStreaming(botId);
1678 - return;
1679 - }
1680 1408 var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1681 1409 if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1682 1410 disableChatInput(botId);
1683 1411 }
@@ -1696,73 +1424,8 @@
1696 1424 sendMessage(botId);
1697 1425 }
1698 1426 });
1699 1427
1700 -// Chat input character counter + soft limit feedback (plan 7091a2).
1701 -// Language-neutral: numbers + color only, no translatable strings. The counter
1702 -// reveals near the cap and ramps neutral -> amber -> red; an over-limit keystroke
1703 -// or trimmed paste produces a brief border-flash/shake so the maxlength cap (plan
1704 -// a3fae2) is never a silent "input jumps back". Per-bot scoped via .input-container.
1705 -function mxchatUpdateCharCounter(inputEl) {
1706 - if (!inputEl || !inputEl.closest) return;
1707 - var max = parseInt(inputEl.getAttribute('maxlength'), 10);
1708 - var container = inputEl.closest('.input-container');
1709 - if (!container || !max || max <= 0) return;
1710 - var counter = container.querySelector('.mxchat-char-counter');
1711 - if (!counter) return;
1712 - var len = inputEl.value.length;
1713 - var ratio = len / max;
1714 - var nearThreshold = 0.8; // start surfacing the counter at 80% of the cap
1715 - var cur = counter.querySelector('.mxchat-char-counter-current');
1716 - if (cur) cur.textContent = len;
1717 - var warn = ratio >= nearThreshold && len < max;
1718 - var full = len >= max;
1719 - counter.classList.toggle('is-visible', ratio >= nearThreshold);
1720 - counter.classList.toggle('is-warn', warn);
1721 - counter.classList.toggle('is-full', full);
1722 - container.classList.toggle('mxchat-input-near-limit', warn);
1723 - container.classList.toggle('mxchat-input-at-limit', full);
1724 -}
1725 -
1726 -function mxchatBumpInput(inputEl) {
1727 - var container = inputEl && inputEl.closest ? inputEl.closest('.input-container') : null;
1728 - if (!container) return;
1729 - container.classList.remove('mxchat-input-bump');
1730 - void container.offsetWidth; // reflow so a rapid second hit retriggers the animation
1731 - container.classList.add('mxchat-input-bump');
1732 - clearTimeout($(container).data('mxchatBumpTimeout'));
1733 - var t = setTimeout(function() { container.classList.remove('mxchat-input-bump'); }, 220);
1734 - $(container).data('mxchatBumpTimeout', t);
1735 -}
1736 -
1737 -// Live counter update on every input.
1738 -$(document).on('input', '.chat-input', function() {
1739 - mxchatUpdateCharCounter(this);
1740 -});
1741 -
1742 -// Visible "you've hit the edge" feedback when a printable keystroke is about to be
1743 -// rejected at the cap (maxlength silently swallows it otherwise).
1744 -$(document).on('keydown', '.chat-input', function(e) {
1745 - var max = parseInt(this.getAttribute('maxlength'), 10);
1746 - if (!max || max <= 0 || this.value.length < max) return;
1747 - if (e.ctrlKey || e.metaKey || e.altKey) return;
1748 - // A single printable char with no selection to overwrite WILL be rejected.
1749 - if (e.key && e.key.length === 1 && this.selectionStart === this.selectionEnd) {
1750 - mxchatBumpInput(this);
1751 - }
1752 -});
1753 -
1754 -// A paste that gets trimmed to the cap also bumps, so truncation is never silent.
1755 -$(document).on('paste', '.chat-input', function() {
1756 - var el = this;
1757 - var max = parseInt(el.getAttribute('maxlength'), 10);
1758 - if (!max || max <= 0) return;
1759 - setTimeout(function() {
1760 - mxchatUpdateCharCounter(el);
1761 - if (el.value.length >= max) mxchatBumpInput(el);
1762 - }, 0);
1763 -});
1764 -
1765 1428 // Builds the list of overflow-menu items for a given bot.
1766 1429 // Adding a future item is one push to this array — do NOT hardcode "only download."
1767 1430 function mxchatGetHeaderMenuItems(botId) {
1768 1431 var items = [];
@@ -1780,26 +1443,8 @@
1780 1443 }
1781 1444 });
1782 1445 }
1783 1446
1784 - // "Start new chat" — surfaces the EXISTING per-conversation reset
1785 - // (MxChatInstances.resetChatSession) so a visitor can start a fresh thread
1786 - // without the site owner disabling chat persistence globally. Default OFF;
1787 - // gated by the reset_chat_enabled option. plan ac2e81.
1788 - if (settings.reset_chat_enabled === 'on') {
1789 - items.push({
1790 - id: 'reset-chat',
1791 - label: settings.reset_chat_label || 'Start new chat',
1792 - 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>',
1793 - action: function() {
1794 - var confirmMsg = settings.reset_chat_confirm || 'Start a new chat? This clears the current conversation.';
1795 - if (window.confirm(confirmMsg)) {
1796 - MxChatInstances.resetChatSession(botId);
1797 - }
1798 - }
1799 - });
1800 - }
1801 -
1802 1447 return items;
1803 1448 }
1804 1449
1805 1450 // Builds a clean markdown transcript of the current conversation and triggers
@@ -1877,31 +1522,30 @@
1877 1522 var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1878 1523 if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1879 1524 }
1880 1525
1881 -// Renders (or re-renders) the item list for one menu wrap. Split out of
1882 -// mxchatInitHeaderMenu so the dynamic-settings merge (plan-32db95) can
1883 -// rebuild items + trigger visibility WITHOUT re-binding the one-time
1884 -// open/close/keyboard wiring. closeMenu is passed in by the init closure;
1885 -// a rebuild before init (never happens, but harmless) just skips it.
1886 -function mxchatRenderHeaderMenuItems(botId, $wrap, closeMenuFn) {
1526 +// One-time per-widget init: renders menu items, wires open/close,
1527 +// outside-click, Escape, and arrow-key navigation. If no items, hides the trigger.
1528 +function mxchatInitHeaderMenu(botId) {
1529 + var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1530 + if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1531 +
1887 1532 var $trigger = $wrap.find('.mxchat-menu-trigger');
1888 1533 var $menu = $wrap.find('.mxchat-header-menu');
1889 1534 var items = mxchatGetHeaderMenuItems(botId);
1890 1535
1891 - $menu.empty();
1536 + // Initial color sync — covers normal page load.
1537 + mxchatSyncMenuColors(botId, $wrap);
1892 1538
1893 1539 if (!items.length) {
1894 1540 $trigger.hide();
1895 1541 $menu.hide();
1542 + $wrap.data('mxchatMenuReady', true);
1896 1543 return;
1897 1544 }
1898 1545
1899 - // Clear any inline display:none a previous zero-item render left behind —
1900 - // open/close visibility is governed by the hidden prop + is-open class.
1901 - $trigger.css('display', '');
1902 - $menu.css('display', '');
1903 -
1546 + // Build the menu items.
1547 + $menu.empty();
1904 1548 items.forEach(function(item, idx) {
1905 1549 var $btn = $('<button>', {
1906 1550 type: 'button',
1907 1551 'class': 'mxchat-menu-item',
@@ -1914,50 +1558,14 @@
1914 1558 $btn.find('.mxchat-menu-item-label').text(item.label);
1915 1559 $btn.on('click', function(e) {
1916 1560 e.preventDefault();
1917 1561 e.stopPropagation();
1918 - if (closeMenuFn) closeMenuFn();
1562 + closeMenu();
1919 1563 try { item.action(); } catch (err) { /* no-op */ }
1920 1564 });
1921 1565 $menu.append($btn);
1922 1566 });
1923 -}
1924 1567
1925 -// Re-render every menu on the page after a dynamic-settings merge
1926 -// (multi-bot: each wrap re-reads its items). An OPEN menu is left alone —
1927 -// swapping items under the user mid-interaction yanks focus — and the
1928 -// rebuild runs when it closes instead (closeMenu checks the pending flag).
1929 -function mxchatRebuildHeaderMenus() {
1930 - $('.mxchat-header-menu-wrap').each(function() {
1931 - var $wrap = $(this);
1932 - var botId = $wrap.data('bot-id');
1933 - if (!botId) return;
1934 - if (!$wrap.data('mxchatMenuReady')) {
1935 - mxchatInitHeaderMenu(botId);
1936 - return;
1937 - }
1938 - if ($wrap.find('.mxchat-header-menu').hasClass('is-open')) {
1939 - $wrap.data('mxchatMenuRebuildPending', true);
1940 - return;
1941 - }
1942 - mxchatRenderHeaderMenuItems(botId, $wrap, $wrap.data('mxchatMenuClose'));
1943 - });
1944 -}
1945 -
1946 -// One-time per-widget init: renders menu items, wires open/close,
1947 -// outside-click, Escape, and arrow-key navigation. If no items, hides the
1948 -// trigger. Wiring happens even when there are zero items at init, so a
1949 -// later dynamic-settings rebuild that adds items has a working trigger.
1950 -function mxchatInitHeaderMenu(botId) {
1951 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1952 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1953 -
1954 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1955 - var $menu = $wrap.find('.mxchat-header-menu');
1956 -
1957 - // Initial color sync — covers normal page load.
1958 - mxchatSyncMenuColors(botId, $wrap);
1959 -
1960 1568 function openMenu() {
1961 1569 // Re-sync each open in case the active theme changed since init.
1962 1570 mxchatSyncMenuColors(botId, $wrap);
1963 1571 $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
@@ -1971,14 +1579,8 @@
1971 1579 $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1972 1580 $trigger.attr('aria-expanded', 'false');
1973 1581 $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1974 1582 if (returnFocus) $trigger.trigger('focus');
1975 - // A dynamic-settings rebuild that arrived while the menu was open
1976 - // was deferred (mxchatRebuildHeaderMenus) — run it now.
1977 - if ($wrap.data('mxchatMenuRebuildPending')) {
1978 - $wrap.removeData('mxchatMenuRebuildPending');
1979 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
1980 - }
1981 1583 }
1982 1584
1983 1585 // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1984 1586 // click-to-collapse handler does not fire.
@@ -2032,13 +1634,8 @@
2032 1634 openMenu();
2033 1635 }
2034 1636 });
2035 1637
2036 - // Expose closeMenu for out-of-closure re-renders (mxchatRebuildHeaderMenus),
2037 - // then do the initial item render.
2038 - $wrap.data('mxchatMenuClose', closeMenu);
2039 - mxchatRenderHeaderMenuItems(botId, $wrap, closeMenu);
2040 -
2041 1638 $wrap.data('mxchatMenuReady', true);
2042 1639 }
2043 1640
2044 1641 // Initialize header menus for every rendered widget on DOM ready.
@@ -2046,18 +1643,8 @@
2046 1643 $('.mxchat-header-menu-wrap').each(function() {
2047 1644 var botId = $(this).data('bot-id');
2048 1645 if (botId) mxchatInitHeaderMenu(botId);
2049 1646 });
2050 -
2051 - // Embedded (non-floating) widgets are open from the moment the page
2052 - // renders — refresh dynamic settings at init (plan-32db95). Floating
2053 - // widgets refresh on first launcher open instead.
2054 - var hasEmbeddedWidget = $('.mxchat-chatbot-wrapper').filter(function() {
2055 - return !$(this).closest('.floating-chatbot').length;
2056 - }).length > 0;
2057 - if (hasEmbeddedWidget) {
2058 - mxchatRefreshDynamicSettings();
2059 - }
2060 1647 });
2061 1648
2062 1649 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
2063 1650 try {
@@ -2134,11 +1721,9 @@
2134 1721
2135 1722 messageDiv.html(fullMessage);
2136 1723
2137 1724 if (isTemporary) {
2138 - // In-flight bubble: hold aria-busy so the live region stays quiet
2139 - // until the content is finalized (plan 67f126).
2140 - messageDiv.addClass('temporary-message').attr('aria-busy', 'true');
1725 + messageDiv.addClass('temporary-message');
2141 1726 }
2142 1727
2143 1728 // Append to the correct chatbot instance's chat-box
2144 1729 var $chatBox = getElement(botId, 'chat-box');
@@ -2273,16 +1858,13 @@
2273 1858 }
2274 1859
2275 1860 if (lastMessageDiv.length) {
2276 1861 // Replace content immediately to prevent visual gap between thinking dots and response
2277 - // aria-busy released AFTER the final content is set, so the live region
2278 - // announces the finished message once (plan 67f126).
2279 1862 lastMessageDiv
2280 1863 .html(fullMessage)
2281 1864 .removeClass('bot-message user-message temporary-message')
2282 1865 .addClass(messageClass)
2283 - .attr('dir', 'auto')
2284 - .attr('aria-busy', 'false');
1866 + .attr('dir', 'auto');
2285 1867
2286 1868 // Only apply inline colors if AI theme is not active (let CSS handle it)
2287 1869 var skipColors = mxchatChat.skip_inline_colors || shouldSkipInlineColors(botId);
2288 1870 if (!skipColors) {
@@ -2342,15 +1924,10 @@
2342 1924 var botMessageFontColor = mxchatChat.bot_message_font_color;
2343 1925 var botMessageBgColor = mxchatChat.bot_message_bg_color;
2344 1926
2345 1927 // Build thinking dots HTML - skip inline colors if AI theme is active
2346 - // The dots are decorative; the sr-only span is what the live region
2347 - // announces for the waiting state (plan 67f126). Server-localized
2348 - // string — safe to inject (esc_html__ output, no user content).
2349 1928 var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
2350 - var srThinking = mxchatChat.thinking_announcement || 'Assistant is typing';
2351 - var thinkingHtml = '<span class="sr-only">' + srThinking + '</span>' +
2352 - '<div class="thinking-dots-container" aria-hidden="true">' +
1929 + var thinkingHtml = '<div class="thinking-dots-container">' +
2353 1930 '<div class="thinking-dots">' +
2354 1931 '<span class="dot"' + dotStyle + '></span>' +
2355 1932 '<span class="dot"' + dotStyle + '></span>' +
2356 1933 '<span class="dot"' + dotStyle + '></span>' +
@@ -3050,30 +2627,9 @@
3050 2627 }
3051 2628
3052 2629 // Only process if there are actual messages
3053 2630 if (response.data.conversation.length > 0) {
3054 - // Restored history must be SILENT to screen readers
3055 - // (plan 67f126): these are DOM additions inside the
3056 - // live region and would otherwise announce as if
3057 - // they just arrived. Lift aria-live for the batch
3058 - // repopulate, restore it after the browser has
3059 - // processed the mutations.
3060 - var mxLiveRegionEl = $chatBox.get(0);
3061 - var mxSavedAriaLive = mxLiveRegionEl ? mxLiveRegionEl.getAttribute('aria-live') : null;
3062 - if (mxLiveRegionEl) {
3063 - mxLiveRegionEl.setAttribute('aria-live', 'off');
3064 - }
3065 -
3066 - // IMPORTANT: Clear existing messages before loading history.
3067 - // Detach the greeting first and put it back below —
3068 - // it is server-rendered and never stored in the
3069 - // transcript, so the old unconditional .empty()
3070 - // deleted it for the rest of the page life (plan
3071 - // a1a79b). Detach rather than rebuild: intro_message
3072 - // is not localized to JS, and this node already
3073 - // carries the per-bot inline colors and any
3074 - // {visitor_name} substitution already applied to it.
3075 - var $mxIntro = $chatBox.find('.mxchat-intro-message').first().detach();
2631 + // IMPORTANT: Clear existing messages before loading history
3076 2632 $chatBox.empty();
3077 2633
3078 2634 $.each(response.data.conversation, function(index, message) {
3079 2635 // Skip agent messages if persistence is off
@@ -3113,16 +2669,12 @@
3113 2669
3114 2670 // Skip linkify for messages containing structured HTML
3115 2671 // (forms, product cards, galleries, etc.) to avoid
3116 2672 // markdown formatting corrupting HTML attributes
3117 - // (e.g. underscores in name="field_name" becoming <em> tags).
3118 - // One family check instead of a per-card literal list: any
3119 - // element carrying an mxchat- prefixed class is MxChat-generated
3120 - // structured markup and replays raw. The old list drifted every
3121 - // time an add-on minted a new card class — the filtered-search
3122 - // card ("mxchat-filtered-product-card") missed it and replayed
3123 - // through linkify as visible markup.
3124 - if (/<[a-z][^>]*class\s*=\s*["'][^"']*\bmxchat-/i.test(content) ||
2673 + // (e.g. underscores in name="field_name" becoming <em> tags)
2674 + if (content.includes("mxchat-product-card") ||
2675 + content.includes("mxchat-image-gallery") ||
2676 + content.includes("mxchat-featured-products") ||
3125 2677 content.includes("<form") ||
3126 2678 content.includes("<input") ||
3127 2679 content.includes("<select") ||
3128 2680 content.includes("<textarea")) {
@@ -3140,29 +2692,12 @@
3140 2692 instance.processedMessageIds.add(message.id);
3141 2693 }
3142 2694 });
3143 2695
3144 - // Only append messages and scroll if we have content.
3145 - // Greeting goes back FIRST, above the restored
3146 - // history: "Hello — [earlier conversation]" is the
3147 - // natural reading and matches the order a fresh
3148 - // visitor sees (plan a1a79b).
3149 - if ($mxIntro && $mxIntro.length) {
3150 - $chatBox.append($mxIntro);
3151 - }
2696 + // Only append messages and scroll if we have content
3152 2697 $chatBox.append($fragment);
3153 2698 scrollToBottom(botId, true);
3154 2699
3155 - // Re-attach live semantics AFTER the rehydration
3156 - // mutations have been processed with the region off
3157 - // (plan 67f126). Restoring later announces nothing
3158 - // retroactively; new turns announce normally.
3159 - if (mxLiveRegionEl) {
3160 - setTimeout(function() {
3161 - mxLiveRegionEl.setAttribute('aria-live', mxSavedAriaLive || 'polite');
3162 - }, 200);
3163 - }
3164 -
3165 2700 // Collapse quick questions if we have conversation history
3166 2701 // BUT skip auto-collapse for embedded bots (they should stay expanded)
3167 2702 if (hasQuickQuestions(botId) && !isEmbeddedBot(botId)) {
3168 2703 collapseQuickQuestions(botId);
@@ -3257,10 +2792,10 @@
3257 2792 .then(data => {
3258 2793 if (data.success) {
3259 2794 container.style.display = 'none';
3260 2795 nameElement.textContent = '';
3261 - instance.activePdfFile = null;
3262 - appendMessage('bot', 'PDF removed.', '', [], false, botId);
2796 + activePdfFile = null;
2797 + appendMessage('bot', 'PDF removed.');
3263 2798 }
3264 2799 })
3265 2800 .catch(error => {
3266 2801 // Error removing PDF - silently continue
@@ -3266,16 +2801,14 @@
3266 2801 // Error removing PDF - silently continue
3267 2802 });
3268 2803 }
3269 2804
3270 - function removeActiveWord(botId) {
3271 - botId = botId || 'default';
3272 - var instance = MxChatInstances.get(botId);
3273 - const container = getElementDOM(botId, 'active-word-container');
3274 - const nameElement = getElementDOM(botId, 'active-word-name');
3275 -
3276 - if (!container || !nameElement || !instance.activeWordFile) return;
3277 -
2805 + function removeActiveWord() {
2806 + const container = document.getElementById('active-word-container');
2807 + const nameElement = document.getElementById('active-word-name');
2808 +
2809 + if (!container || !nameElement || !activeWordFile) return;
2810 +
3278 2811 fetch(mxchatChat.ajax_url, {
3279 2812 method: 'POST',
3280 2813 headers: {
3281 2814 'Content-Type': 'application/x-www-form-urlencoded',
@@ -3281,9 +2814,9 @@
3281 2814 'Content-Type': 'application/x-www-form-urlencoded',
3282 2815 },
3283 2816 body: new URLSearchParams({
3284 2817 'action': 'mxchat_remove_word',
3285 - 'session_id': getChatSession(botId),
2818 + 'session_id': sessionId,
3286 2819 'nonce': mxchatChat.nonce
3287 2820 })
3288 2821 })
3289 2822 .then(response => response.json())
@@ -3290,10 +2823,10 @@
3290 2823 .then(data => {
3291 2824 if (data.success) {
3292 2825 container.style.display = 'none';
3293 2826 nameElement.textContent = '';
3294 - instance.activeWordFile = null;
3295 - appendMessage('bot', 'Word document removed.', '', [], false, botId);
2827 + activeWordFile = null;
2828 + appendMessage('bot', 'Word document removed.');
3296 2829 }
3297 2830 })
3298 2831 .catch(error => {
3299 2832 // Error removing Word document - silently continue
@@ -3452,30 +2985,8 @@
3452 2985 e.stopPropagation();
3453 2986 var botId = getBotIdFromElement(this);
3454 2987 collapseQuickQuestions(botId);
3455 2988 });
3456 -
3457 -// Consent-safe YouTube embed (plan 03ba33): the server only ever ships a
3458 -// thumbnail facade — no Google iframe exists until the visitor taps play.
3459 -// Delegated so it also works for embeds restored from chat history.
3460 -$(document).on('click', '.mxchat-youtube-embed .mxchat-youtube-facade', function(e) {
3461 - e.preventDefault();
3462 - var $wrap = $(this).closest('.mxchat-youtube-embed');
3463 - var videoId = String($wrap.data('video-id') || '').replace(/[^A-Za-z0-9_-]/g, '');
3464 - if (!videoId) {
3465 - return;
3466 - }
3467 - var title = $wrap.find('.mxchat-youtube-title').text() || 'YouTube video';
3468 - var $iframe = $('<iframe>', {
3469 - src: 'https://www.youtube-nocookie.com/embed/' + videoId + '?autoplay=1&rel=0',
3470 - title: title,
3471 - allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
3472 - allowfullscreen: true,
3473 - frameborder: 0
3474 - }).addClass('mxchat-youtube-iframe');
3475 - $wrap.addClass('mxchat-youtube-playing');
3476 - $(this).replaceWith($iframe);
3477 -});
3478 2989
3479 2990 // Chatbot visibility toggle handlers - use class selector for multi-instance support
3480 2991 // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
3481 2992 $(document).on('click keydown', '.floating-chatbot-button', function(e) {
@@ -3495,13 +3006,8 @@
3495 3006 $badge.hide(); // Hide notification when opening chat
3496 3007 disableScroll();
3497 3008 $preChat.fadeOut(250);
3498 3009
3499 - // First open per page load: re-fetch behavior settings in case
3500 - // this page's inline values came from a stale full-page cache
3501 - // (plan-32db95). Idempotent — later opens are a no-op.
3502 - mxchatRefreshDynamicSettings();
3503 -
3504 3010 // Load chat history for returning visitors (persistence)
3505 3011 var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3506 3012 if (chatPersistenceEnabled) {
3507 3013 MxChatInstances.ensureSession(botId);
@@ -3585,20 +3091,17 @@
3585 3091 var wordInput = getElementDOM(botId, 'word-upload');
3586 3092 if (wordInput) wordInput.click();
3587 3093 });
3588 3094
3589 - // PDF file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'pdf-upload')
3590 - $(document).on('change', '.pdf-upload', async function(e) {
3591 - var botId = getBotIdFromElement(this);
3592 - var instance = MxChatInstances.get(botId);
3593 - const file = this.files[0];
3594 - const sessionId = MxChatInstances.ensureSession(botId);
3595 -
3095 + // PDF file input change handler
3096 + addSafeEventListener('pdf-upload', 'change', async function(e) {
3097 + const file = e.target.files[0];
3098 +
3596 3099 if (!file || file.type !== 'application/pdf') {
3597 3100 alert('Please select a valid PDF file.');
3598 3101 return;
3599 3102 }
3600 -
3103 +
3601 3104 if (!sessionId) {
3602 3105 alert('Error: No session ID found');
3603 3106 return;
3604 3107 }
@@ -3606,15 +3109,14 @@
3606 3109 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3607 3110 alert('Error: Ajax configuration missing');
3608 3111 return;
3609 3112 }
3610 -
3113 +
3611 3114 // Disable buttons and show loading state
3612 - const uploadBtn = getElementDOM(botId, 'pdf-upload-btn');
3613 - const sendBtn = getElementDOM(botId, 'send-button');
3614 - if (!uploadBtn) return;
3115 + const uploadBtn = document.getElementById('pdf-upload-btn');
3116 + const sendBtn = document.getElementById('send-button');
3615 3117 const originalBtnContent = uploadBtn.innerHTML;
3616 -
3118 +
3617 3119 try {
3618 3120 // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3619 3121 await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3620 3122 const formData = new FormData();
@@ -3621,34 +3123,35 @@
3621 3123 formData.append('action', 'mxchat_upload_pdf');
3622 3124 formData.append('pdf_file', file);
3623 3125 formData.append('session_id', sessionId);
3624 3126 formData.append('nonce', mxchatChat.nonce);
3625 -
3127 +
3626 3128 uploadBtn.disabled = true;
3627 - if (sendBtn) sendBtn.disabled = true;
3129 + sendBtn.disabled = true;
3628 3130 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3629 3131 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3630 3132 </svg>`;
3631 -
3133 +
3632 3134 const response = await fetch(mxchatChat.ajax_url, {
3633 3135 method: 'POST',
3634 3136 body: formData
3635 3137 });
3636 -
3138 +
3637 3139 const data = await response.json();
3638 -
3140 +
3639 3141 if (data.success) {
3640 3142 // Hide popular questions if they exist
3641 - if (hasQuickQuestions(botId)) {
3642 - collapseQuickQuestions(botId);
3143 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
3144 + if (hasQuickQuestions()) {
3145 + collapseQuickQuestions();
3643 3146 }
3644 -
3147 +
3645 3148 // Show the active PDF name
3646 - showActivePdf(data.data.filename, botId);
3647 -
3648 - appendMessage('bot', data.data.message, '', [], false, botId);
3649 - scrollToBottom(botId);
3650 - instance.activePdfFile = data.data.filename;
3149 + showActivePdf(data.data.filename);
3150 +
3151 + appendMessage('bot', data.data.message);
3152 + scrollToBottom();
3153 + activePdfFile = data.data.filename;
3651 3154 } else {
3652 3155 alert('Failed to upload PDF. Please try again.');
3653 3156 }
3654 3157 } catch (error) {
@@ -3654,42 +3157,33 @@
3654 3157 } catch (error) {
3655 3158 alert('Error uploading file. Please try again.');
3656 3159 } finally {
3657 3160 uploadBtn.disabled = false;
3658 - if (sendBtn) sendBtn.disabled = false;
3161 + sendBtn.disabled = false;
3659 3162 uploadBtn.innerHTML = originalBtnContent;
3660 3163 this.value = ''; // Reset file input
3661 3164 }
3662 3165 });
3663 3166
3664 - // Word file input change handler - delegated, bot-aware (was bound to stale un-suffixed id 'word-upload')
3665 - $(document).on('change', '.word-upload', async function(e) {
3666 - var botId = getBotIdFromElement(this);
3667 - var instance = MxChatInstances.get(botId);
3668 - const file = this.files[0];
3669 - const sessionId = MxChatInstances.ensureSession(botId);
3670 -
3167 + // Word file input change handler
3168 + addSafeEventListener('word-upload', 'change', async function(e) {
3169 + const file = e.target.files[0];
3170 +
3671 3171 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
3672 3172 alert('Please select a valid Word document (.docx).');
3673 3173 return;
3674 3174 }
3675 -
3175 +
3676 3176 if (!sessionId) {
3677 3177 alert('Error: No session ID found');
3678 3178 return;
3679 3179 }
3680 3180
3681 - if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
3682 - alert('Error: Ajax configuration missing');
3683 - return;
3684 - }
3685 -
3686 3181 // Disable buttons and show loading state
3687 - const uploadBtn = getElementDOM(botId, 'word-upload-btn');
3688 - const sendBtn = getElementDOM(botId, 'send-button');
3689 - if (!uploadBtn) return;
3182 + const uploadBtn = document.getElementById('word-upload-btn');
3183 + const sendBtn = document.getElementById('send-button');
3690 3184 const originalBtnContent = uploadBtn.innerHTML;
3691 -
3185 +
3692 3186 try {
3693 3187 // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3694 3188 await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3695 3189 const formData = new FormData();
@@ -3696,34 +3190,35 @@
3696 3190 formData.append('action', 'mxchat_upload_word');
3697 3191 formData.append('word_file', file);
3698 3192 formData.append('session_id', sessionId);
3699 3193 formData.append('nonce', mxchatChat.nonce);
3700 -
3194 +
3701 3195 uploadBtn.disabled = true;
3702 - if (sendBtn) sendBtn.disabled = true;
3196 + sendBtn.disabled = true;
3703 3197 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3704 3198 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3705 3199 </svg>`;
3706 -
3200 +
3707 3201 const response = await fetch(mxchatChat.ajax_url, {
3708 3202 method: 'POST',
3709 3203 body: formData
3710 3204 });
3711 -
3205 +
3712 3206 const data = await response.json();
3713 -
3207 +
3714 3208 if (data.success) {
3715 3209 // Hide popular questions if they exist
3716 - if (hasQuickQuestions(botId)) {
3717 - collapseQuickQuestions(botId);
3210 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
3211 + if (hasQuickQuestions()) {
3212 + collapseQuickQuestions();
3718 3213 }
3719 -
3214 +
3720 3215 // Show the active Word document name
3721 - showActiveWord(data.data.filename, botId);
3722 -
3723 - appendMessage('bot', data.data.message, '', [], false, botId);
3724 - scrollToBottom(botId);
3725 - instance.activeWordFile = data.data.filename;
3216 + showActiveWord(data.data.filename);
3217 +
3218 + appendMessage('bot', data.data.message);
3219 + scrollToBottom();
3220 + activeWordFile = data.data.filename;
3726 3221 } else {
3727 3222 alert('Failed to upload Word document. Please try again.');
3728 3223 }
3729 3224 } catch (error) {
@@ -3729,25 +3224,25 @@
3729 3224 } catch (error) {
3730 3225 alert('Error uploading file. Please try again.');
3731 3226 } finally {
3732 3227 uploadBtn.disabled = false;
3733 - if (sendBtn) sendBtn.disabled = false;
3228 + sendBtn.disabled = false;
3734 3229 uploadBtn.innerHTML = originalBtnContent;
3735 3230 this.value = ''; // Reset file input
3736 3231 }
3737 3232 });
3738 3233
3739 - // Remove button click handlers - delegated, bot-aware (were bound to stale un-suffixed ids)
3740 - $(document).on('click', '.remove-pdf-btn', function(e) {
3234 + // Remove button click handlers
3235 + document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
3741 3236 e.preventDefault();
3742 3237 e.stopPropagation();
3743 - removeActivePdf(getBotIdFromElement(this));
3238 + removeActivePdf();
3744 3239 });
3745 -
3746 - $(document).on('click', '.remove-word-btn', function(e) {
3240 +
3241 + document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
3747 3242 e.preventDefault();
3748 3243 e.stopPropagation();
3749 - removeActiveWord(getBotIdFromElement(this));
3244 + removeActiveWord();
3750 3245 });
3751 3246
3752 3247 // Window resize handlers
3753 3248 $(window).on('resize orientationchange', function() {
@@ -3891,13 +3386,10 @@
3891 3386 function replaceVisitorNamePlaceholder(botId, visitorName) {
3892 3387 var chatBox = getElementDOM(botId, 'chat-box');
3893 3388 if (!chatBox) return;
3894 3389
3895 - // Find the greeting by its marker, not by position (plan a1a79b) —
3896 - // after a persistence restore the first .bot-message is a restored
3897 - // reply, and {visitor_name} was being substituted into that instead.
3898 - // Positional fallback for HTML cached before this release only.
3899 - var introMessage = chatBox.querySelector('.mxchat-intro-message') || chatBox.querySelector('.bot-message');
3390 + // Find the first bot message (intro message)
3391 + var introMessage = chatBox.querySelector('.bot-message');
3900 3392 if (!introMessage) return;
3901 3393
3902 3394 var messageContent = introMessage.querySelector('div[dir="auto"]');
3903 3395 if (!messageContent) return;
@@ -4069,9 +3561,8 @@
4069 3561 }
4070 3562
4071 3563 var emailInput = getElementDOM(botId, 'user-email');
4072 3564 var nameInput = getElementDOM(botId, 'user-name');
4073 - var consentInput = getElementDOM(botId, 'user-consent');
4074 3565 var userEmail = emailInput ? emailInput.value.trim() : '';
4075 3566 var userName = nameInput ? nameInput.value.trim() : '';
4076 3567 var sessionId = MxChatInstances.ensureSession(botId);
4077 3568
@@ -4091,15 +3582,8 @@
4091 3582 showEmailError(botId, 'Please enter a valid name (2-100 characters).');
4092 3583 return false;
4093 3584 }
4094 3585
4095 - // Consent checkbox (b062c4): backstop behind the native required
4096 - // attribute; the server enforces this independently either way.
4097 - if (consentInput && consentInput.required && !consentInput.checked) {
4098 - showEmailError(botId, 'Please tick the consent box to continue.');
4099 - return false;
4100 - }
4101 -
4102 3586 clearEmailError(botId);
4103 3587 setEmailSubmissionState(botId, true);
4104 3588
4105 3589 // Prepare form data
@@ -4113,14 +3597,8 @@
4113 3597 if (userName) {
4114 3598 formData.append('name', userName);
4115 3599 }
4116 3600
4117 - // Ticked/unticked both travel when the checkbox is rendered, so an
4118 - // optional-consent "no" is recorded as a decision, not an absence.
4119 - if (consentInput) {
4120 - formData.append('consent', consentInput.checked ? '1' : '0');
4121 - }
4122 -
4123 3601 fetch(mxchatChat.ajax_url, {
4124 3602 method: 'POST',
4125 3603 headers: {
4126 3604 'Content-Type': 'application/x-www-form-urlencoded',
@@ -4465,26 +3943,13 @@
4465 3943 // SATISFACTION RATING (v3.2.6)
4466 3944 // ============================================================================
4467 3945 // Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
4468 3946 // inactivity following a bot reply. One prompt per session, deduped via
4469 -// localStorage. Runs ONLY when the satisfaction_rating_enabled option is on —
4470 -// the option (default off) is authoritative.
3947 +// localStorage. Disabled site-wide when mxchatChat.satisfaction_rating_enabled
3948 +// is exactly false (default ON).
4471 3949 jQuery(function($) {
4472 3950 if (typeof mxchatChat === 'undefined') return;
4473 - // wp_localize_script stringifies scalars: a PHP boolean false arrives as
4474 - // '' and true as '1', so this must be an explicit-enable allowlist — the
4475 - // old "disabled when exactly false/'off'" check let '' through and the
4476 - // bubble rendered on sites with the option off/unset (plan-4bba64). PHP
4477 - // now emits 'on'/'off' strings; true/'1'/1 keep cached pre-fix HTML
4478 - // (boolean-true localizations) working.
4479 - // NOTE (plan-32db95): this gate reads the INLINE value at DOM ready and is
4480 - // deliberately NOT re-evaluated after the widget's dynamic-settings refresh
4481 - // merges fresh values over mxchatChat (that merge fires on first widget
4482 - // open, after this module has already decided). Re-evaluating would mean
4483 - // restructuring the whole module to late-bind its listeners — not worth it
4484 - // for a prompt that is at worst stale for one page load on a cached page.
4485 - var sre = mxchatChat.satisfaction_rating_enabled;
4486 - if (sre !== 'on' && sre !== true && sre !== '1' && sre !== 1) return;
3951 + if (mxchatChat.satisfaction_rating_enabled === false || mxchatChat.satisfaction_rating_enabled === 'off') return;
4487 3952
4488 3953 // wp_localize_script stringifies ints, so accept both number and numeric string.
4489 3954 var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
4490 3955 var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);