PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.23
MxChat – AI Chatbot & Content Generation for WordPress v3.2.23
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 +64 -100 3.2.25 → 3.2.23 View file →
@@ -1306,26 +1306,11 @@
1306 1306 let testingDataReceived = false;
1307 1307 let streamingStarted = false;
1308 1308 // Server-pushed html to append as its OWN bot bubble once the stream
1309 1309 // finishes (e.g. the consent-safe YouTube embed, plan 03ba33). Rendering is
1310 - // deferred to the end of the stream so the embed always lands BELOW the
1311 - // streamed text.
1310 + // deferred to [DONE] so the embed always lands BELOW the streamed text.
1312 1311 let pendingAppendHtml = '';
1313 1312
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 -
1328 1313 // Abortable stream: a fresh controller per turn, keyed by bot instance.
1329 1314 // The Stop control (send button swapped in place) aborts both the read
1330 1315 // loop and the underlying request.
1331 1316 var streamControl = { controller: new AbortController(), aborted: false };
@@ -1406,10 +1391,8 @@
1406 1391
1407 1392 // Re-enable chat input when stream ends with content
1408 1393 enableChatInput(botId);
1409 1394
1410 - flushAppendHtml();
1411 -
1412 1395 // Scroll the user's last message to the top now that the
1413 1396 // bot's full reply has rendered (gives max reading room).
1414 1397 var $chatBoxDone = getElement(botId, 'chat-box');
1415 1398 var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
@@ -1440,9 +1423,16 @@
1440 1423
1441 1424 // Re-enable chat input after streaming completes
1442 1425 enableChatInput(botId);
1443 1426
1444 - flushAppendHtml();
1427 + // Render any server-pushed appendix html (e.g. the
1428 + // YouTube embed) as its own bot bubble below the
1429 + // streamed text — mirrors how it is saved in the
1430 + // transcript, so history replays identically.
1431 + if (pendingAppendHtml) {
1432 + appendMessage("bot", "", pendingAppendHtml, [], false, botId);
1433 + pendingAppendHtml = '';
1434 + }
1445 1435
1446 1436 // Scroll the user's last message to the top now
1447 1437 // that the bot's full reply has rendered.
1448 1438 var $chatBoxStreamDone = getElement(botId, 'chat-box');
@@ -2261,56 +2251,8 @@
2261 2251 // Error rendering message - silently continue
2262 2252 }
2263 2253 }
2264 2254
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 -
2313 2255 // Helper function to attach link tracking with proper event handling
2314 2256 function attachLinkTracking(messageDiv, messageText, botId) {
2315 2257 botId = botId || 'default';
2316 2258 // Use a slight delay to ensure DOM is ready
@@ -2328,13 +2270,11 @@
2328 2270 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
2329 2271 // Remove any existing click handlers first
2330 2272 $link.off('click.tracking');
2331 2273
2332 - // The anchor carries its own target: the browser follows it
2333 - prepareTrackedLink($link);
2334 -
2335 2274 // Add new click handler with namespace
2336 2275 $link.on('click.tracking', function(e) {
2276 + e.preventDefault();
2337 2277 e.stopPropagation();
2338 2278
2339 2279 const messageContext = typeof messageText === 'string'
2340 2280 ? messageText.substring(0, 200)
@@ -2339,10 +2279,30 @@
2339 2279 const messageContext = typeof messageText === 'string'
2340 2280 ? messageText.substring(0, 200)
2341 2281 : '';
2342 2282
2343 - prepareTrackedLink($link);
2344 - trackLinkClick(originalHref, messageContext, botId);
2283 + // Track the click
2284 + $.ajax({
2285 + url: mxchatChat.ajax_url,
2286 + type: 'POST',
2287 + data: {
2288 + action: 'mxchat_track_url_click',
2289 + session_id: getChatSession(botId),
2290 + url: originalHref,
2291 + message_context: messageContext,
2292 + nonce: mxchatChat.nonce
2293 + },
2294 + complete: function() {
2295 + // Always redirect, even if tracking fails
2296 + if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
2297 + window.open(originalHref, '_blank');
2298 + } else {
2299 + window.location.href = originalHref;
2300 + }
2301 + }
2302 + });
2303 +
2304 + return false; // Extra insurance to prevent default
2345 2305 });
2346 2306 }
2347 2307 });
2348 2308 }, 100); // Small delay to ensure DOM is ready
@@ -2637,17 +2597,9 @@
2637 2597 });
2638 2598
2639 2599 // Process standalone URLs - but NOT if they're already in <a> tags or brackets
2640 2600 // Updated pattern to be more careful about what it matches
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;
2601 + const urlPattern = /(^|[^">=\[\]])(https?:\/\/[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim;
2650 2602 processedText = processedText.replace(urlPattern, (match, prefix, url) => {
2651 2603 // Extra check: make sure this isn't already linked
2652 2604 if (match.includes('href=') || match.includes('</a>')) {
2653 2605 return match;
@@ -2658,14 +2610,10 @@
2658 2610 const safeUrl = safeEncodeUrl(cleanUrl);
2659 2611 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2660 2612 });
2661 2613
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;
2614 + // Process www. URLs - but NOT if they're already in <a> tags or brackets
2615 + const wwwPattern = /(^|[^">/\[\]])(www\.[\S]+)(?![^<]*<\/a>)(?!\])/gim;
2668 2616 processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
2669 2617 // Extra check: make sure this isn't already linked
2670 2618 if (match.includes('href=') || match.includes('</a>')) {
2671 2619 return match;
@@ -2729,19 +2677,13 @@
2729 2677 protectedSegments.push(match);
2730 2678 return placeholder;
2731 2679 });
2732 2680
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.
2681 + // Step 1c: Also protect bare URLs that might exist
2738 2682 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;
2741 2683 const placeholder = `__PROTECTED_${protectedSegments.length}__`;
2742 - protectedSegments.push(url);
2743 - return placeholder + (markers ? markers[0] : '');
2684 + protectedSegments.push(match);
2685 + return placeholder;
2744 2686 });
2745 2687
2746 2688 // Step 2: Now apply text styling to the protected text
2747 2689 // Handle bold text (**text**)
@@ -4608,8 +4550,14 @@
4608 4550 if (messageDiv.length > 0) {
4609 4551 const originalHref = $link.attr('href');
4610 4552
4611 4553 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
4554 + e.preventDefault();
4555 + e.stopPropagation();
4556 +
4557 + // Mark as tracked
4558 + $link.attr('data-tracked', 'true');
4559 +
4612 4560 // Get bot ID from the chat box context
4613 4561 var botId = getBotIdFromElement(this);
4614 4562
4615 4563 // Get message context from the message div
@@ -4614,12 +4562,28 @@
4614 4562
4615 4563 // Get message context from the message div
4616 4564 const messageText = messageDiv.text().substring(0, 200);
4617 4565
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);
4566 + $.ajax({
4567 + url: mxchatChat.ajax_url,
4568 + type: 'POST',
4569 + data: {
4570 + action: 'mxchat_track_url_click',
4571 + session_id: getChatSession(botId),
4572 + url: originalHref,
4573 + message_context: messageText,
4574 + nonce: mxchatChat.nonce
4575 + },
4576 + complete: function() {
4577 + if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
4578 + window.open(originalHref, '_blank');
4579 + } else {
4580 + window.location.href = originalHref;
4581 + }
4582 + }
4583 + });
4584 +
4585 + return false;
4622 4586 }
4623 4587 }
4624 4588 });
4625 4589