PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.25
MxChat – AI Chatbot & Content Generation for WordPress v3.2.25
3.2.25 3.2.24 3.2.23 3.2.22 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 All 156 releases
← All changes | js/chat-script.js +117 -76 3.2.22 → 3.2.25 View file →
@@ -847,9 +847,12 @@
847 847 if (typeof customMxChatFilter === 'function') {
848 848 message = customMxChatFilter(message, "prompt");
849 849 }
850 850
851 - if (message) {
851 + // Whitespace-only is empty: nothing is sent and nothing is disabled. When
852 + // there IS a message the original string (spaces and all) is what goes out,
853 + // so the transcript is unchanged (plan b67fd5).
854 + if (message && String(message).trim() !== '') {
852 855 // Don't disable input in live agent mode - let users chat freely
853 856 var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
854 857 var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
855 858 if (!isAgentMode) {
@@ -893,8 +896,14 @@
893 896 if (typeof customMxChatFilter === 'function') {
894 897 message = customMxChatFilter(message, "prompt");
895 898 }
896 899
900 + // Nothing to send means nothing to disable (plan b67fd5): a caller handing
901 + // over an empty string must not lock the box the way Enter-on-empty did.
902 + if (!message || String(message).trim() === '') {
903 + return;
904 + }
905 +
897 906 // Don't disable input in live agent mode - let users chat freely
898 907 var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
899 908 var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
900 909 if (!isAgentMode) {
@@ -1297,11 +1306,26 @@
1297 1306 let testingDataReceived = false;
1298 1307 let streamingStarted = false;
1299 1308 // Server-pushed html to append as its OWN bot bubble once the stream
1300 1309 // finishes (e.g. the consent-safe YouTube embed, plan 03ba33). Rendering is
1301 - // deferred to [DONE] so the embed always lands BELOW the streamed text.
1310 + // deferred to the end of the stream so the embed always lands BELOW the
1311 + // streamed text.
1302 1312 let pendingAppendHtml = '';
1303 1313
1314 + // Render the stashed appendix as its own bot bubble below the streamed
1315 + // text — mirrors how it is saved in the transcript, so history replays
1316 + // identically. Called from BOTH ends of a stream: the [DONE] line and
1317 + // the stream simply closing (the OpenAI Responses path and Gemini end
1318 + // without a [DONE] line). The stash is cleared, so a stream that sends
1319 + // [DONE] and then closes renders it once. Any future appendix-type
1320 + // event belongs in here, not in one of the two branches.
1321 + function flushAppendHtml() {
1322 + if (pendingAppendHtml) {
1323 + appendMessage("bot", "", pendingAppendHtml, [], false, botId);
1324 + pendingAppendHtml = '';
1325 + }
1326 + }
1327 +
1304 1328 // Abortable stream: a fresh controller per turn, keyed by bot instance.
1305 1329 // The Stop control (send button swapped in place) aborts both the read
1306 1330 // loop and the underlying request.
1307 1331 var streamControl = { controller: new AbortController(), aborted: false };
@@ -1382,8 +1406,10 @@
1382 1406
1383 1407 // Re-enable chat input when stream ends with content
1384 1408 enableChatInput(botId);
1385 1409
1410 + flushAppendHtml();
1411 +
1386 1412 // Scroll the user's last message to the top now that the
1387 1413 // bot's full reply has rendered (gives max reading room).
1388 1414 var $chatBoxDone = getElement(botId, 'chat-box');
1389 1415 var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
@@ -1414,16 +1440,9 @@
1414 1440
1415 1441 // Re-enable chat input after streaming completes
1416 1442 enableChatInput(botId);
1417 1443
1418 - // Render any server-pushed appendix html (e.g. the
1419 - // YouTube embed) as its own bot bubble below the
1420 - // streamed text — mirrors how it is saved in the
1421 - // transcript, so history replays identically.
1422 - if (pendingAppendHtml) {
1423 - appendMessage("bot", "", pendingAppendHtml, [], false, botId);
1424 - pendingAppendHtml = '';
1425 - }
1444 + flushAppendHtml();
1426 1445
1427 1446 // Scroll the user's last message to the top now
1428 1447 // that the bot's full reply has rendered.
1429 1448 var $chatBoxStreamDone = getElement(botId, 'chat-box');
@@ -1753,25 +1772,21 @@
1753 1772 if (this.__mxchatStopJustShown) return;
1754 1773 mxchatStopStreaming(botId);
1755 1774 return;
1756 1775 }
1757 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1758 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1759 - disableChatInput(botId);
1760 - }
1776 + // sendMessage() owns the disable - see the Enter handler below (plan b67fd5).
1761 1777 sendMessage(botId);
1762 1778 });
1763 1779
1764 -// Override enter key handler (using event delegation)
1780 +// Enter sends, Shift+Enter inserts a newline (using event delegation).
1781 +// sendMessage() owns the disable: it greys the box only when a real message is
1782 +// about to go out. Disabling here first left the widget locked for good after
1783 +// Enter on an empty box - the send returned early and no reply path ever
1784 +// re-enabled it (plan b67fd5).
1765 1785 $(document).on('keypress', '.chat-input', function(e) {
1766 1786 if (e.which == 13 && !e.shiftKey) {
1767 1787 e.preventDefault();
1768 - var botId = getBotIdFromElement(this);
1769 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1770 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1771 - disableChatInput(botId);
1772 - }
1773 - sendMessage(botId);
1788 + sendMessage(getBotIdFromElement(this));
1774 1789 }
1775 1790 });
1776 1791
1777 1792 // Chat input character counter + soft limit feedback (plan 7091a2).
@@ -2246,8 +2261,56 @@
2246 2261 // Error rendering message - silently continue
2247 2262 }
2248 2263 }
2249 2264
2265 +// Link clicks in bot and agent messages: the click is recorded, the browser
2266 +// follows the anchor itself. Navigation must not wait for the tracking
2267 +// request: a window.open() made from that request's callback is outside the
2268 +// visitor's tap, and Safari's pop-up blocker drops it without a message.
2269 +
2270 +// Give a tracked anchor the target it should open in, and noopener on a new tab.
2271 +function prepareTrackedLink($link) {
2272 + if (linkTarget === '_blank' && $link.attr('target') !== '_blank') {
2273 + $link.attr('target', '_blank');
2274 + }
2275 + if ($link.attr('target') === '_blank') {
2276 + var rel = $link.attr('rel') || '';
2277 + if (!/(^|\s)noopener(\s|$)/.test(rel)) {
2278 + $link.attr('rel', $.trim(rel + ' noopener'));
2279 + }
2280 + }
2281 +}
2282 +
2283 +// Record one click, fire-and-forget. sendBeacon survives the page unloading
2284 +// when the link opens in the same tab.
2285 +function trackLinkClick(href, messageContext, botId) {
2286 + var fields = {
2287 + action: 'mxchat_track_url_click',
2288 + session_id: getChatSession(botId),
2289 + url: href,
2290 + message_context: messageContext,
2291 + nonce: mxchatChat.nonce
2292 + };
2293 + try {
2294 + if (navigator.sendBeacon && typeof FormData !== 'undefined') {
2295 + var body = new FormData();
2296 + $.each(fields, function(name, value) {
2297 + body.append(name, value == null ? '' : value);
2298 + });
2299 + if (navigator.sendBeacon(mxchatChat.ajax_url, body)) {
2300 + return;
2301 + }
2302 + }
2303 + } catch (error) {
2304 + // Fall through to the plain request
2305 + }
2306 + $.ajax({
2307 + url: mxchatChat.ajax_url,
2308 + type: 'POST',
2309 + data: fields
2310 + });
2311 +}
2312 +
2250 2313 // Helper function to attach link tracking with proper event handling
2251 2314 function attachLinkTracking(messageDiv, messageText, botId) {
2252 2315 botId = botId || 'default';
2253 2316 // Use a slight delay to ensure DOM is ready
@@ -2265,11 +2328,13 @@
2265 2328 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
2266 2329 // Remove any existing click handlers first
2267 2330 $link.off('click.tracking');
2268 2331
2332 + // The anchor carries its own target: the browser follows it
2333 + prepareTrackedLink($link);
2334 +
2269 2335 // Add new click handler with namespace
2270 2336 $link.on('click.tracking', function(e) {
2271 - e.preventDefault();
2272 2337 e.stopPropagation();
2273 2338
2274 2339 const messageContext = typeof messageText === 'string'
2275 2340 ? messageText.substring(0, 200)
@@ -2274,30 +2339,10 @@
2274 2339 const messageContext = typeof messageText === 'string'
2275 2340 ? messageText.substring(0, 200)
2276 2341 : '';
2277 2342
2278 - // Track the click
2279 - $.ajax({
2280 - url: mxchatChat.ajax_url,
2281 - type: 'POST',
2282 - data: {
2283 - action: 'mxchat_track_url_click',
2284 - session_id: getChatSession(botId),
2285 - url: originalHref,
2286 - message_context: messageContext,
2287 - nonce: mxchatChat.nonce
2288 - },
2289 - complete: function() {
2290 - // Always redirect, even if tracking fails
2291 - if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
2292 - window.open(originalHref, '_blank');
2293 - } else {
2294 - window.location.href = originalHref;
2295 - }
2296 - }
2297 - });
2298 -
2299 - return false; // Extra insurance to prevent default
2343 + prepareTrackedLink($link);
2344 + trackLinkClick(originalHref, messageContext, botId);
2300 2345 });
2301 2346 }
2302 2347 });
2303 2348 }, 100); // Small delay to ensure DOM is ready
@@ -2592,9 +2637,17 @@
2592 2637 });
2593 2638
2594 2639 // Process standalone URLs - but NOT if they're already in <a> tags or brackets
2595 2640 // Updated pattern to be more careful about what it matches
2596 - const urlPattern = /(^|[^">=\[\]])(https?:\/\/[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim;
2641 + //
2642 + // The prefix group (plan 78618d) is the start of a line, an ordinary
2643 + // character, OR a tag this formatter itself opened or closed (paragraph,
2644 + // list item, table cell, heading, emphasis, line break), so an address
2645 + // that opens a paragraph or sits inside **bold** is linked too. Never
2646 + // an anchor (its text would be linked twice; the lookahead guards that
2647 + // as well) and never a code span, which stays as typed. No lookbehind,
2648 + // for Safari.
2649 + const urlPattern = /(^|[^">=\[\]]|<\/?(?:p|li|td|th|h[1-6]|strong|em|b|i|del|br)(?:\s[^<>]*)?\/?>)(https?:\/\/[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim;
2597 2650 processedText = processedText.replace(urlPattern, (match, prefix, url) => {
2598 2651 // Extra check: make sure this isn't already linked
2599 2652 if (match.includes('href=') || match.includes('</a>')) {
2600 2653 return match;
@@ -2605,10 +2658,14 @@
2605 2658 const safeUrl = safeEncodeUrl(cleanUrl);
2606 2659 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2607 2660 });
2608 2661
2609 - // Process www. URLs - but NOT if they're already in <a> tags or brackets
2610 - const wwwPattern = /(^|[^">/\[\]])(www\.[\S]+)(?![^<]*<\/a>)(?!\])/gim;
2662 + // Process www. URLs - but NOT if they're already in <a> tags or brackets.
2663 + // The address stops at the same characters the https pattern stops at
2664 + // (plan 78618d): it used to run through the closing paragraph tag, so
2665 + // "Try www.example.com/page." linked to www.example.com/page.%3C/p%3E.
2666 + // Same prefix group as the https pattern, same reasons.
2667 + const wwwPattern = /(^|[^">/\[\]]|<\/?(?:p|li|td|th|h[1-6]|strong|em|b|i|del|br)(?:\s[^<>]*)?\/?>)(www\.[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim;
2611 2668 processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
2612 2669 // Extra check: make sure this isn't already linked
2613 2670 if (match.includes('href=') || match.includes('</a>')) {
2614 2671 return match;
@@ -2672,13 +2729,19 @@
2672 2729 protectedSegments.push(match);
2673 2730 return placeholder;
2674 2731 });
2675 2732
2676 - // Step 1c: Also protect bare URLs that might exist
2733 + // Step 1c: Also protect bare URLs that might exist. Trailing emphasis
2734 + // markers are not part of the address (plan 78618d): **https://x.com**
2735 + // used to protect "https://x.com**", so the bold never paired and the
2736 + // asterisks ended up inside the link. The markers stay outside the
2737 + // placeholder so the styling rules below can pair them.
2677 2738 protectedText = protectedText.replace(/(https?:\/\/[^\s<>"]+)/gi, function(match) {
2739 + const markers = match.match(/[*_~]+$/);
2740 + const url = markers ? match.slice(0, match.length - markers[0].length) : match;
2678 2741 const placeholder = `__PROTECTED_${protectedSegments.length}__`;
2679 - protectedSegments.push(match);
2680 - return placeholder;
2742 + protectedSegments.push(url);
2743 + return placeholder + (markers ? markers[0] : '');
2681 2744 });
2682 2745
2683 2746 // Step 2: Now apply text styling to the protected text
2684 2747 // Handle bold text (**text**)
@@ -4545,14 +4608,8 @@
4545 4608 if (messageDiv.length > 0) {
4546 4609 const originalHref = $link.attr('href');
4547 4610
4548 4611 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
4549 - e.preventDefault();
4550 - e.stopPropagation();
4551 -
4552 - // Mark as tracked
4553 - $link.attr('data-tracked', 'true');
4554 -
4555 4612 // Get bot ID from the chat box context
4556 4613 var botId = getBotIdFromElement(this);
4557 4614
4558 4615 // Get message context from the message div
@@ -4557,28 +4614,12 @@
4557 4614
4558 4615 // Get message context from the message div
4559 4616 const messageText = messageDiv.text().substring(0, 200);
4560 4617
4561 - $.ajax({
4562 - url: mxchatChat.ajax_url,
4563 - type: 'POST',
4564 - data: {
4565 - action: 'mxchat_track_url_click',
4566 - session_id: getChatSession(botId),
4567 - url: originalHref,
4568 - message_context: messageText,
4569 - nonce: mxchatChat.nonce
4570 - },
4571 - complete: function() {
4572 - if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
4573 - window.open(originalHref, '_blank');
4574 - } else {
4575 - window.location.href = originalHref;
4576 - }
4577 - }
4578 - });
4579 -
4580 - return false;
4618 + // Same path as attachLinkTracking(): set the target, record the
4619 + // click, and let the browser follow the anchor.
4620 + prepareTrackedLink($link);
4621 + trackLinkClick(originalHref, messageText, botId);
4581 4622 }
4582 4623 }
4583 4624 });
4584 4625