PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.24
MxChat – AI Chatbot & Content Generation for WordPress v3.2.24
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/mxchat_transcripts.js +174 -19 3.2.15 → 3.2.24 View file →
@@ -943,9 +943,12 @@
943 943 renderActionsContext(response.data, $actionsContent);
944 944
945 945 // Update badge counts
946 946 const sourcesCount = response.data.top_matches ? response.data.top_matches.length : 0;
947 - const actionsCount = response.data.action_analysis ? response.data.action_analysis.length : 0;
947 + // 470f68: the Actions tab now carries two mechanisms — tools
948 + // that ran and trigger phrases that scored. Badge counts both.
949 + const toolCallsCount = response.data.tool_calls ? response.data.tool_calls.length : 0;
950 + const actionsCount = (response.data.action_analysis ? response.data.action_analysis.length : 0) + toolCallsCount;
948 951
949 952 if (sourcesCount > 0) {
950 953 $('#mxch-sources-count').text(sourcesCount).show();
951 954 }
@@ -981,21 +984,43 @@
981 984
982 985 function renderRagContext(data, $container) {
983 986 let html = '';
984 987
985 - // Check if we have any source data
986 - if (!data.top_matches || data.top_matches.length === 0) {
987 - html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>';
988 + // 67fc92: "the KB was searched and nothing matched" and "nothing was
989 + // recorded for this row" are different facts. Retrieval keys present
990 + // means the search ran and was recorded (top_matches may be empty);
991 + // no retrieval keys means there is simply no record (older rows, or
992 + // turns that never consulted the knowledge base).
993 + const retrievalRecorded = typeof data.total_documents_checked !== 'undefined'
994 + || (data.top_matches && data.top_matches.length > 0);
995 +
996 + if (!retrievalRecorded) {
997 + html += '<div class="mxch-no-results"><p>No retrieval data was recorded for this response.</p></div>';
988 998 $container.html(html);
989 999 return;
990 1000 }
991 1001
1002 + // Hybrid keyword boost (38ffa1): rows carry matched_via + fused_rank
1003 + // when hybrid retrieval was on for this response. Cosine % stays the
1004 + // anchor; the chip explains WHY a low-% row ranked high.
1005 + const topMatches = data.top_matches || [];
1006 + const hybridOn = topMatches.some(function(m) { return m.matched_via; });
1007 +
992 1008 html += '<div class="mxch-rag-summary">';
993 1009 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Knowledge Base:</span> <span class="mxch-rag-value">' + escapeHtml(data.knowledge_base_type || 'WordPress Database') + '</span></div>';
994 1010 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Similarity Threshold:</span> <span class="mxch-rag-value">' + Math.round((data.similarity_threshold || 0.35) * 100) + '%</span></div>';
995 1011 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Documents Checked:</span> <span class="mxch-rag-value">' + (data.total_documents_checked || 0) + '</span></div>';
1012 + if (hybridOn) {
1013 + html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Retrieval:</span> <span class="mxch-rag-value">Hybrid</span></div>';
1014 + }
996 1015 html += '</div>';
997 1016
1017 + if (topMatches.length === 0) {
1018 + html += '<div class="mxch-no-results"><p>The knowledge base was searched &mdash; no documents matched this response.</p></div>';
1019 + $container.html(html);
1020 + return;
1021 + }
1022 +
998 1023 const groupedByUrl = {};
999 1024
1000 1025 data.top_matches.forEach(function(match) {
1001 1026 const url = match.source_display || 'Unknown';
@@ -1004,9 +1029,11 @@
1004 1029 url: url,
1005 1030 isUrl: url.startsWith('http'),
1006 1031 bestScore: 0,
1007 1032 usedForContext: false,
1008 - matchedChunks: []
1033 + matchedChunks: [],
1034 + bestFusedRank: Infinity,
1035 + viaSet: {}
1009 1036 };
1010 1037 }
1011 1038
1012 1039 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
@@ -1016,8 +1043,15 @@
1016 1043 if (match.used_for_context) {
1017 1044 groupedByUrl[url].usedForContext = true;
1018 1045 }
1019 1046
1047 + if (match.fused_rank && match.fused_rank < groupedByUrl[url].bestFusedRank) {
1048 + groupedByUrl[url].bestFusedRank = match.fused_rank;
1049 + }
1050 + if (match.matched_via) {
1051 + groupedByUrl[url].viaSet[match.matched_via] = true;
1052 + }
1053 +
1020 1054 groupedByUrl[url].matchedChunks.push({
1021 1055 chunkIndex: match.chunk_index,
1022 1056 score: match.similarity_percentage,
1023 1057 usedForContext: match.used_for_context
@@ -1023,9 +1057,16 @@
1023 1057 usedForContext: match.used_for_context
1024 1058 });
1025 1059 });
1026 1060
1027 - const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore);
1061 + // Hybrid on: order by fused rank (rows without one sort last);
1062 + // otherwise by cosine, exactly as before.
1063 + const urlGroups = Object.values(groupedByUrl).sort(function(a, b) {
1064 + if (hybridOn && a.bestFusedRank !== b.bestFusedRank) {
1065 + return a.bestFusedRank - b.bestFusedRank;
1066 + }
1067 + return b.bestScore - a.bestScore;
1068 + });
1028 1069 const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length;
1029 1070 const chunksInfo = data.total_chunks_used > 0 ? data.total_chunks_used + ' chunks sent to AI' : '';
1030 1071
1031 1072 html += '<div class="mxch-rag-matches">';
@@ -1040,8 +1081,17 @@
1040 1081 html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1041 1082 html += '<div class="mxch-rag-match-header">';
1042 1083 html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
1043 1084
1085 + if (hybridOn) {
1086 + const vias = Object.keys(group.viaSet);
1087 + if (vias.length) {
1088 + const viaLabel = (vias.length > 1 || vias[0] === 'both') ? 'Both'
1089 + : (vias[0] === 'keyword' ? 'Keyword' : 'Vector');
1090 + html += '<span class="mxch-rag-via-chip mxch-rag-via-' + viaLabel.toLowerCase() + '">' + viaLabel + '</span>';
1091 + }
1092 + }
1093 +
1044 1094 if (group.matchedChunks.length > 1) {
1045 1095 html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
1046 1096 }
1047 1097
@@ -1061,19 +1111,86 @@
1061 1111 html += '</div>';
1062 1112 $container.html(html);
1063 1113 }
1064 1114
1115 + // AI Tools trace (470f68): one card per tool EXECUTION for this message.
1116 + // Rendered ABOVE the trigger-phrase scores — a tool that actually ran
1117 + // outranks a phrase that merely scored.
1118 + function renderToolCalls(toolCalls) {
1119 + const failed = toolCalls.filter(function(t) { return !t.ok; }).length;
1120 +
1121 + let html = '<div class="mxch-rag-summary">';
1122 + html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Tools Used:</span> <span class="mxch-rag-value">' + toolCalls.length + '</span></div>';
1123 + if (failed > 0) {
1124 + html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Failed:</span> <span class="mxch-rag-value" style="color: #ef4444; font-weight: 600;">' + failed + '</span></div>';
1125 + }
1126 + html += '</div>';
1127 +
1128 + html += '<div class="mxch-rag-matches">';
1129 + html += '<h3>AI Tools</h3>';
1130 + html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">Tools the assistant chose and ran for this message, in order.</p>';
1131 +
1132 + toolCalls.forEach(function(tool) {
1133 + const ok = !!tool.ok;
1134 + const cardClass = ok ? 'mxch-rag-match-used' : 'mxch-tool-failed';
1135 + const statusClass = ok ? 'status-used' : 'status-failed';
1136 + const statusIcon = ok ? '&#10003;' : '&#10007;';
1137 + const statusLabel = ok ? 'Ran' : 'Failed';
1138 +
1139 + html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1140 + html += '<div class="mxch-rag-match-header">';
1141 + html += '<span class="mxch-rag-match-score">&#9889;</span>';
1142 + if (typeof tool.ms === 'number') {
1143 + html += '<span class="mxch-tool-duration">' + tool.ms + ' ms</span>';
1144 + }
1145 + html += '<span class="mxch-rag-match-status ' + statusClass + '">' + statusIcon + ' ' + statusLabel + '</span>';
1146 + html += '</div>';
1147 +
1148 + html += '<div class="mxch-action-details">';
1149 + html += '<div class="mxch-action-label">' + escapeHtml(tool.label || tool.name) + '</div>';
1150 + html += '<div class="mxch-action-callback"><span class="mxch-action-callback-label">Tool:</span> ' + escapeHtml(tool.name) + '</div>';
1151 + html += '</div>';
1152 +
1153 + if (!ok && tool.error) {
1154 + html += '<div class="mxch-tool-error">' + escapeHtml(tool.error) + '</div>';
1155 + }
1156 +
1157 + if (tool.args_redacted === 'sensitive') {
1158 + html += '<div class="mxch-tool-redacted">Arguments not recorded &mdash; this tool is marked sensitive.</div>';
1159 + } else if (tool.args_excerpt) {
1160 + html += '<details class="mxch-tool-args"><summary>Arguments</summary>';
1161 + html += '<div class="mxch-tool-args-body">' + escapeHtml(tool.args_excerpt) + '</div></details>';
1162 + }
1163 +
1164 + html += '</div>';
1165 + });
1166 +
1167 + html += '</div>';
1168 + return html;
1169 + }
1170 +
1065 1171 function renderActionsContext(data, $container) {
1066 1172 let html = '';
1067 1173
1068 - // Check if we have action analysis data
1069 - if (!data.action_analysis || data.action_analysis.length === 0) {
1070 - html += '<div class="mxch-no-results"><p>No action analysis available for this message.</p><p style="color: var(--mxch-text-secondary); font-size: 13px; margin-top: 8px;">Actions are only evaluated when enabled in your bot configuration.</p></div>';
1174 + const toolCalls = (data.tool_calls && data.tool_calls.length) ? data.tool_calls : [];
1175 + const actions = (data.action_analysis && data.action_analysis.length) ? data.action_analysis : [];
1176 +
1177 + // 95d79d's empty state now shows only when NEITHER mechanism produced
1178 + // anything — the string itself is unchanged.
1179 + if (toolCalls.length === 0 && actions.length === 0) {
1180 + html += '<div class="mxch-no-results"><p>No trigger-phrase analysis for this message.</p><p style="color: var(--mxch-text-secondary); font-size: 13px; margin-top: 8px;">This panel shows how your <strong>Trigger Phrases</strong> scored &mdash; it stays empty if you have none enabled for this bot, or if the answer came from <strong>AI Tools</strong>, which don\'t produce similarity scores. It\'s recorded when the answer is generated, so it won\'t appear on older conversations.</p></div>';
1071 1181 $container.html(html);
1072 1182 return;
1073 1183 }
1074 1184
1075 - const actions = data.action_analysis;
1185 + if (toolCalls.length > 0) {
1186 + html += renderToolCalls(toolCalls);
1187 + }
1188 +
1189 + if (actions.length === 0) {
1190 + $container.html(html);
1191 + return;
1192 + }
1076 1193 const triggeredAction = actions.find(a => a.triggered);
1077 1194 const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
1078 1195
1079 1196 // Summary section
@@ -1521,13 +1638,31 @@
1521 1638 });
1522 1639 $wrap.html(html);
1523 1640 }
1524 1641
1642 + // A lead's key is its email address, or "session:<id>" when the capture
1643 + // had no address (c0cfaf). Email keys are lowercased exactly as before;
1644 + // session keys pass through untouched, because the server resolves them
1645 + // back to a real session id and case matters there. Both shapes travel on
1646 + // the same wire field the tab has always used, so the selection set, the
1647 + // delete call and the export call need no new parameter.
1648 + function leadKeyOf(r) {
1649 + const email = (r && r.email ? String(r.email) : '').trim();
1650 + if (email) return email.toLowerCase();
1651 + if (r && r.lead_key) return String(r.lead_key);
1652 + return 'session:' + (r && r.latest_session_id ? r.latest_session_id : '');
1653 + }
1654 +
1655 + function normalizeLeadKey(value) {
1656 + const key = (value === undefined || value === null) ? '' : String(value);
1657 + return key.indexOf('session:') === 0 ? key : key.toLowerCase();
1658 + }
1659 +
1525 1660 function renderLeadsTable(rows) {
1526 1661 const $tbody = $('#mxch-leads-tbody');
1527 1662 if (!rows || rows.length === 0) {
1528 1663 $tbody.html(`
1529 - <tr><td colspan="6" class="mxch-leads-empty">
1664 + <tr><td colspan="7" class="mxch-leads-empty">
1530 1665 <div class="mxch-leads-empty-wrap">
1531 1666 <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
1532 1667 <p>No leads match the current filters.</p>
1533 1668 </div>
@@ -1537,9 +1672,9 @@
1537 1672 }
1538 1673
1539 1674 let html = '';
1540 1675 rows.forEach(function(r) {
1541 - const emailKey = (r.email || '').toLowerCase();
1676 + const emailKey = leadKeyOf(r);
1542 1677 const isChecked = leadsState.selected.has(emailKey) ? ' checked' : '';
1543 1678 // Status: 'active' (has conversations), 'chat_deleted' (admin removed the chat), 'orphan' (no chat ever).
1544 1679 const status = r.status || (r.is_orphan ? 'orphan' : 'active');
1545 1680 const isOrphan = (status === 'orphan');
@@ -1546,12 +1681,21 @@
1546 1681 const isChatDeleted = (status === 'chat_deleted');
1547 1682 const nameLine = r.name
1548 1683 ? `<span class="mxch-leads-lead-name">${escapeHtmlLeads(r.name)}</span>`
1549 1684 : '';
1685 + // A lead captured with no email address (c0cfaf) still has a
1686 + // name; show that on the primary line rather than an empty cell,
1687 + // and say plainly that there is no address.
1688 + const primaryLine = r.email
1689 + ? `<span class="mxch-leads-lead-email" title="${escapeHtmlLeads(r.email)}">${escapeHtmlLeads(r.email)}</span>`
1690 + : `<span class="mxch-leads-lead-email">${escapeHtmlLeads(r.name || 'Unnamed lead')}</span>`;
1691 + const secondaryLine = r.email
1692 + ? nameLine
1693 + : '<span class="mxch-leads-muted">No email address</span>';
1550 1694 const leadCell = `
1551 1695 <div class="mxch-leads-lead-cell">
1552 - <span class="mxch-leads-lead-email" title="${escapeHtmlLeads(r.email)}">${escapeHtmlLeads(r.email)}</span>
1553 - ${nameLine}
1696 + ${primaryLine}
1697 + ${secondaryLine}
1554 1698 </div>`;
1555 1699 let countCell;
1556 1700 if (isOrphan) {
1557 1701 countCell = `<span class="mxch-leads-pill mxch-leads-pill-orphan">Orphan</span>`;
@@ -1563,8 +1707,18 @@
1563 1707 const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : ''));
1564 1708 const pageCell = r.top_page_url
1565 1709 ? `<a href="${escapeHtmlLeads(r.top_page_url)}" target="_blank" rel="noopener" class="mxch-leads-page-link" title="${escapeHtmlLeads(r.top_page_url)}">${escapeHtmlLeads(r.top_page_title || r.top_page_url)}</a>`
1566 1710 : '<span class="mxch-leads-muted">—</span>';
1711 + // Consent (b062c4): 'yes'/'no' are recorded decisions; '' means the
1712 + // capture predates the checkbox — "Not recorded", never "No".
1713 + let consentCell;
1714 + if (r.consent === 'yes' || r.consent === 'no') {
1715 + const consentTitle = (r.consent_at ? 'Recorded ' + r.consent_at : '') +
1716 + (r.consent_label ? (r.consent_at ? ' — ' : '') + '"' + r.consent_label + '"' : '');
1717 + consentCell = `<span class="mxch-leads-pill${r.consent === 'yes' ? ' mxch-leads-pill-consent-yes' : ' mxch-leads-pill-consent-no'}" title="${escapeHtmlLeads(consentTitle)}">${r.consent === 'yes' ? 'Yes' : 'No'}</span>`;
1718 + } else {
1719 + consentCell = '<span class="mxch-leads-muted" title="Captured before the consent checkbox was enabled">Not recorded</span>';
1720 + }
1567 1721 // View Convo only for active leads (orphans and chat_deleted have no viewable session).
1568 1722 const viewBtn = (status === 'active' && r.latest_session_id)
1569 1723 ? `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-leads-view" data-session-id="${escapeHtmlLeads(r.latest_session_id)}" title="View latest conversation">
1570 1724 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
@@ -1570,20 +1724,21 @@
1570 1724 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
1571 1725 <span>View convo</span>
1572 1726 </button>`
1573 1727 : '';
1574 - const deleteBtn = `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-btn-danger-ghost mxch-leads-delete-row" data-email="${escapeHtmlLeads(r.email)}" title="Delete lead">
1728 + const deleteBtn = `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-btn-danger-ghost mxch-leads-delete-row" data-email="${escapeHtmlLeads(emailKey)}" title="Delete lead">
1575 1729 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
1576 1730 </button>`;
1577 1731
1578 1732 const rowStateClass = isOrphan ? ' is-orphan' : (isChatDeleted ? ' is-chat-deleted' : '');
1579 1733 html += `
1580 - <tr class="mxch-leads-row${rowStateClass}" data-email="${escapeHtmlLeads(r.email)}">
1734 + <tr class="mxch-leads-row${rowStateClass}" data-email="${escapeHtmlLeads(emailKey)}">
1581 1735 <td class="mxch-leads-col-check"><input type="checkbox" class="mxch-leads-rowcheck"${isChecked}></td>
1582 1736 <td class="mxch-leads-col-lead">${leadCell}</td>
1583 1737 <td class="mxch-leads-col-count">${countCell}</td>
1584 1738 <td class="mxch-leads-col-last">${lastCell}</td>
1585 1739 <td class="mxch-leads-col-page">${pageCell}</td>
1740 + <td class="mxch-leads-col-consent">${consentCell}</td>
1586 1741 <td class="mxch-leads-col-actions">${viewBtn}${deleteBtn}</td>
1587 1742 </tr>
1588 1743 `;
1589 1744 });
@@ -1704,9 +1859,9 @@
1704 1859 $('#mxch-leads-select-all').on('change', function() {
1705 1860 const on = $(this).is(':checked');
1706 1861 $('.mxch-leads-rowcheck').prop('checked', on);
1707 1862 $('.mxch-leads-row').each(function() {
1708 - const email = ($(this).data('email') || '').toString().toLowerCase();
1863 + const email = normalizeLeadKey($(this).data('email'));
1709 1864 if (on) {
1710 1865 leadsState.selected.add(email);
1711 1866 } else {
1712 1867 leadsState.selected.delete(email);
@@ -1716,9 +1871,9 @@
1716 1871 });
1717 1872
1718 1873 // Row checkbox
1719 1874 $leads().on('change', '.mxch-leads-rowcheck', function() {
1720 - const email = ($(this).closest('.mxch-leads-row').data('email') || '').toString().toLowerCase();
1875 + const email = normalizeLeadKey($(this).closest('.mxch-leads-row').data('email'));
1721 1876 if ($(this).is(':checked')) {
1722 1877 leadsState.selected.add(email);
1723 1878 } else {
1724 1879 leadsState.selected.delete(email);
@@ -1784,9 +1939,9 @@
1784 1939 success: function(response) {
1785 1940 $btn.prop('disabled', false).text('Delete permanently');
1786 1941 closeLeadsConfirm();
1787 1942 if (response && response.success) {
1788 - emails.forEach(function(e) { leadsState.selected.delete(e.toLowerCase()); });
1943 + emails.forEach(function(e) { leadsState.selected.delete(normalizeLeadKey(e)); });
1789 1944 loadLeads(leadsState.page);
1790 1945 } else {
1791 1946 alert((response && response.data && response.data.message) || 'Failed to delete leads.');
1792 1947 }