PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.8
MxChat – AI Chatbot & Content Generation for WordPress v3.0.8
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 +37 -736 3.2.213.0.8 View file →
@@ -178,11 +178,9 @@
178 178 $('.mxch-chat-checkbox').prop('checked', isChecked);
179 179
180 180 if (isChecked) {
181 181 $('.mxch-chat-item').each(function() {
182 - // Use .attr() — jQuery's .data() coerces "null"/"true"/numeric strings to JS types,
183 - // which causes fetch/delete of those sessions to silently fail.
184 - selectedSessions.add($(this).attr('data-session-id'));
182 + selectedSessions.add($(this).data('session-id'));
185 183 $(this).addClass('selected');
186 184 });
187 185 $('#mxch-chat-list').addClass('selection-mode');
188 186 } else {
@@ -216,104 +214,29 @@
216 214 $('#mxch-select-all').prop('checked', totalItems > 0 && checkedItems === totalItems);
217 215 $('#mxch-select-all').prop('indeterminate', checkedItems > 0 && checkedItems < totalItems);
218 216 }
219 217
220 - // Sort button — opens a small menu with 4 sort modes (plan-a5b006 adds rating sorts).
221 - $('#mxch-sort-btn').on('click', function(e) {
222 - e.stopPropagation();
223 - const $btn = $(this);
224 - let $menu = $('#mxch-sort-menu');
225 - if (!$menu.length) {
226 - $menu = $(
227 - '<div id="mxch-sort-menu" class="mxch-sort-menu" role="menu">' +
228 - ' <button type="button" class="mxch-sort-option" data-sort="desc" role="menuitem">Newest first</button>' +
229 - ' <button type="button" class="mxch-sort-option" data-sort="asc" role="menuitem">Oldest first</button>' +
230 - ' <button type="button" class="mxch-sort-option" data-sort="rating_positive" role="menuitem"><span class="mxch-sort-option-icon mxch-rating-thumb-up"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H7"/><path d="M3 10h4"/></svg></span>Positive ratings first</button>' +
231 - ' <button type="button" class="mxch-sort-option" data-sort="rating_negative" role="menuitem"><span class="mxch-sort-option-icon mxch-rating-thumb-down"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H17"/><path d="M21 14h-4"/></svg></span>Negative ratings first</button>' +
232 - '</div>'
233 - );
234 - $('body').append($menu);
235 - $menu.on('click', '.mxch-sort-option', function() {
236 - currentSortOrder = $(this).data('sort');
237 - $menu.find('.mxch-sort-option').removeClass('active');
238 - $(this).addClass('active');
239 - $menu.hide();
240 - loadChatList(1, $('#mxch-search-transcripts').val());
241 - });
242 - $(document).on('click.mxchSortMenu', function(ev) {
243 - if (!$(ev.target).closest('#mxch-sort-menu, #mxch-sort-btn').length) {
244 - $menu.hide();
245 - }
246 - });
247 - }
248 - $menu.find('.mxch-sort-option').removeClass('active');
249 - $menu.find('[data-sort="' + currentSortOrder + '"]').addClass('active');
250 - const offset = $btn.offset();
251 - const btnHeight = $btn.outerHeight();
252 - $menu.css({
253 - position: 'absolute',
254 - top: (offset.top + btnHeight + 4) + 'px',
255 - left: offset.left + 'px'
256 - }).toggle();
218 + // Sort button
219 + $('#mxch-sort-btn').on('click', function() {
220 + currentSortOrder = currentSortOrder === 'desc' ? 'asc' : 'desc';
221 + $(this).find('svg').css('transform', currentSortOrder === 'asc' ? 'rotate(180deg)' : 'rotate(0deg)');
222 + loadChatList(currentPage, $('#mxch-search-transcripts').val());
257 223 });
258 224
259 - // Render a rating badge for a session row. Uses inline SVG so the glyph
260 - // renders the same across OSes (emoji fonts vary). Tooltip surfaces the
261 - // optional feedback text.
262 - function renderRatingBadge(session) {
263 - const value = (session && typeof session.rating_value === 'number') ? session.rating_value : null;
264 - const feedback = (session && session.rating_feedback) ? session.rating_feedback : '';
265 - if (value === 1) {
266 - const title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat positively';
267 - return '<span class="mxch-chat-rating mxch-chat-rating-up" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H7"/><path d="M3 10h4"/></svg></span>';
268 - }
269 - if (value === -1) {
270 - const title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat negatively';
271 - return '<span class="mxch-chat-rating mxch-chat-rating-down" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H17"/><path d="M21 14h-4"/></svg></span>';
272 - }
273 - return '<span class="mxch-chat-rating mxch-chat-rating-empty" title="No rating yet" aria-label="No rating yet">—</span>';
274 - }
275 -
276 - // Delete selected button — opens the shared confirm modal with the "also delete lead" checkbox.
225 + // Delete selected button
277 226 $('#mxch-delete-selected').on('click', function() {
278 227 const count = selectedSessions.size;
279 228 if (count === 0) return;
280 - openTranscriptConfirm(Array.from(selectedSessions), count);
281 - });
282 229
283 - // Transcript delete confirm (shared by bulk + individual) ---------------------------
284 - let transcriptConfirmSessionIds = [];
230 + if (!confirm('Are you sure you want to delete ' + count + ' conversation(s)? This action cannot be undone.')) {
231 + return;
232 + }
285 233
286 - function openTranscriptConfirm(sessionIds, count) {
287 - transcriptConfirmSessionIds = sessionIds.slice();
288 - const n = count || sessionIds.length;
289 - $('#mxch-transcript-confirm-title').text(n === 1 ? 'Delete conversation?' : 'Delete ' + n + ' conversations?');
290 - $('#mxch-transcript-confirm-body').text(
291 - n === 1
292 - ? 'This removes the conversation and its messages.'
293 - : 'This removes ' + n + ' conversations and their messages.'
294 - );
295 - $('#mxch-transcript-also-delete-lead').prop('checked', false);
296 - $('#mxch-transcript-confirm').fadeIn(120);
297 - }
298 -
299 - function closeTranscriptConfirm() {
300 - $('#mxch-transcript-confirm').fadeOut(120);
301 - transcriptConfirmSessionIds = [];
302 - }
303 -
304 - $(document).on('click', '[data-mxch-transcript-close]', closeTranscriptConfirm);
305 -
306 - $('#mxch-transcript-confirm-go').on('click', function() {
307 - const ids = transcriptConfirmSessionIds.slice();
308 - if (!ids.length) { closeTranscriptConfirm(); return; }
309 - const alsoDeleteLead = $('#mxch-transcript-also-delete-lead').is(':checked');
310 - closeTranscriptConfirm();
311 - deleteMultipleSessions(ids, alsoDeleteLead);
234 + deleteMultipleSessions(Array.from(selectedSessions));
312 235 });
313 236
314 237 // Delete multiple sessions
315 - function deleteMultipleSessions(sessionIds, alsoDeleteLead) {
238 + function deleteMultipleSessions(sessionIds) {
316 239 $.ajax({
317 240 url: ajaxurl,
318 241 type: 'POST',
319 242 data: {
@@ -318,9 +241,8 @@
318 241 type: 'POST',
319 242 data: {
320 243 action: 'mxchat_delete_chat_history',
321 244 delete_session_ids: sessionIds,
322 - also_delete_lead: alsoDeleteLead ? '1' : '0',
323 245 security: $('#mxchat_delete_chat_nonce').val()
324 246 },
325 247 success: function(response) {
326 248 try {
@@ -341,13 +263,8 @@
341 263 }
342 264
343 265 // Reload list
344 266 loadChatList(currentPage, $('#mxch-search-transcripts').val());
345 -
346 - // The Leads tab shares this data (Chat deleted pill, nav badge,
347 - // stats) — invalidate it so switching tabs re-fetches instead of
348 - // showing stale "active lead" rows.
349 - invalidateLeadsData();
350 267 } else if (jsonResponse.error) {
351 268 alert('Error: ' + jsonResponse.error);
352 269 }
353 270 } catch (e) {
@@ -415,9 +332,8 @@
415 332 <div class="mxch-chat-name">${escapeHtml(session.display_name)}</div>
416 333 <div class="mxch-chat-preview">${escapeHtml(session.preview)}</div>
417 334 </div>
418 335 <div class="mxch-chat-meta">
419 - ${renderRatingBadge(session)}
420 336 <span class="mxch-chat-time">${escapeHtml(session.time_display)}</span>
421 337 <span class="mxch-chat-count">${session.message_count}</span>
422 338 </div>
423 339 </div>
@@ -429,9 +345,9 @@
429 345 // Attach checkbox handlers
430 346 $('.mxch-chat-checkbox').on('click', function(e) {
431 347 e.stopPropagation(); // Prevent triggering chat item click
432 348 const $item = $(this).closest('.mxch-chat-item');
433 - const sessionId = $item.attr('data-session-id');
349 + const sessionId = $item.data('session-id');
434 350
435 351 if ($(this).is(':checked')) {
436 352 selectedSessions.add(sessionId);
437 353 $item.addClass('selected');
@@ -447,9 +363,9 @@
447 363 $('.mxch-chat-item').on('click', function(e) {
448 364 // Don't trigger if clicking on checkbox
449 365 if ($(e.target).is('.mxch-chat-checkbox')) return;
450 366
451 - const sessionId = $(this).attr('data-session-id');
367 + const sessionId = $(this).data('session-id');
452 368 selectChat(sessionId);
453 369
454 370 // Update active state
455 371 $('.mxch-chat-item').removeClass('active');
@@ -570,18 +486,8 @@
570 486 } else {
571 487 $('#mxch-detail-email-row').hide();
572 488 }
573 489
574 - // Feedback row — .text() (not .html()) for XSS-safe display of user-submitted text.
575 - // rating_feedback is already sanitize_text_field()'d + mb_substr(200) on save.
576 - var feedback = (data && data.rating_feedback) ? String(data.rating_feedback).trim() : '';
577 - if (feedback) {
578 - $('#mxch-detail-feedback').text(feedback);
579 - $('#mxch-detail-feedback-row').show();
580 - } else {
581 - $('#mxch-detail-feedback-row').hide();
582 - }
583 -
584 490 // Clicked links
585 491 if (data.clicked_urls && data.clicked_urls.length > 0) {
586 492 let linksHtml = '';
587 493 data.clicked_urls.forEach(function(url) {
@@ -607,19 +513,13 @@
607 513 <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>
608 514 </div>
609 515 `;
610 516 } else {
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>';
517 + const ragLink = msg.has_rag ? `<a href="#" class="mxch-rag-link" data-message-id="${msg.id}">Sources</a>` : '';
618 518 messagesHtml += `
619 - <div class="mxch-message mxch-message-bot${isAgent ? ' mxch-message-agent' : ''}" data-message-id="${msg.id}">
519 + <div class="mxch-message mxch-message-bot" data-message-id="${msg.id}">
620 520 <div class="mxch-message-header">
621 - ${senderLabel}
521 + <span class="mxch-bot-label">AI Assistant</span>
622 522 ${ragLink}
623 523 </div>
624 524 <div class="mxch-message-row">
625 525 <div class="mxch-message-bubble">
@@ -661,16 +561,21 @@
661 561 $btn.addClass('active');
662 562 }
663 563 });
664 564
665 - // Delete current chat — opens the shared confirm modal.
565 + // Delete current chat
666 566 $('#mxch-delete-current').on('click', function() {
667 567 if (!currentSessionId) return;
668 - openTranscriptConfirm([currentSessionId], 1);
568 +
569 + if (!confirm('Are you sure you want to delete this conversation? This action cannot be undone.')) {
570 + return;
571 + }
572 +
573 + deleteSession(currentSessionId);
669 574 });
670 575
671 576 // Delete session function
672 - function deleteSession(sessionId, alsoDeleteLead) {
577 + function deleteSession(sessionId) {
673 578 $.ajax({
674 579 url: ajaxurl,
675 580 type: 'POST',
676 581 data: {
@@ -675,9 +580,8 @@
675 580 type: 'POST',
676 581 data: {
677 582 action: 'mxchat_delete_chat_history',
678 583 delete_session_ids: [sessionId],
679 - also_delete_lead: alsoDeleteLead ? '1' : '0',
680 584 security: $('#mxchat_delete_chat_nonce').val()
681 585 },
682 586 success: function(response) {
683 587 try {
@@ -943,12 +847,9 @@
943 847 renderActionsContext(response.data, $actionsContent);
944 848
945 849 // Update badge counts
946 850 const sourcesCount = response.data.top_matches ? response.data.top_matches.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;
851 + const actionsCount = response.data.action_analysis ? response.data.action_analysis.length : 0;
951 852
952 853 if (sourcesCount > 0) {
953 854 $('#mxch-sources-count').text(sourcesCount).show();
954 855 }
@@ -984,43 +885,21 @@
984 885
985 886 function renderRagContext(data, $container) {
986 887 let html = '';
987 888
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>';
889 + // Check if we have any source data
890 + if (!data.top_matches || data.top_matches.length === 0) {
891 + html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>';
998 892 $container.html(html);
999 893 return;
1000 894 }
1001 895
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 -
1008 896 html += '<div class="mxch-rag-summary">';
1009 897 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>';
1010 898 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>';
1011 899 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 - }
1015 900 html += '</div>';
1016 901
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 -
1023 902 const groupedByUrl = {};
1024 903
1025 904 data.top_matches.forEach(function(match) {
1026 905 const url = match.source_display || 'Unknown';
@@ -1029,11 +908,9 @@
1029 908 url: url,
1030 909 isUrl: url.startsWith('http'),
1031 910 bestScore: 0,
1032 911 usedForContext: false,
1033 - matchedChunks: [],
1034 - bestFusedRank: Infinity,
1035 - viaSet: {}
912 + matchedChunks: []
1036 913 };
1037 914 }
1038 915
1039 916 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
@@ -1043,15 +920,8 @@
1043 920 if (match.used_for_context) {
1044 921 groupedByUrl[url].usedForContext = true;
1045 922 }
1046 923
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 -
1054 924 groupedByUrl[url].matchedChunks.push({
1055 925 chunkIndex: match.chunk_index,
1056 926 score: match.similarity_percentage,
1057 927 usedForContext: match.used_for_context
@@ -1057,22 +927,14 @@
1057 927 usedForContext: match.used_for_context
1058 928 });
1059 929 });
1060 930
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 - });
1069 - const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length;
1070 - const chunksInfo = data.total_chunks_used > 0 ? data.total_chunks_used + ' chunks sent to AI' : '';
931 + const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore);
932 + const usedUrlCount = urlGroups.filter(g => g.usedForContext).length;
1071 933
1072 934 html += '<div class="mxch-rag-matches">';
1073 935 html += '<h3>Retrieved Documents</h3>';
1074 - html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">' + usedUrlCount + ' source' + (usedUrlCount === 1 ? '' : 's') + ' used for response' + (chunksInfo ? ' &middot; ' + chunksInfo : '') + '</p>';
936 + html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">' + usedUrlCount + ' entr' + (usedUrlCount === 1 ? 'y' : 'ies') + ' used for response</p>';
1075 937
1076 938 urlGroups.forEach(function(group) {
1077 939 const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
1078 940 const statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
@@ -1081,17 +943,8 @@
1081 943 html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1082 944 html += '<div class="mxch-rag-match-header">';
1083 945 html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
1084 946
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 -
1094 947 if (group.matchedChunks.length > 1) {
1095 948 html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
1096 949 }
1097 950
@@ -1111,86 +964,19 @@
1111 964 html += '</div>';
1112 965 $container.html(html);
1113 966 }
1114 967
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 -
1171 968 function renderActionsContext(data, $container) {
1172 969 let html = '';
1173 970
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>';
971 + // Check if we have action analysis data
972 + if (!data.action_analysis || data.action_analysis.length === 0) {
973 + 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>';
1181 974 $container.html(html);
1182 975 return;
1183 976 }
1184 977
1185 - if (toolCalls.length > 0) {
1186 - html += renderToolCalls(toolCalls);
1187 - }
1188 -
1189 - if (actions.length === 0) {
1190 - $container.html(html);
1191 - return;
1192 - }
978 + const actions = data.action_analysis;
1193 979 const triggeredAction = actions.find(a => a.triggered);
1194 980 const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
1195 981
1196 982 // Summary section
@@ -1487,491 +1273,6 @@
1487 1273 clearTimeout(resizeTimeout);
1488 1274 resizeTimeout = setTimeout(function() {
1489 1275 initActivityChart();
1490 1276 }, 250);
1491 - });
1492 -
1493 - // ==========================================================================
1494 - // Leads Tab
1495 - // ==========================================================================
1496 -
1497 - const leadsState = {
1498 - loaded: false,
1499 - page: 1,
1500 - perPage: 25,
1501 - totalPages: 1,
1502 - totalCount: 0,
1503 - selected: new Set(),
1504 - filters: {
1505 - search: '',
1506 - dateRange: 'all',
1507 - status: 'all',
1508 - pageUrl: '',
1509 - pageTitle: ''
1510 - },
1511 - pendingDelete: [],
1512 - leadsRows: [] // last-rendered rows for quick lookup
1513 - };
1514 -
1515 - function $leads() { return $('#leads'); }
1516 -
1517 - // Called after a transcript delete from the All Chats side. Marks the Leads tab
1518 - // data stale so the next tab visit re-fetches, and refreshes immediately if the
1519 - // Leads tab happens to already be visible.
1520 - function invalidateLeadsData() {
1521 - leadsState.loaded = false;
1522 - if ($('#leads').hasClass('active')) {
1523 - loadLeads(1);
1524 - }
1525 - }
1526 -
1527 - function escapeHtmlLeads(s) {
1528 - if (s === null || typeof s === 'undefined') return '';
1529 - return String(s)
1530 - .replace(/&/g, '&amp;')
1531 - .replace(/</g, '&lt;')
1532 - .replace(/>/g, '&gt;')
1533 - .replace(/"/g, '&quot;')
1534 - .replace(/'/g, '&#039;');
1535 - }
1536 -
1537 - function leadsFiltersActive() {
1538 - const f = leadsState.filters;
1539 - return f.search !== '' || f.dateRange !== 'all' || f.status !== 'all' || f.pageUrl !== '';
1540 - }
1541 -
1542 - function updateClearFiltersButton() {
1543 - if (leadsFiltersActive()) {
1544 - $('#mxch-leads-clear-filters').show();
1545 - } else {
1546 - $('#mxch-leads-clear-filters').hide();
1547 - }
1548 - }
1549 -
1550 - function setPageFilterChip(url, title) {
1551 - leadsState.filters.pageUrl = url || '';
1552 - leadsState.filters.pageTitle = title || url || '';
1553 - const $chip = $('#mxch-leads-active-page-filter');
1554 - if (url) {
1555 - $chip.find('.mxch-leads-page-chip-label').text('Page: ' + (title || url));
1556 - $chip.show();
1557 - } else {
1558 - $chip.hide();
1559 - }
1560 - updateClearFiltersButton();
1561 - }
1562 -
1563 - function loadLeads(page) {
1564 - if (typeof page === 'number') leadsState.page = page;
1565 -
1566 - const $tbody = $('#mxch-leads-tbody');
1567 - $tbody.html('<tr><td colspan="6" class="mxch-leads-loading"><span class="spinner is-active"></span></td></tr>');
1568 -
1569 - $.ajax({
1570 - url: ajaxurl,
1571 - type: 'POST',
1572 - data: {
1573 - action: 'mxchat_fetch_leads',
1574 - page: leadsState.page,
1575 - per_page: leadsState.perPage,
1576 - search: leadsState.filters.search,
1577 - date_range: leadsState.filters.dateRange,
1578 - status: leadsState.filters.status,
1579 - page_url: leadsState.filters.pageUrl
1580 - },
1581 - success: function(response) {
1582 - leadsState.loaded = true;
1583 - if (!response || !response.success) {
1584 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1585 - return;
1586 - }
1587 - leadsState.totalPages = response.total_pages || 1;
1588 - leadsState.totalCount = response.total_count || 0;
1589 - leadsState.leadsRows = response.leads || [];
1590 -
1591 - renderLeadsStats(response.stats || {});
1592 - renderLeadsTopPages(response.top_pages || []);
1593 - renderLeadsTable(response.leads || []);
1594 - renderLeadsCount(response.showing_start, response.showing_end, response.total_count);
1595 - renderLeadsPagination(response.page, response.total_pages);
1596 -
1597 - // Nav badge
1598 - if (response.stats && typeof response.stats.total_leads === 'number') {
1599 - const $badge = $('#mxch-leads-nav-badge');
1600 - if (response.stats.total_leads > 0) {
1601 - $badge.text(response.stats.total_leads).show();
1602 - } else {
1603 - $badge.hide();
1604 - }
1605 - }
1606 - },
1607 - error: function() {
1608 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1609 - }
1610 - });
1611 - }
1612 -
1613 - function renderLeadsStats(stats) {
1614 - $('#mxch-leads-stat-total').text(stats.total_leads || 0);
1615 - $('#mxch-leads-stat-new').text(stats.new_this_week || 0);
1616 - $('#mxch-leads-stat-avg').text(stats.avg_convos || 0);
1617 - const pct = stats.orphan_pct || 0;
1618 - $('#mxch-leads-stat-orphan').text(pct + '%');
1619 - const orphanCount = stats.orphan_count || 0;
1620 - $('#mxch-leads-stat-orphan-sub').text(orphanCount + (orphanCount === 1 ? ' lead captured but never chatted' : ' leads captured but never chatted'));
1621 - }
1622 -
1623 - function renderLeadsTopPages(pages) {
1624 - const $wrap = $('#mxch-leads-toppages-list');
1625 - if (!pages || pages.length === 0) {
1626 - $wrap.html('<div class="mxch-leads-empty-mini">No page data yet.</div>');
1627 - return;
1628 - }
1629 - let html = '';
1630 - pages.forEach(function(p) {
1631 - const isActive = leadsState.filters.pageUrl === p.url ? ' is-active' : '';
1632 - html += `
1633 - <button type="button" class="mxch-leads-toppage-row${isActive}" data-url="${escapeHtmlLeads(p.url)}" data-title="${escapeHtmlLeads(p.title)}">
1634 - <span class="mxch-leads-toppage-title">${escapeHtmlLeads(p.title || p.url)}</span>
1635 - <span class="mxch-leads-toppage-count">${p.lead_count}</span>
1636 - </button>
1637 - `;
1638 - });
1639 - $wrap.html(html);
1640 - }
1641 -
1642 - function renderLeadsTable(rows) {
1643 - const $tbody = $('#mxch-leads-tbody');
1644 - if (!rows || rows.length === 0) {
1645 - $tbody.html(`
1646 - <tr><td colspan="7" class="mxch-leads-empty">
1647 - <div class="mxch-leads-empty-wrap">
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>
1649 - <p>No leads match the current filters.</p>
1650 - </div>
1651 - </td></tr>
1652 - `);
1653 - return;
1654 - }
1655 -
1656 - let html = '';
1657 - rows.forEach(function(r) {
1658 - const emailKey = (r.email || '').toLowerCase();
1659 - const isChecked = leadsState.selected.has(emailKey) ? ' checked' : '';
1660 - // Status: 'active' (has conversations), 'chat_deleted' (admin removed the chat), 'orphan' (no chat ever).
1661 - const status = r.status || (r.is_orphan ? 'orphan' : 'active');
1662 - const isOrphan = (status === 'orphan');
1663 - const isChatDeleted = (status === 'chat_deleted');
1664 - const nameLine = r.name
1665 - ? `<span class="mxch-leads-lead-name">${escapeHtmlLeads(r.name)}</span>`
1666 - : '';
1667 - const leadCell = `
1668 - <div class="mxch-leads-lead-cell">
1669 - <span class="mxch-leads-lead-email" title="${escapeHtmlLeads(r.email)}">${escapeHtmlLeads(r.email)}</span>
1670 - ${nameLine}
1671 - </div>`;
1672 - let countCell;
1673 - if (isOrphan) {
1674 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-orphan">Orphan</span>`;
1675 - } else if (isChatDeleted) {
1676 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-deleted" title="Chat was deleted by an admin">Chat deleted</span>`;
1677 - } else {
1678 - countCell = `<span class="mxch-leads-pill">${r.conversation_count}</span>`;
1679 - }
1680 - const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : ''));
1681 - const pageCell = r.top_page_url
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>`
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 - }
1694 - // View Convo only for active leads (orphans and chat_deleted have no viewable session).
1695 - const viewBtn = (status === 'active' && r.latest_session_id)
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">
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>
1698 - <span>View convo</span>
1699 - </button>`
1700 - : '';
1701 - 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">
1702 - <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>
1703 - </button>`;
1704 -
1705 - const rowStateClass = isOrphan ? ' is-orphan' : (isChatDeleted ? ' is-chat-deleted' : '');
1706 - html += `
1707 - <tr class="mxch-leads-row${rowStateClass}" data-email="${escapeHtmlLeads(r.email)}">
1708 - <td class="mxch-leads-col-check"><input type="checkbox" class="mxch-leads-rowcheck"${isChecked}></td>
1709 - <td class="mxch-leads-col-lead">${leadCell}</td>
1710 - <td class="mxch-leads-col-count">${countCell}</td>
1711 - <td class="mxch-leads-col-last">${lastCell}</td>
1712 - <td class="mxch-leads-col-page">${pageCell}</td>
1713 - <td class="mxch-leads-col-consent">${consentCell}</td>
1714 - <td class="mxch-leads-col-actions">${viewBtn}${deleteBtn}</td>
1715 - </tr>
1716 - `;
1717 - });
1718 -
1719 - $tbody.html(html);
1720 - updateLeadsSelectionUI();
1721 - }
1722 -
1723 - function renderLeadsCount(start, end, total) {
1724 - if (!total) {
1725 - $('#mxch-leads-count').text('0 leads');
1726 - } else {
1727 - $('#mxch-leads-count').text(start + '-' + end + ' / ' + total + ' leads');
1728 - }
1729 - }
1730 -
1731 - function renderLeadsPagination(currentPage, totalPages) {
1732 - const $c = $('#mxch-leads-pagination');
1733 - if (!totalPages || totalPages <= 1) { $c.html(''); return; }
1734 - let html = '<div class="mxch-pagination-btns">';
1735 - if (currentPage > 1) {
1736 - html += `<button class="mxch-page-btn" data-page="${currentPage - 1}">&laquo;</button>`;
1737 - }
1738 - html += `<span class="mxch-page-info">${currentPage} / ${totalPages}</span>`;
1739 - if (currentPage < totalPages) {
1740 - html += `<button class="mxch-page-btn" data-page="${currentPage + 1}">&raquo;</button>`;
1741 - }
1742 - html += '</div>';
1743 - $c.html(html);
1744 - }
1745 -
1746 - function updateLeadsSelectionUI() {
1747 - const count = leadsState.selected.size;
1748 - const $countEl = $('#mxch-leads-selected-count');
1749 - const $del = $('#mxch-leads-delete-selected');
1750 - if (count > 0) {
1751 - $countEl.text(count + ' selected').addClass('has-selection');
1752 - $del.prop('disabled', false);
1753 - } else {
1754 - $countEl.text('0').removeClass('has-selection');
1755 - $del.prop('disabled', true);
1756 - }
1757 - // Selected-scope export menu items
1758 - $('#mxch-leads-export-menu button[data-scope="selected"]').prop('disabled', count === 0);
1759 -
1760 - // Select-all checkbox state
1761 - const $checks = $('.mxch-leads-rowcheck');
1762 - const checked = $checks.filter(':checked').length;
1763 - const total = $checks.length;
1764 - $('#mxch-leads-select-all').prop('checked', total > 0 && checked === total);
1765 - $('#mxch-leads-select-all').prop('indeterminate', checked > 0 && checked < total);
1766 - }
1767 -
1768 - // Trigger leads load when switching to the tab (works alongside the main nav handler above).
1769 - $('.mxch-nav-link[data-target="leads"], .mxch-mobile-nav-link[data-target="leads"]').on('click', function() {
1770 - if (!leadsState.loaded) {
1771 - loadLeads(1);
1772 - }
1773 - });
1774 -
1775 - // Filter: search (debounced)
1776 - let leadsSearchTimer;
1777 - $('#mxch-leads-search').on('input', function() {
1778 - clearTimeout(leadsSearchTimer);
1779 - const val = $(this).val();
1780 - leadsSearchTimer = setTimeout(function() {
1781 - leadsState.filters.search = (val || '').trim();
1782 - updateClearFiltersButton();
1783 - loadLeads(1);
1784 - }, 300);
1785 - });
1786 -
1787 - // Filter: date range
1788 - $('#mxch-leads-date-range').on('change', function() {
1789 - leadsState.filters.dateRange = $(this).val();
1790 - updateClearFiltersButton();
1791 - loadLeads(1);
1792 - });
1793 -
1794 - // Filter: status
1795 - $('#mxch-leads-status').on('change', function() {
1796 - leadsState.filters.status = $(this).val();
1797 - updateClearFiltersButton();
1798 - loadLeads(1);
1799 - });
1800 -
1801 - // Clear filters
1802 - $('#mxch-leads-clear-filters').on('click', function() {
1803 - leadsState.filters = { search: '', dateRange: 'all', status: 'all', pageUrl: '', pageTitle: '' };
1804 - $('#mxch-leads-search').val('');
1805 - $('#mxch-leads-date-range').val('all');
1806 - $('#mxch-leads-status').val('all');
1807 - setPageFilterChip('', '');
1808 - loadLeads(1);
1809 - });
1810 -
1811 - // Remove page chip
1812 - $leads().on('click', '.mxch-leads-page-chip-remove', function() {
1813 - setPageFilterChip('', '');
1814 - loadLeads(1);
1815 - });
1816 -
1817 - // Top Pages click -> set filter
1818 - $leads().on('click', '.mxch-leads-toppage-row', function() {
1819 - const url = $(this).data('url') || '';
1820 - const title = $(this).data('title') || '';
1821 - setPageFilterChip(url, title);
1822 - loadLeads(1);
1823 - });
1824 -
1825 - // Pagination click
1826 - $leads().on('click', '#mxch-leads-pagination .mxch-page-btn', function() {
1827 - const p = parseInt($(this).data('page'), 10);
1828 - if (p > 0) loadLeads(p);
1829 - });
1830 -
1831 - // Select-all
1832 - $('#mxch-leads-select-all').on('change', function() {
1833 - const on = $(this).is(':checked');
1834 - $('.mxch-leads-rowcheck').prop('checked', on);
1835 - $('.mxch-leads-row').each(function() {
1836 - const email = ($(this).data('email') || '').toString().toLowerCase();
1837 - if (on) {
1838 - leadsState.selected.add(email);
1839 - } else {
1840 - leadsState.selected.delete(email);
1841 - }
1842 - });
1843 - updateLeadsSelectionUI();
1844 - });
1845 -
1846 - // Row checkbox
1847 - $leads().on('change', '.mxch-leads-rowcheck', function() {
1848 - const email = ($(this).closest('.mxch-leads-row').data('email') || '').toString().toLowerCase();
1849 - if ($(this).is(':checked')) {
1850 - leadsState.selected.add(email);
1851 - } else {
1852 - leadsState.selected.delete(email);
1853 - }
1854 - updateLeadsSelectionUI();
1855 - });
1856 -
1857 - // View convo -> jump to All Chats tab and open the session
1858 - $leads().on('click', '.mxch-leads-view', function() {
1859 - const sid = $(this).attr('data-session-id');
1860 - if (!sid) return;
1861 - $('.mxch-nav-link[data-target="all-chats"]').trigger('click');
1862 - // selectChat is defined earlier in this closure
1863 - if (typeof selectChat === 'function') {
1864 - setTimeout(function() { selectChat(sid); }, 30);
1865 - }
1866 - });
1867 -
1868 - // Row delete -> confirm for one
1869 - $leads().on('click', '.mxch-leads-delete-row', function() {
1870 - const email = $(this).data('email');
1871 - if (!email) return;
1872 - openLeadsConfirm([String(email)]);
1873 - });
1874 -
1875 - // Bulk delete -> confirm for N
1876 - $('#mxch-leads-delete-selected').on('click', function() {
1877 - if (leadsState.selected.size === 0) return;
1878 - openLeadsConfirm(Array.from(leadsState.selected));
1879 - });
1880 -
1881 - function openLeadsConfirm(emails) {
1882 - leadsState.pendingDelete = emails;
1883 - const count = emails.length;
1884 - const msg = count === 1
1885 - ? 'Delete lead "' + emails[0] + '" and all of their conversations?'
1886 - : 'Delete ' + count + ' leads and all of their conversations?';
1887 - $('#mxch-leads-confirm-body').text(msg);
1888 - $('#mxch-leads-confirm').fadeIn(120);
1889 - }
1890 -
1891 - function closeLeadsConfirm() {
1892 - $('#mxch-leads-confirm').fadeOut(120);
1893 - leadsState.pendingDelete = [];
1894 - }
1895 -
1896 - $leads().on('click', '[data-mxch-leads-close]', closeLeadsConfirm);
1897 -
1898 - $('#mxch-leads-confirm-go').on('click', function() {
1899 - const emails = leadsState.pendingDelete.slice();
1900 - if (!emails.length) { closeLeadsConfirm(); return; }
1901 -
1902 - const $btn = $(this).prop('disabled', true).text('Deleting...');
1903 -
1904 - $.ajax({
1905 - url: ajaxurl,
1906 - type: 'POST',
1907 - data: {
1908 - action: 'mxchat_delete_leads',
1909 - security: $('#mxchat_leads_delete_nonce').val(),
1910 - emails: emails
1911 - },
1912 - success: function(response) {
1913 - $btn.prop('disabled', false).text('Delete permanently');
1914 - closeLeadsConfirm();
1915 - if (response && response.success) {
1916 - emails.forEach(function(e) { leadsState.selected.delete(e.toLowerCase()); });
1917 - loadLeads(leadsState.page);
1918 - } else {
1919 - alert((response && response.data && response.data.message) || 'Failed to delete leads.');
1920 - }
1921 - },
1922 - error: function() {
1923 - $btn.prop('disabled', false).text('Delete permanently');
1924 - alert('Network error while deleting.');
1925 - }
1926 - });
1927 - });
1928 -
1929 - // Export dropdown
1930 - $('#mxch-leads-export-btn').on('click', function(e) {
1931 - e.stopPropagation();
1932 - $('#mxch-leads-export-menu').toggleClass('is-open');
1933 - });
1934 -
1935 - $(document).on('click', function() {
1936 - $('#mxch-leads-export-menu').removeClass('is-open');
1937 - });
1938 -
1939 - $('#mxch-leads-export-menu').on('click', function(e) { e.stopPropagation(); });
1940 -
1941 - $('#mxch-leads-export-menu button').on('click', function() {
1942 - if ($(this).prop('disabled')) return;
1943 - const scope = $(this).data('scope') || 'all';
1944 - const fields = $(this).data('fields') || 'email_and_name';
1945 - submitLeadsExport(scope, fields);
1946 - $('#mxch-leads-export-menu').removeClass('is-open');
1947 - });
1948 -
1949 - function submitLeadsExport(scope, fields) {
1950 - const $form = $('<form>', { method: 'POST', action: ajaxurl, style: 'display:none;' });
1951 - $form.append($('<input>', { type: 'hidden', name: 'action', value: 'mxchat_export_leads' }));
1952 - $form.append($('<input>', { type: 'hidden', name: 'security', value: $('#mxchat_leads_export_nonce').val() }));
1953 - $form.append($('<input>', { type: 'hidden', name: 'scope', value: scope }));
1954 - $form.append($('<input>', { type: 'hidden', name: 'fields', value: fields }));
1955 - if (scope === 'selected') {
1956 - Array.from(leadsState.selected).forEach(function(e) {
1957 - $form.append($('<input>', { type: 'hidden', name: 'emails[]', value: e }));
1958 - });
1959 - }
1960 - $form.appendTo('body').submit().remove();
1961 - }
1962 -
1963 - // Preload leads metadata on page load (for the nav badge count only) without rendering.
1964 - // We keep this light — the full fetch only runs when the tab is clicked.
1965 - $.ajax({
1966 - url: ajaxurl,
1967 - type: 'POST',
1968 - data: { action: 'mxchat_fetch_leads', page: 1, per_page: 1 },
1969 - success: function(response) {
1970 - if (response && response.success && response.stats) {
1971 - const total = response.stats.total_leads || 0;
1972 - const $badge = $('#mxch-leads-nav-badge');
1973 - if (total > 0) $badge.text(total).show();
1974 - }
1975 - }
1976 1277 });
1977 1278 });