PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.7
MxChat – AI Chatbot & Content Generation for WordPress v3.1.7
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/chat-script.js +68 -784 3.2.73.1.7 View file →
@@ -1,111 +1,23 @@
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') {
3 + // Nonce refresh is deferred until first user interaction (ensureSession)
4 + // to avoid admin-ajax calls on passive page loads.
5 + var nonceRefreshed = false;
6 + function refreshNonceIfNeeded(callback) {
7 + if (nonceRefreshed || typeof mxchatChat === 'undefined' || !mxchatChat.ajax_url) {
55 8 if (callback) callback();
56 9 return;
57 10 }
58 - var now = Date.now();
59 - if (cachedFreshNonce && (now - cachedFreshNonceFetchedAt) < NONCE_TTL_MS) {
60 - mxchatChat.nonce = cachedFreshNonce;
11 + nonceRefreshed = true;
12 + $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' }, function(res) {
13 + if (res && res.success && res.data && res.data.nonce) {
14 + mxchatChat.nonce = res.data.nonce;
15 + }
61 16 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 - });
17 + });
100 18 }
101 19
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 20 // ====================================
109 21 // MULTI-INSTANCE MANAGEMENT SYSTEM
110 22 // ====================================
111 23
@@ -148,10 +60,10 @@
148 60 return Object.keys(this.instances);
149 61 },
150 62
151 63 // Session management per bot
152 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
153 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
64 + // Returns existing session ID from cookie or localStorage, or null if none exists.
65 + // Does NOT create a new session — use ensureSession() for that.
154 66 getChatSession: function(botId) {
155 67 var cookieName = 'mxchat_session_id_' + botId;
156 68 var storageKey = 'mxchat_session_id_' + botId;
157 69 var sessionId = getCookie(cookieName);
@@ -160,21 +72,8 @@
160 72 if (!sessionId) {
161 73 try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
162 74 }
163 75
164 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
165 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
166 - // storage). Without this, ensureSession() can generate and store an ID that
167 - // getChatSession() then can't read back, causing null session_ids on send.
168 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
169 - sessionId = this.instances[botId].sessionId;
170 - }
171 -
172 - // Guard against stored sentinel values that indicate earlier broken writes.
173 - if (sessionId === 'null' || sessionId === 'undefined') {
174 - sessionId = null;
175 - }
176 -
177 76 // Re-sync cookie from localStorage if cookie was lost
178 77 if (sessionId && !getCookie(cookieName)) {
179 78 document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
180 79 }
@@ -206,10 +105,12 @@
206 105 // Now that we have a session, do the deferred work
207 106 refreshNonceIfNeeded();
208 107 trackOriginatingPage();
209 108
210 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
211 - // so we do NOT call it here to avoid a race condition.
109 + var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
110 + if (chatPersistenceEnabled && mxchatChat.email_collection_enabled !== 'on') {
111 + loadChatHistory(botId);
112 + }
212 113
213 114 return instance.sessionId;
214 115 },
215 116
@@ -700,28 +601,13 @@
700 601
701 602 // Get instance for session start timestamp (used when persistence is OFF)
702 603 var instance = MxChatInstances.get(botId);
703 604
704 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
705 - // and returns the guaranteed-present session id from the in-memory instance even when
706 - // cookie/localStorage writes are silently blocked by the browser.
707 - var sessionId = MxChatInstances.ensureSession(botId);
708 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
709 - // Last-resort generation to ensure we never POST a null marker.
710 - sessionId = generateSessionId();
711 - MxChatInstances.setChatSession(botId, sessionId);
712 - }
713 -
714 - // Wait for the page-cache nonce refresh to complete before firing the
715 - // chat-send AJAX. On cached pages the inline mxchatChat.nonce is stale
716 - // until refreshNonceIfNeeded() returns; constructing ajaxData inside the
717 - // callback guarantees we read the fresh value. See plan-c5457f.
718 - refreshNonceIfNeeded(function() {
719 605 // Prepare AJAX data
720 606 const ajaxData = {
721 607 action: 'mxchat_handle_chat_request',
722 608 message: message,
723 - session_id: sessionId,
609 + session_id: getChatSession(botId),
724 610 nonce: mxchatChat.nonce,
725 611 current_page_url: window.location.href,
726 612 current_page_title: document.title,
727 613 bot_id: botId,
@@ -727,14 +613,14 @@
727 613 bot_id: botId,
728 614 // Pass session start timestamp so AI context matches what user sees
729 615 session_start_timestamp: instance.sessionStartTimestamp || 0
730 616 };
731 -
617 +
732 618 // Add page context if available
733 619 if (pageContext) {
734 620 ajaxData.page_context = JSON.stringify(pageContext);
735 621 }
736 -
622 +
737 623 // CHECK FOR VISION FLAGS AND ADD THEM
738 624 if (window.mxchatVisionProcessed) {
739 625 ajaxData.vision_processed = true;
740 626 ajaxData.original_user_message = window.mxchatOriginalMessage || message;
@@ -743,9 +629,9 @@
743 629 window.mxchatVisionProcessed = false;
744 630 window.mxchatOriginalMessage = null;
745 631 window.mxchatVisionImagesCount = 0;
746 632 }
747 -
633 +
748 634 $.ajax({
749 635 url: mxchatChat.ajax_url,
750 636 type: 'POST',
751 637 dataType: 'json',
@@ -934,9 +820,8 @@
934 820
935 821 replaceLastMessage("bot", errorMessage, '', [], botId);
936 822 }
937 823 });
938 - }); // refreshNonceIfNeeded
939 824 }
940 825
941 826 function callMxChatStream(message, callback, botId) {
942 827 botId = botId || getMxChatBotId();
@@ -955,25 +840,12 @@
955 840
956 841 // Get instance for session start timestamp (used when persistence is OFF)
957 842 var instance = MxChatInstances.get(botId);
958 843
959 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
960 - // non-string value via String(), so passing `null` would POST the literal string "null"
961 - // and land in the transcripts table as a ghost session. ensureSession() always returns
962 - // a real string even when cookies/localStorage are blocked.
963 - var streamSessionId = MxChatInstances.ensureSession(botId);
964 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
965 - streamSessionId = generateSessionId();
966 - MxChatInstances.setChatSession(botId, streamSessionId);
967 - }
968 -
969 - // Wait for the page-cache nonce refresh before constructing formData (which
970 - // captures mxchatChat.nonce by value). Mirrors callMxChat's wrapping. See plan-c5457f.
971 - refreshNonceIfNeeded(function() {
972 844 const formData = new FormData();
973 845 formData.append('action', 'mxchat_stream_chat');
974 846 formData.append('message', message);
975 - formData.append('session_id', streamSessionId);
847 + formData.append('session_id', getChatSession(botId));
976 848 formData.append('nonce', mxchatChat.nonce);
977 849 formData.append('current_page_url', window.location.href);
978 850 formData.append('current_page_title', document.title);
979 851 formData.append('bot_id', botId);
@@ -1073,16 +945,8 @@
1073 945
1074 946 // Re-enable chat input when stream ends with content
1075 947 enableChatInput(botId);
1076 948
1077 - // Scroll the user's last message to the top now that the
1078 - // bot's full reply has rendered (gives max reading room).
1079 - var $chatBoxDone = getElement(botId, 'chat-box');
1080 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
1081 - if ($lastUserMsgDone.length) {
1082 - scrollElementToTop($lastUserMsgDone, botId);
1083 - }
1084 -
1085 949 if (callback) {
1086 950 callback(accumulatedContent);
1087 951 }
1088 952 return;
@@ -1105,16 +969,8 @@
1105 969
1106 970 // Re-enable chat input after streaming completes
1107 971 enableChatInput(botId);
1108 972
1109 - // Scroll the user's last message to the top now
1110 - // that the bot's full reply has rendered.
1111 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1112 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1113 - if ($lastUserMsgStreamDone.length) {
1114 - scrollElementToTop($lastUserMsgStreamDone, botId);
1115 - }
1116 -
1117 973 if (callback) {
1118 974 callback(accumulatedContent);
1119 975 }
1120 976 return;
@@ -1193,9 +1049,8 @@
1193 1049 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1194 1050 callMxChat(message, callback, botId);
1195 1051 }
1196 1052 });
1197 - }); // refreshNonceIfNeeded
1198 1053 }
1199 1054
1200 1055 // Helper function to handle non-streaming responses
1201 1056 function handleNonStreamResponse(data, callback, botId) {
@@ -1424,229 +1279,9 @@
1424 1279 sendMessage(botId);
1425 1280 }
1426 1281 });
1427 1282
1428 -// Builds the list of overflow-menu items for a given bot.
1429 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1430 -function mxchatGetHeaderMenuItems(botId) {
1431 - var items = [];
1432 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1433 -
1434 - // The `print_button_*` keys still gate this item for back-compat with
1435 - // existing user options. The action is now a transcript download, not print.
1436 - if (settings.print_button_enabled === 'on') {
1437 - items.push({
1438 - id: 'download-transcript',
1439 - label: settings.print_button_label || 'Download Transcript',
1440 - 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>',
1441 - action: function() {
1442 - mxchatDownloadTranscript(botId);
1443 - }
1444 - });
1445 - }
1446 -
1447 - return items;
1448 -}
1449 -
1450 -// Builds a clean markdown transcript of the current conversation and triggers
1451 -// a file download. Used by the "Download Transcript" menu item.
1452 -function mxchatDownloadTranscript(botId) {
1453 - var $chatBox = getElement(botId, 'chat-box');
1454 - if (!$chatBox || !$chatBox.length) return;
1455 -
1456 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1457 - var headerTitle = settings.print_header_title || 'Chat transcript';
1458 - var now = new Date();
1459 - var stamp = now.toLocaleString();
1460 -
1461 - var lines = [];
1462 - lines.push('# ' + headerTitle);
1463 - lines.push('');
1464 - lines.push('Exported: ' + stamp);
1465 - lines.push('');
1466 - lines.push('---');
1467 - lines.push('');
1468 -
1469 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1470 - var $msg = $(this);
1471 - // Skip thinking placeholders and any in-flight temporary messages.
1472 - if ($msg.find('.thinking-dots').length) return;
1473 - if ($msg.hasClass('temporary-message')) return;
1474 -
1475 - var sender;
1476 - if ($msg.hasClass('user-message')) sender = 'User';
1477 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1478 - else sender = 'AI Agent';
1479 -
1480 - // Strip interactive UI from the cloned message so we get the conversation text.
1481 - var $clone = $msg.clone();
1482 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1483 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1484 - if (!text) return;
1485 -
1486 - lines.push('**' + sender + '**');
1487 - lines.push('');
1488 - lines.push(text);
1489 - lines.push('');
1490 - });
1491 -
1492 - var content = lines.join('\n');
1493 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1494 - var fname = 'mxchat-transcript-' + iso + '.md';
1495 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1496 - var url = URL.createObjectURL(blob);
1497 - var a = document.createElement('a');
1498 - a.href = url;
1499 - a.download = fname;
1500 - a.style.display = 'none';
1501 - document.body.appendChild(a);
1502 - a.click();
1503 - setTimeout(function() {
1504 - if (a.parentNode) a.parentNode.removeChild(a);
1505 - URL.revokeObjectURL(url);
1506 - }, 100);
1507 -}
1508 -
1509 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1510 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1511 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1512 -function mxchatSyncMenuColors(botId, $wrap) {
1513 - if (!$wrap || !$wrap.length) return;
1514 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1515 - if (!$bot.length) return;
1516 - var cs = window.getComputedStyle($bot[0]);
1517 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1518 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1519 - }
1520 - // Bot text color usually lives on a child div, not .bot-message itself.
1521 - var $textChild = $bot.find('[style*="color"]').first();
1522 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1523 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1524 -}
1525 -
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 -
1532 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1533 - var $menu = $wrap.find('.mxchat-header-menu');
1534 - var items = mxchatGetHeaderMenuItems(botId);
1535 -
1536 - // Initial color sync — covers normal page load.
1537 - mxchatSyncMenuColors(botId, $wrap);
1538 -
1539 - if (!items.length) {
1540 - $trigger.hide();
1541 - $menu.hide();
1542 - $wrap.data('mxchatMenuReady', true);
1543 - return;
1544 - }
1545 -
1546 - // Build the menu items.
1547 - $menu.empty();
1548 - items.forEach(function(item, idx) {
1549 - var $btn = $('<button>', {
1550 - type: 'button',
1551 - 'class': 'mxchat-menu-item',
1552 - 'role': 'menuitem',
1553 - 'tabindex': '-1',
1554 - 'data-menu-id': item.id,
1555 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1556 - '<span class="mxchat-menu-item-label"></span>'
1557 - });
1558 - $btn.find('.mxchat-menu-item-label').text(item.label);
1559 - $btn.on('click', function(e) {
1560 - e.preventDefault();
1561 - e.stopPropagation();
1562 - closeMenu();
1563 - try { item.action(); } catch (err) { /* no-op */ }
1564 - });
1565 - $menu.append($btn);
1566 - });
1567 -
1568 - function openMenu() {
1569 - // Re-sync each open in case the active theme changed since init.
1570 - mxchatSyncMenuColors(botId, $wrap);
1571 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1572 - $trigger.attr('aria-expanded', 'true');
1573 - // Focus the first item for keyboard users
1574 - setTimeout(function() {
1575 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1576 - }, 0);
1577 - }
1578 - function closeMenu(returnFocus) {
1579 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1580 - $trigger.attr('aria-expanded', 'false');
1581 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1582 - if (returnFocus) $trigger.trigger('focus');
1583 - }
1584 -
1585 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1586 - // click-to-collapse handler does not fire.
1587 - $trigger.on('click', function(e) {
1588 - e.preventDefault();
1589 - e.stopPropagation();
1590 - if ($menu.hasClass('is-open')) closeMenu();
1591 - else openMenu();
1592 - });
1593 -
1594 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1595 - $menu.on('click', function(e) {
1596 - e.stopPropagation();
1597 - });
1598 -
1599 - // Outside click closes the menu.
1600 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1601 - if (!$menu.hasClass('is-open')) return;
1602 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1603 - closeMenu();
1604 - });
1605 -
1606 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1607 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1608 - var $items = $menu.find('.mxchat-menu-item');
1609 - var idx = $items.index(this);
1610 - if (e.key === 'Escape') {
1611 - e.preventDefault();
1612 - closeMenu(true);
1613 - } else if (e.key === 'ArrowDown') {
1614 - e.preventDefault();
1615 - var $next = $items.eq((idx + 1) % $items.length);
1616 - $items.attr('tabindex', '-1');
1617 - $next.attr('tabindex', '0').trigger('focus');
1618 - } else if (e.key === 'ArrowUp') {
1619 - e.preventDefault();
1620 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1621 - $items.attr('tabindex', '-1');
1622 - $prev.attr('tabindex', '0').trigger('focus');
1623 - } else if (e.key === 'Enter' || e.key === ' ') {
1624 - e.preventDefault();
1625 - $(this).trigger('click');
1626 - }
1627 - });
1628 - $trigger.on('keydown', function(e) {
1629 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1630 - e.preventDefault();
1631 - closeMenu(true);
1632 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1633 - e.preventDefault();
1634 - openMenu();
1635 - }
1636 - });
1637 -
1638 - $wrap.data('mxchatMenuReady', true);
1639 -}
1640 -
1641 -// Initialize header menus for every rendered widget on DOM ready.
1642 -$(function() {
1643 - $('.mxchat-header-menu-wrap').each(function() {
1644 - var botId = $(this).data('bot-id');
1645 - if (botId) mxchatInitHeaderMenu(botId);
1646 - });
1647 -});
1648 -
1283 +
1649 1284 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1650 1285 try {
1651 1286 // Determine styles based on sender type
1652 1287 let messageClass, bgColor, fontColor;
@@ -1738,12 +1373,8 @@
1738 1373 if (lastUserMessage.length) {
1739 1374 scrollElementToTop(lastUserMessage, botId);
1740 1375 }
1741 1376 }
1742 -
1743 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1744 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1745 - }
1746 1377 });
1747 1378
1748 1379 if (messageText.id) {
1749 1380 var instance = MxChatInstances.get(botId);
@@ -1890,12 +1521,8 @@
1890 1521 }
1891 1522
1892 1523 // Re-enable chat input after response is displayed
1893 1524 enableChatInput(botId);
1894 -
1895 - if (sender === "bot" || sender === "agent") {
1896 - if (typeof mxchatInitHeaderMenu === 'function') mxchatInitHeaderMenu(botId);
1897 - }
1898 1525 } else {
1899 1526 appendMessage(sender, responseText, responseHtml, images, false, botId);
1900 1527 // Re-enable chat input after response is displayed
1901 1528 enableChatInput(botId);
@@ -2348,14 +1975,13 @@
2348 1975 requestAnimationFrame(smoothScroll);
2349 1976 }
2350 1977 }
2351 1978
2352 - function scrollElementToTop(element, botId, topOffset) {
1979 + function scrollElementToTop(element, botId) {
2353 1980 botId = botId || 'default';
2354 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2355 1981 var chatBox = getElement(botId, 'chat-box');
2356 1982 var elementTop = element.position().top + chatBox.scrollTop();
2357 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
1983 + chatBox.animate({ scrollTop: elementTop }, 500);
2358 1984 }
2359 1985
2360 1986 function showChatWidget(botId) {
2361 1987 botId = botId || 'default';
@@ -2666,19 +2292,9 @@
2666 2292 var content = message.content;
2667 2293 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2668 2294 content = decodeHTMLEntities(content);
2669 2295
2670 - // Skip linkify for messages containing structured HTML
2671 - // (forms, product cards, galleries, etc.) to avoid
2672 - // markdown formatting corrupting HTML attributes
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") ||
2677 - content.includes("<form") ||
2678 - content.includes("<input") ||
2679 - content.includes("<select") ||
2680 - content.includes("<textarea")) {
2296 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2681 2297 messageElement.html(content);
2682 2298 } else {
2683 2299 var formattedContent = linkify(content);
2684 2300 messageElement.html(formattedContent);
@@ -2987,14 +2603,9 @@
2987 2603 collapseQuickQuestions(botId);
2988 2604 });
2989 2605
2990 2606 // Chatbot visibility toggle handlers - use class selector for multi-instance support
2991 - // Handles click + Enter/Space keypresses for keyboard accessibility (WCAG 2.1 SC 2.1.1).
2992 - $(document).on('click keydown', '.floating-chatbot-button', function(e) {
2993 - if (e.type === 'keydown') {
2994 - if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
2995 - e.preventDefault();
2996 - }
2607 + $(document).on('click', '.floating-chatbot-button', function() {
2997 2608 var botId = getBotIdFromElement(this);
2998 2609 var $chatbot = getElement(botId, 'floating-chatbot');
2999 2610 var $badge = getElement(botId, 'chat-notification-badge');
3000 2611 var $preChat = getElement(botId, 'pre-chat-message');
@@ -2999,11 +2610,10 @@
2999 2610 var $badge = getElement(botId, 'chat-notification-badge');
3000 2611 var $preChat = getElement(botId, 'pre-chat-message');
3001 2612
3002 2613 if ($chatbot.hasClass('hidden')) {
3003 - $chatbot.removeClass('hidden').addClass('visible')
3004 - .attr('aria-modal', 'true').attr('role', 'dialog');
3005 - $(this).addClass('hidden').attr('aria-expanded', 'true');
2614 + $chatbot.removeClass('hidden').addClass('visible');
2615 + $(this).addClass('hidden');
3006 2616 $badge.hide(); // Hide notification when opening chat
3007 2617 disableScroll();
3008 2618 $preChat.fadeOut(250);
3009 2619
@@ -3018,60 +2628,25 @@
3018 2628 var instance = MxChatInstances.get(botId);
3019 2629 if (emailBlocker && !instance.emailCheckDone) {
3020 2630 instance.emailCheckDone = true;
3021 2631 resolveEmailState(botId);
3022 - } else if (!emailBlocker) {
3023 - // No email collection — still route through showChatContainerForBot
3024 - // so the loader is shown while chat history loads
3025 - showChatContainerForBot(botId);
3026 2632 }
3027 -
3028 - // Move keyboard focus into the message input after the open transition.
3029 - setTimeout(function() {
3030 - var chatInput = getElementDOM(botId, 'chat-input');
3031 - if (chatInput && !chatInput.disabled) {
3032 - try { chatInput.focus({ preventScroll: true }); } catch (err) { chatInput.focus(); }
3033 - }
3034 - }, 300);
3035 2633 } else {
3036 - $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
3037 - $(this).removeClass('hidden').attr('aria-expanded', 'false');
2634 + $chatbot.removeClass('visible').addClass('hidden');
2635 + $(this).removeClass('hidden');
3038 2636 enableScroll();
3039 2637 checkPreChatDismissal(botId);
3040 2638 }
3041 2639 });
3042 2640
3043 - // Allow clicking anywhere on the title bar to close the chatbot.
3044 - // Returns keyboard focus to the launcher so keyboard users don't get
3045 - // stranded at <body> (WCAG SC 2.4.3 Focus Order). :focus-visible is
3046 - // heuristic-based so mouse-triggered close won't show a focus ring.
2641 + // Allow clicking anywhere on the title bar to close the chatbot
3047 2642 $(document).on('click', '.chatbot-top-bar', function() {
3048 2643 var botId = getBotIdFromElement(this);
3049 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3050 - var $launcher = getElement(botId, 'floating-chatbot-button');
3051 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
2644 + getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2645 + getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3052 2646 enableScroll();
3053 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3054 2647 });
3055 2648
3056 - // Global Escape-key handler — closes any visible chat widget and
3057 - // returns focus to its launcher. Standard modal-dismissal pattern;
3058 - // pairs with aria-modal="true" set on the widget when it opens.
3059 - $(document).on('keydown', function(e) {
3060 - if (e.key !== 'Escape' && e.key !== 'Esc') return;
3061 - var $visible = $('.floating-chatbot.visible');
3062 - if (!$visible.length) return;
3063 - e.preventDefault();
3064 - $visible.each(function() {
3065 - var botId = getBotIdFromElement(this);
3066 - $(this).addClass('hidden').removeClass('visible').removeAttr('aria-modal');
3067 - var $launcher = getElement(botId, 'floating-chatbot-button');
3068 - $launcher.removeClass('hidden').attr('aria-expanded', 'false');
3069 - try { $launcher.trigger('focus'); } catch (err) { /* no-op */ }
3070 - });
3071 - enableScroll();
3072 - });
3073 -
3074 2649 $(document).on('click', '.close-pre-chat-message', function(e) {
3075 2650 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
3076 2651 var botId = getBotIdFromElement(this);
3077 2652 handlePreChatDismissal(botId);
@@ -3116,10 +2691,8 @@
3116 2691 const sendBtn = document.getElementById('send-button');
3117 2692 const originalBtnContent = uploadBtn.innerHTML;
3118 2693
3119 2694 try {
3120 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3121 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3122 2695 const formData = new FormData();
3123 2696 formData.append('action', 'mxchat_upload_pdf');
3124 2697 formData.append('pdf_file', file);
3125 2698 formData.append('session_id', sessionId);
@@ -3183,10 +2756,8 @@
3183 2756 const sendBtn = document.getElementById('send-button');
3184 2757 const originalBtnContent = uploadBtn.innerHTML;
3185 2758
3186 2759 try {
3187 - // Wait for page-cache nonce refresh before reading mxchatChat.nonce. See plan-c5457f.
3188 - await new Promise(function(resolve) { refreshNonceIfNeeded(resolve); });
3189 2760 const formData = new FormData();
3190 2761 formData.append('action', 'mxchat_upload_word');
3191 2762 formData.append('word_file', file);
3192 2763 formData.append('session_id', sessionId);
@@ -3280,59 +2851,8 @@
3280 2851 });
3281 2852
3282 2853
3283 2854 // ====================================
3284 -// INIT LOADER & CHAT CONTAINER HELPERS
3285 -// ====================================
3286 -// These must be outside the email collection block so they're always available
3287 -// (used by persistence loading even when email collection is off)
3288 -
3289 -function showInitLoader(botId) {
3290 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3291 - if (loader) loader.style.display = 'flex';
3292 -}
3293 -
3294 -function hideInitLoader(botId) {
3295 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3296 - if (loader) loader.style.display = 'none';
3297 -}
3298 -
3299 -function showEmailFormForBot(botId) {
3300 - hideInitLoader(botId);
3301 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3302 - var chatContainer = getElementDOM(botId, 'chat-container');
3303 - if (emailBlocker) emailBlocker.style.display = 'flex';
3304 - if (chatContainer) chatContainer.style.display = 'none';
3305 -}
3306 -
3307 -function showChatContainerForBot(botId) {
3308 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3309 - var chatContainer = getElementDOM(botId, 'chat-container');
3310 - if (emailBlocker) emailBlocker.style.display = 'none';
3311 -
3312 - var instance = MxChatInstances.get(botId);
3313 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3314 -
3315 - // If persistence is on and history hasn't loaded yet, show loader
3316 - // while history loads to prevent flash of empty chat
3317 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3318 - if (chatContainer) chatContainer.style.display = 'none';
3319 - showInitLoader(botId);
3320 - loadChatHistory(botId, function() {
3321 - hideInitLoader(botId);
3322 - if (chatContainer) chatContainer.style.display = 'flex';
3323 - scrollToBottom(botId, true);
3324 - });
3325 - } else {
3326 - hideInitLoader(botId);
3327 - if (chatContainer) chatContainer.style.display = 'flex';
3328 - if (typeof loadChatHistory === 'function') {
3329 - loadChatHistory(botId);
3330 - }
3331 - }
3332 -}
3333 -
3334 -// ====================================
3335 2855 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3336 2856 // ====================================
3337 2857 // Only run email collection setup if it's enabled
3338 2858 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
@@ -3368,8 +2888,40 @@
3368 2888 `;
3369 2889 document.head.appendChild(style);
3370 2890 }
3371 2891
2892 + // Helper functions for email collection (multi-instance aware)
2893 + function showEmailFormForBot(botId) {
2894 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2895 + var chatContainer = getElementDOM(botId, 'chat-container');
2896 + if (emailBlocker) emailBlocker.style.display = 'flex';
2897 + if (chatContainer) chatContainer.style.display = 'none';
2898 + }
2899 +
2900 + function showChatContainerForBot(botId) {
2901 + var emailBlocker = getElementDOM(botId, 'email-blocker');
2902 + var chatContainer = getElementDOM(botId, 'chat-container');
2903 + if (emailBlocker) emailBlocker.style.display = 'none';
2904 +
2905 + var instance = MxChatInstances.get(botId);
2906 + var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2907 +
2908 + // If persistence is on and history hasn't loaded yet, keep container
2909 + // hidden until history loads to prevent flash of empty chat
2910 + if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
2911 + if (chatContainer) chatContainer.style.display = 'none';
2912 + loadChatHistory(botId, function() {
2913 + if (chatContainer) chatContainer.style.display = 'flex';
2914 + scrollToBottom(botId, true);
2915 + });
2916 + } else {
2917 + if (chatContainer) chatContainer.style.display = 'flex';
2918 + if (typeof loadChatHistory === 'function') {
2919 + loadChatHistory(botId);
2920 + }
2921 + }
2922 + }
2923 +
3372 2924 function isValidEmailAddress(email) {
3373 2925 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3374 2926 return emailRegex.test(email.trim()) && email.length <= 254;
3375 2927 }
@@ -3507,14 +3059,13 @@
3507 3059
3508 3060 function checkSessionAndEmailForBot(botId) {
3509 3061 const sessionId = MxChatInstances.ensureSession(botId);
3510 3062
3511 - // Hide both panels while we check — show loader instead
3063 + // Hide both panels while we check — prevents flash of wrong state
3512 3064 var emailBlocker = getElementDOM(botId, 'email-blocker');
3513 3065 var chatContainer = getElementDOM(botId, 'chat-container');
3514 3066 if (emailBlocker) emailBlocker.style.display = 'none';
3515 3067 if (chatContainer) chatContainer.style.display = 'none';
3516 - showInitLoader(botId);
3517 3068
3518 3069 fetch(mxchatChat.ajax_url, {
3519 3070 method: 'POST',
3520 3071 headers: {
@@ -3694,8 +3245,9 @@
3694 3245 $('.mxchat-chatbot-wrapper').each(function() {
3695 3246 var botId = $(this).data('bot-id') || 'default';
3696 3247 var emailBlocker = getElementDOM(botId, 'email-blocker');
3697 3248
3249 + // Only check if email blocker exists for this bot
3698 3250 if (emailBlocker) {
3699 3251 if (isEmbeddedBot(botId)) {
3700 3252 // Embedded bots are always visible — check now
3701 3253 resolveEmailState(botId);
@@ -3700,15 +3252,8 @@
3700 3252 // Embedded bots are always visible — check now
3701 3253 resolveEmailState(botId);
3702 3254 }
3703 3255 // Floating bots: handled in the widget open handler
3704 - } else if (isEmbeddedBot(botId)) {
3705 - // Embedded bot, no email collection — load history with loader
3706 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3707 - if (chatPersistenceEnabled) {
3708 - MxChatInstances.ensureSession(botId);
3709 - showChatContainerForBot(botId);
3710 - }
3711 3256 }
3712 3257 });
3713 3258 }
3714 3259
@@ -3733,10 +3278,8 @@
3733 3278 var instance = MxChatInstances.get(botId);
3734 3279 if (emailBlocker && !instance.emailCheckDone) {
3735 3280 instance.emailCheckDone = true;
3736 3281 resolveEmailState(botId);
3737 - } else if (!emailBlocker) {
3738 - showChatContainerForBot(botId);
3739 3282 }
3740 3283 }
3741 3284 });
3742 3285
@@ -3936,265 +3479,6 @@
3936 3479 }, 2000);
3937 3480 });
3938 3481 }
3939 3482 }
3940 -});
3941 -
3942 -// ============================================================================
3943 -// SATISFACTION RATING (v3.2.6)
3944 -// ============================================================================
3945 -// Per-session 👍/👎 prompt that appears in the chat-box after 60s of user
3946 -// inactivity following a bot reply. One prompt per session, deduped via
3947 -// localStorage. Disabled site-wide when mxchatChat.satisfaction_rating_enabled
3948 -// is exactly false (default ON).
3949 -jQuery(function($) {
3950 - if (typeof mxchatChat === 'undefined') return;
3951 - if (mxchatChat.satisfaction_rating_enabled === false || mxchatChat.satisfaction_rating_enabled === 'off') return;
3952 -
3953 - // wp_localize_script stringifies ints, so accept both number and numeric string.
3954 - var idleRaw = mxchatChat.satisfaction_rating_idle_seconds;
3955 - var idleSeconds = (typeof idleRaw === 'number') ? idleRaw : parseInt(idleRaw, 10);
3956 - if (!isFinite(idleSeconds)) idleSeconds = 60;
3957 - if (idleSeconds < 5) idleSeconds = 5;
3958 - if (idleSeconds > 600) idleSeconds = 600;
3959 - var IDLE_MS = idleSeconds * 1000;
3960 - var MIN_BOT_REPLIES = 2;
3961 - var ratingState = {};
3962 -
3963 - function getState(botId) {
3964 - if (!ratingState[botId]) {
3965 - ratingState[botId] = { idleTimer: null, botReplies: 0, promptShown: false, dismissed: false };
3966 - }
3967 - return ratingState[botId];
3968 - }
3969 -
3970 - function getSessionId(botId) {
3971 - if (typeof MxChatInstances !== 'undefined' && MxChatInstances.getChatSession) {
3972 - return MxChatInstances.getChatSession(botId);
3973 - }
3974 - return null;
3975 - }
3976 -
3977 - function isAlreadyRated(sessionId) {
3978 - if (!sessionId) return false;
3979 - try { return localStorage.getItem('mxchat_rated:' + sessionId) === '1'; } catch (e) { return false; }
3980 - }
3981 -
3982 - function markRated(sessionId) {
3983 - if (!sessionId) return;
3984 - try { localStorage.setItem('mxchat_rated:' + sessionId, '1'); } catch (e) {}
3985 - }
3986 -
3987 - function esc(s) {
3988 - return String(s == null ? '' : s)
3989 - .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
3990 - .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
3991 - }
3992 -
3993 - // Mirror shouldSkipInlineColors so rating bubbles defer to AI-theme CSS.
3994 - function ratingSkipInlineColors(botId) {
3995 - if (mxchatChat.skip_inline_colors) return true;
3996 - var botAssignments = mxchatChat.bot_theme_assignments || {};
3997 - return botAssignments.hasOwnProperty(botId);
3998 - }
3999 -
4000 - function botBubbleStyleAttr(botId) {
4001 - if (ratingSkipInlineColors(botId)) return '';
4002 - var bg = mxchatChat.bot_message_bg_color;
4003 - var fg = mxchatChat.bot_message_font_color;
4004 - if (!bg && !fg) return '';
4005 - return ' style="background-color: ' + esc(bg || '') + '; color: ' + esc(fg || '') + ';"';
4006 - }
4007 -
4008 - function copy(key) {
4009 - var c = mxchatChat.satisfaction_rating_copy || {};
4010 - var d = {
4011 - question: 'Was this helpful?',
4012 - helpful: 'Helpful',
4013 - not_helpful: 'Not helpful',
4014 - dismiss: 'Dismiss',
4015 - thanks: 'Thanks! Anything we should improve? (optional)',
4016 - placeholder: 'Tell us what could be better…',
4017 - send: 'Send',
4018 - skip: 'Skip',
4019 - saved: 'Thanks for the feedback.'
4020 - };
4021 - return c[key] || d[key];
4022 - }
4023 -
4024 - function thumbUpSvg() {
4025 - 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>';
4026 - }
4027 - function thumbDownSvg() {
4028 - 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>';
4029 - }
4030 -
4031 - function buildPromptHtml(botId) {
4032 - var styleAttr = botBubbleStyleAttr(botId);
4033 - return ''
4034 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4035 - + '<div class="mxchat-rating-prompt" data-bot-id="' + esc(botId) + '" role="group" aria-label="' + esc(copy('question')) + '">'
4036 - + '<div class="mxchat-rating-question">' + esc(copy('question')) + '</div>'
4037 - + '<div class="mxchat-rating-actions">'
4038 - + '<span class="mxchat-rating-buttons">'
4039 - + '<button type="button" class="mxchat-rating-btn" data-rating="1" aria-label="' + esc(copy('helpful')) + '">' + thumbUpSvg() + '</button>'
4040 - + '<button type="button" class="mxchat-rating-btn" data-rating="-1" aria-label="' + esc(copy('not_helpful')) + '">' + thumbDownSvg() + '</button>'
4041 - + '</span>'
4042 - + '<button type="button" class="mxchat-rating-dismiss" aria-label="' + esc(copy('dismiss')) + '">×</button>'
4043 - + '</div>'
4044 - + '</div>'
4045 - + '</div>';
4046 - }
4047 -
4048 - function buildFeedbackHtml(botId, rating) {
4049 - var styleAttr = botBubbleStyleAttr(botId);
4050 - return ''
4051 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4052 - + '<div class="mxchat-rating-feedback" data-bot-id="' + esc(botId) + '" data-rating="' + esc(String(rating)) + '">'
4053 - + '<div class="mxchat-rating-feedback-label">' + esc(copy('thanks')) + '</div>'
4054 - + '<textarea class="mxchat-rating-feedback-input" maxlength="500" placeholder="' + esc(copy('placeholder')) + '" rows="2"></textarea>'
4055 - + '<div class="mxchat-rating-feedback-actions">'
4056 - + '<button type="button" class="mxchat-rating-skip">' + esc(copy('skip')) + '</button>'
4057 - + '<button type="button" class="mxchat-rating-submit">' + esc(copy('send')) + '</button>'
4058 - + '</div>'
4059 - + '</div>'
4060 - + '</div>';
4061 - }
4062 -
4063 - function buildSavedHtml(botId) {
4064 - var styleAttr = botBubbleStyleAttr(botId);
4065 - return ''
4066 - + '<div class="bot-message mxchat-rating-bot-bubble"' + styleAttr + '>'
4067 - + '<div class="mxchat-rating-saved">' + esc(copy('saved')) + '</div>'
4068 - + '</div>';
4069 - }
4070 -
4071 - function getChatBoxByBotId(botId) {
4072 - var $byId = $('#chat-box-' + botId);
4073 - if ($byId.length) return $byId.first();
4074 - return $('.chat-box').first();
4075 - }
4076 -
4077 - function scrollChatBoxToBottom($chatBox) {
4078 - if (!$chatBox || !$chatBox.length) return;
4079 - $chatBox.scrollTop($chatBox[0].scrollHeight);
4080 - }
4081 -
4082 - function showPrompt(botId) {
4083 - var s = getState(botId);
4084 - if (s.promptShown || s.dismissed) return;
4085 - var sessionId = getSessionId(botId);
4086 - if (!sessionId) return;
4087 - if (isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4088 - var $chatBox = getChatBoxByBotId(botId);
4089 - if (!$chatBox.length) return;
4090 - if ($chatBox.find('.mxchat-rating-prompt').length) { s.promptShown = true; return; }
4091 - $chatBox.append(buildPromptHtml(botId));
4092 - s.promptShown = true;
4093 - scrollChatBoxToBottom($chatBox);
4094 - }
4095 -
4096 - function submitRating(botId, rating, feedback) {
4097 - var sessionId = getSessionId(botId);
4098 - if (!sessionId) return;
4099 - $.post(mxchatChat.ajax_url, {
4100 - action: 'mxchat_save_rating',
4101 - session_id: sessionId,
4102 - bot_id: botId,
4103 - rating: rating,
4104 - feedback: feedback || ''
4105 - });
4106 - markRated(sessionId);
4107 - }
4108 -
4109 - function onBotReply(botId) {
4110 - var s = getState(botId);
4111 - s.botReplies += 1;
4112 - if (s.promptShown || s.dismissed) return;
4113 - var sessionId = getSessionId(botId);
4114 - if (sessionId && isAlreadyRated(sessionId)) { s.promptShown = true; return; }
4115 - if (s.botReplies < MIN_BOT_REPLIES) return;
4116 - if (s.idleTimer) clearTimeout(s.idleTimer);
4117 - s.idleTimer = setTimeout(function() { showPrompt(botId); }, IDLE_MS);
4118 - }
4119 -
4120 - function onUserMessage(botId) {
4121 - var s = getState(botId);
4122 - if (s.idleTimer) { clearTimeout(s.idleTimer); s.idleTimer = null; }
4123 - }
4124 -
4125 - function botIdFromChatBox(el) {
4126 - var id = el && el.id ? el.id : '';
4127 - return id.indexOf('chat-box-') === 0 ? id.substring('chat-box-'.length) : 'default';
4128 - }
4129 -
4130 - function setupObserver(chatBox) {
4131 - var botId = botIdFromChatBox(chatBox);
4132 - try {
4133 - var observer = new MutationObserver(function(mutations) {
4134 - mutations.forEach(function(m) {
4135 - for (var i = 0; i < m.addedNodes.length; i++) {
4136 - var node = m.addedNodes[i];
4137 - if (!node || node.nodeType !== 1) continue;
4138 - var $n = $(node);
4139 - if ($n.hasClass('mxchat-rating-bot-bubble') || $n.hasClass('mxchat-rating-prompt') || $n.hasClass('mxchat-rating-feedback') || $n.hasClass('mxchat-rating-saved')) continue;
4140 - 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)
4141 - else if ($n.hasClass('user-message')) onUserMessage(botId);
4142 - }
4143 - });
4144 - });
4145 - observer.observe(chatBox, { childList: true });
4146 - } catch (e) { /* noop */ }
4147 - }
4148 -
4149 - $('.chat-box').each(function() { setupObserver(this); });
4150 -
4151 - $(document).on('click', '.mxchat-rating-btn', function(e) {
4152 - e.preventDefault();
4153 - var $btn = $(this);
4154 - var $prompt = $btn.closest('.mxchat-rating-prompt');
4155 - var $wrap = $btn.closest('.mxchat-rating-bot-bubble');
4156 - var botId = $prompt.data('bot-id') || 'default';
4157 - var rating = parseInt($btn.attr('data-rating'), 10);
4158 - if (rating !== 1 && rating !== -1) return;
4159 - submitRating(botId, rating, '');
4160 - ($wrap.length ? $wrap : $prompt).replaceWith(buildFeedbackHtml(botId, rating));
4161 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4162 - });
4163 -
4164 - $(document).on('click', '.mxchat-rating-dismiss', function(e) {
4165 - e.preventDefault();
4166 - var $prompt = $(this).closest('.mxchat-rating-prompt');
4167 - var $wrap = $(this).closest('.mxchat-rating-bot-bubble');
4168 - var botId = $prompt.data('bot-id') || 'default';
4169 - var s = getState(botId);
4170 - s.dismissed = true;
4171 - markRated(getSessionId(botId));
4172 - ($wrap.length ? $wrap : $prompt).remove();
4173 - });
4174 -
4175 - function closeFeedback($fb) {
4176 - var botId = $fb.data('bot-id') || 'default';
4177 - var $wrap = $fb.closest('.mxchat-rating-bot-bubble');
4178 - ($wrap.length ? $wrap : $fb).replaceWith(buildSavedHtml(botId));
4179 - scrollChatBoxToBottom(getChatBoxByBotId(botId));
4180 - }
4181 -
4182 - $(document).on('click', '.mxchat-rating-skip', function(e) {
4183 - e.preventDefault();
4184 - closeFeedback($(this).closest('.mxchat-rating-feedback'));
4185 - });
4186 -
4187 - $(document).on('click', '.mxchat-rating-submit', function(e) {
4188 - e.preventDefault();
4189 - var $fb = $(this).closest('.mxchat-rating-feedback');
4190 - var botId = $fb.data('bot-id') || 'default';
4191 - var rating = parseInt($fb.attr('data-rating'), 10);
4192 - if (rating !== 1 && rating !== -1) { closeFeedback($fb); return; }
4193 - var text = String($fb.find('.mxchat-rating-feedback-input').val() || '').trim();
4194 - if (text !== '') {
4195 - submitRating(botId, rating, text);
4196 - }
4197 - closeFeedback($fb);
4198 - });
4199 3483 });
4200 3484