PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / trunk
MxChat – AI Chatbot & Content Generation for WordPress vtrunk
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/mxchat_transcripts.js +148 -14 3.2.7trunk View file →
@@ -607,13 +607,19 @@
607 607 <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>
608 608 </div>
609 609 `;
610 610 } else {
611 - const ragLink = msg.has_rag ? `<a href="#" class="mxch-rag-link" data-message-id="${msg.id}">Sources</a>` : '';
611 + // Live-agent replies (role 'agent') get their own label and never
612 + // carry a Sources link; bot/assistant rows render exactly as before.
613 + const isAgent = !!msg.is_agent;
614 + const ragLink = (!isAgent && msg.has_rag) ? `<a href="#" class="mxch-rag-link" data-message-id="${msg.id}">Sources</a>` : '';
615 + const senderLabel = isAgent
616 + ? '<span class="mxch-agent-label">Live Agent</span>'
617 + : '<span class="mxch-bot-label">AI Assistant</span>';
612 618 messagesHtml += `
613 - <div class="mxch-message mxch-message-bot" data-message-id="${msg.id}">
619 + <div class="mxch-message mxch-message-bot${isAgent ? ' mxch-message-agent' : ''}" data-message-id="${msg.id}">
614 620 <div class="mxch-message-header">
615 - <span class="mxch-bot-label">AI Assistant</span>
621 + ${senderLabel}
616 622 ${ragLink}
617 623 </div>
618 624 <div class="mxch-message-row">
619 625 <div class="mxch-message-bubble">
@@ -937,9 +943,12 @@
937 943 renderActionsContext(response.data, $actionsContent);
938 944
939 945 // Update badge counts
940 946 const sourcesCount = response.data.top_matches ? response.data.top_matches.length : 0;
941 - 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;
942 951
943 952 if (sourcesCount > 0) {
944 953 $('#mxch-sources-count').text(sourcesCount).show();
945 954 }
@@ -975,21 +984,43 @@
975 984
976 985 function renderRagContext(data, $container) {
977 986 let html = '';
978 987
979 - // Check if we have any source data
980 - if (!data.top_matches || data.top_matches.length === 0) {
981 - 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>';
982 998 $container.html(html);
983 999 return;
984 1000 }
985 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 +
986 1008 html += '<div class="mxch-rag-summary">';
987 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>';
988 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>';
989 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 + }
990 1015 html += '</div>';
991 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 +
992 1023 const groupedByUrl = {};
993 1024
994 1025 data.top_matches.forEach(function(match) {
995 1026 const url = match.source_display || 'Unknown';
@@ -998,9 +1029,11 @@
998 1029 url: url,
999 1030 isUrl: url.startsWith('http'),
1000 1031 bestScore: 0,
1001 1032 usedForContext: false,
1002 - matchedChunks: []
1033 + matchedChunks: [],
1034 + bestFusedRank: Infinity,
1035 + viaSet: {}
1003 1036 };
1004 1037 }
1005 1038
1006 1039 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
@@ -1010,8 +1043,15 @@
1010 1043 if (match.used_for_context) {
1011 1044 groupedByUrl[url].usedForContext = true;
1012 1045 }
1013 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 +
1014 1054 groupedByUrl[url].matchedChunks.push({
1015 1055 chunkIndex: match.chunk_index,
1016 1056 score: match.similarity_percentage,
1017 1057 usedForContext: match.used_for_context
@@ -1017,9 +1057,16 @@
1017 1057 usedForContext: match.used_for_context
1018 1058 });
1019 1059 });
1020 1060
1021 - 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 + });
1022 1069 const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length;
1023 1070 const chunksInfo = data.total_chunks_used > 0 ? data.total_chunks_used + ' chunks sent to AI' : '';
1024 1071
1025 1072 html += '<div class="mxch-rag-matches">';
@@ -1034,8 +1081,17 @@
1034 1081 html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1035 1082 html += '<div class="mxch-rag-match-header">';
1036 1083 html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
1037 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 +
1038 1094 if (group.matchedChunks.length > 1) {
1039 1095 html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
1040 1096 }
1041 1097
@@ -1055,19 +1111,86 @@
1055 1111 html += '</div>';
1056 1112 $container.html(html);
1057 1113 }
1058 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 +
1059 1171 function renderActionsContext(data, $container) {
1060 1172 let html = '';
1061 1173
1062 - // Check if we have action analysis data
1063 - if (!data.action_analysis || data.action_analysis.length === 0) {
1064 - 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>';
1065 1181 $container.html(html);
1066 1182 return;
1067 1183 }
1068 1184
1069 - 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 + }
1070 1193 const triggeredAction = actions.find(a => a.triggered);
1071 1194 const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
1072 1195
1073 1196 // Summary section
@@ -1519,9 +1642,9 @@
1519 1642 function renderLeadsTable(rows) {
1520 1643 const $tbody = $('#mxch-leads-tbody');
1521 1644 if (!rows || rows.length === 0) {
1522 1645 $tbody.html(`
1523 - <tr><td colspan="6" class="mxch-leads-empty">
1646 + <tr><td colspan="7" class="mxch-leads-empty">
1524 1647 <div class="mxch-leads-empty-wrap">
1525 1648 <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>
1526 1649 <p>No leads match the current filters.</p>
1527 1650 </div>
@@ -1557,8 +1680,18 @@
1557 1680 const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : ''));
1558 1681 const pageCell = r.top_page_url
1559 1682 ? `<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>`
1560 1683 : '<span class="mxch-leads-muted">—</span>';
1684 + // Consent (b062c4): 'yes'/'no' are recorded decisions; '' means the
1685 + // capture predates the checkbox — "Not recorded", never "No".
1686 + let consentCell;
1687 + if (r.consent === 'yes' || r.consent === 'no') {
1688 + const consentTitle = (r.consent_at ? 'Recorded ' + r.consent_at : '') +
1689 + (r.consent_label ? (r.consent_at ? ' — ' : '') + '"' + r.consent_label + '"' : '');
1690 + 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>`;
1691 + } else {
1692 + consentCell = '<span class="mxch-leads-muted" title="Captured before the consent checkbox was enabled">Not recorded</span>';
1693 + }
1561 1694 // View Convo only for active leads (orphans and chat_deleted have no viewable session).
1562 1695 const viewBtn = (status === 'active' && r.latest_session_id)
1563 1696 ? `<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">
1564 1697 <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>
@@ -1576,8 +1709,9 @@
1576 1709 <td class="mxch-leads-col-lead">${leadCell}</td>
1577 1710 <td class="mxch-leads-col-count">${countCell}</td>
1578 1711 <td class="mxch-leads-col-last">${lastCell}</td>
1579 1712 <td class="mxch-leads-col-page">${pageCell}</td>
1713 + <td class="mxch-leads-col-consent">${consentCell}</td>
1580 1714 <td class="mxch-leads-col-actions">${viewBtn}${deleteBtn}</td>
1581 1715 </tr>
1582 1716 `;
1583 1717 });