PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.3
MxChat – AI Chatbot & Content Generation for WordPress v3.0.3
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 +76 -870 3.2.43.0.3 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 {
@@ -223,48 +221,22 @@
223 221 $(this).find('svg').css('transform', currentSortOrder === 'asc' ? 'rotate(180deg)' : 'rotate(0deg)');
224 222 loadChatList(currentPage, $('#mxch-search-transcripts').val());
225 223 });
226 224
227 - // Delete selected button — opens the shared confirm modal with the "also delete lead" checkbox.
225 + // Delete selected button
228 226 $('#mxch-delete-selected').on('click', function() {
229 227 const count = selectedSessions.size;
230 228 if (count === 0) return;
231 - openTranscriptConfirm(Array.from(selectedSessions), count);
232 - });
233 229
234 - // Transcript delete confirm (shared by bulk + individual) ---------------------------
235 - let transcriptConfirmSessionIds = [];
230 + if (!confirm('Are you sure you want to delete ' + count + ' conversation(s)? This action cannot be undone.')) {
231 + return;
232 + }
236 233
237 - function openTranscriptConfirm(sessionIds, count) {
238 - transcriptConfirmSessionIds = sessionIds.slice();
239 - const n = count || sessionIds.length;
240 - $('#mxch-transcript-confirm-title').text(n === 1 ? 'Delete conversation?' : 'Delete ' + n + ' conversations?');
241 - $('#mxch-transcript-confirm-body').text(
242 - n === 1
243 - ? 'This removes the conversation and its messages.'
244 - : 'This removes ' + n + ' conversations and their messages.'
245 - );
246 - $('#mxch-transcript-also-delete-lead').prop('checked', false);
247 - $('#mxch-transcript-confirm').fadeIn(120);
248 - }
249 -
250 - function closeTranscriptConfirm() {
251 - $('#mxch-transcript-confirm').fadeOut(120);
252 - transcriptConfirmSessionIds = [];
253 - }
254 -
255 - $(document).on('click', '[data-mxch-transcript-close]', closeTranscriptConfirm);
256 -
257 - $('#mxch-transcript-confirm-go').on('click', function() {
258 - const ids = transcriptConfirmSessionIds.slice();
259 - if (!ids.length) { closeTranscriptConfirm(); return; }
260 - const alsoDeleteLead = $('#mxch-transcript-also-delete-lead').is(':checked');
261 - closeTranscriptConfirm();
262 - deleteMultipleSessions(ids, alsoDeleteLead);
234 + deleteMultipleSessions(Array.from(selectedSessions));
263 235 });
264 236
265 237 // Delete multiple sessions
266 - function deleteMultipleSessions(sessionIds, alsoDeleteLead) {
238 + function deleteMultipleSessions(sessionIds) {
267 239 $.ajax({
268 240 url: ajaxurl,
269 241 type: 'POST',
270 242 data: {
@@ -269,9 +241,8 @@
269 241 type: 'POST',
270 242 data: {
271 243 action: 'mxchat_delete_chat_history',
272 244 delete_session_ids: sessionIds,
273 - also_delete_lead: alsoDeleteLead ? '1' : '0',
274 245 security: $('#mxchat_delete_chat_nonce').val()
275 246 },
276 247 success: function(response) {
277 248 try {
@@ -292,13 +263,8 @@
292 263 }
293 264
294 265 // Reload list
295 266 loadChatList(currentPage, $('#mxch-search-transcripts').val());
296 -
297 - // The Leads tab shares this data (Chat deleted pill, nav badge,
298 - // stats) — invalidate it so switching tabs re-fetches instead of
299 - // showing stale "active lead" rows.
300 - invalidateLeadsData();
301 267 } else if (jsonResponse.error) {
302 268 alert('Error: ' + jsonResponse.error);
303 269 }
304 270 } catch (e) {
@@ -379,9 +345,9 @@
379 345 // Attach checkbox handlers
380 346 $('.mxch-chat-checkbox').on('click', function(e) {
381 347 e.stopPropagation(); // Prevent triggering chat item click
382 348 const $item = $(this).closest('.mxch-chat-item');
383 - const sessionId = $item.attr('data-session-id');
349 + const sessionId = $item.data('session-id');
384 350
385 351 if ($(this).is(':checked')) {
386 352 selectedSessions.add(sessionId);
387 353 $item.addClass('selected');
@@ -397,9 +363,9 @@
397 363 $('.mxch-chat-item').on('click', function(e) {
398 364 // Don't trigger if clicking on checkbox
399 365 if ($(e.target).is('.mxch-chat-checkbox')) return;
400 366
401 - const sessionId = $(this).attr('data-session-id');
367 + const sessionId = $(this).data('session-id');
402 368 selectChat(sessionId);
403 369
404 370 // Update active state
405 371 $('.mxch-chat-item').removeClass('active');
@@ -460,13 +426,8 @@
460 426 // Select and load a chat conversation
461 427 function selectChat(sessionId) {
462 428 currentSessionId = sessionId;
463 429
464 - // Reset translation state when selecting new chat
465 - if (typeof resetTranslationState === 'function') {
466 - resetTranslationState();
467 - }
468 -
469 430 // Show loading in conversation panel
470 431 $('#mxch-conversation-empty').hide();
471 432 $('#mxch-conversation-content').show();
472 433 $('#mxch-messages-area').html('<div class="mxch-messages-loading"><span class="spinner is-active"></span> Loading conversation...</div>');
@@ -480,14 +441,8 @@
480 441 },
481 442 success: function(response) {
482 443 if (response.success) {
483 444 renderConversation(response);
484 - // Load saved translation after rendering
485 - if (typeof loadSavedTranslation === 'function') {
486 - setTimeout(function() {
487 - loadSavedTranslation(sessionId);
488 - }, 100);
489 - }
490 445 } else {
491 446 $('#mxch-messages-area').html('<div class="mxch-messages-error">Failed to load conversation</div>');
492 447 }
493 448 },
@@ -595,16 +550,21 @@
595 550 $btn.addClass('active');
596 551 }
597 552 });
598 553
599 - // Delete current chat — opens the shared confirm modal.
554 + // Delete current chat
600 555 $('#mxch-delete-current').on('click', function() {
601 556 if (!currentSessionId) return;
602 - openTranscriptConfirm([currentSessionId], 1);
557 +
558 + if (!confirm('Are you sure you want to delete this conversation? This action cannot be undone.')) {
559 + return;
560 + }
561 +
562 + deleteSession(currentSessionId);
603 563 });
604 564
605 565 // Delete session function
606 - function deleteSession(sessionId, alsoDeleteLead) {
566 + function deleteSession(sessionId) {
607 567 $.ajax({
608 568 url: ajaxurl,
609 569 type: 'POST',
610 570 data: {
@@ -609,9 +569,8 @@
609 569 type: 'POST',
610 570 data: {
611 571 action: 'mxchat_delete_chat_history',
612 572 delete_session_ids: [sessionId],
613 - also_delete_lead: alsoDeleteLead ? '1' : '0',
614 573 security: $('#mxchat_delete_chat_nonce').val()
615 574 },
616 575 success: function(response) {
617 576 try {
@@ -671,194 +630,19 @@
671 630 }, 2000);
672 631 });
673 632
674 633 // ==========================================================================
675 - // Translation Functionality
634 + // RAG Context Modal
676 635 // ==========================================================================
677 636
678 - // Store original messages for reverting
679 - let originalMessages = null;
680 - let isTranslated = false;
681 - let currentTranslationLang = null;
682 -
683 - // Load saved language preference from localStorage
684 - const savedLang = localStorage.getItem('mxch_translate_lang');
685 - if (savedLang) {
686 - $('#mxch-translate-lang').val(savedLang);
687 - }
688 -
689 - // Save language preference when changed
690 - $('#mxch-translate-lang').on('change', function() {
691 - localStorage.setItem('mxch_translate_lang', $(this).val());
692 - });
693 -
694 - // Apply translations to messages
695 - function applyTranslations(translations) {
696 - // Store original messages if not already stored
697 - if (!originalMessages) {
698 - originalMessages = [];
699 - $('#mxch-messages-area .mxch-message-bubble').each(function() {
700 - originalMessages.push($(this).html());
701 - });
702 - }
703 -
704 - // Apply translations
705 - translations.forEach(function(item) {
706 - const $bubble = $('#mxch-messages-area .mxch-message-bubble').eq(item.index);
707 - if ($bubble.length) {
708 - $bubble.html(item.translated);
709 - $bubble.addClass('translated');
710 - }
711 - });
712 -
713 - isTranslated = true;
714 - $('#mxch-show-original-btn').show();
715 - }
716 -
717 - // Load saved translation for current session
718 - function loadSavedTranslation(sessionId) {
719 - $.ajax({
720 - url: ajaxurl,
721 - type: 'POST',
722 - data: {
723 - action: 'mxchat_get_transcript_translation',
724 - session_id: sessionId
725 - },
726 - success: function(response) {
727 - if (response.success && response.has_translation) {
728 - currentTranslationLang = response.language;
729 - applyTranslations(response.translations);
730 - // Update language selector to show saved language
731 - $('#mxch-translate-lang').val(response.language);
732 - }
733 - }
734 - });
735 - }
736 -
737 - // Translate button click handler
738 - $('#mxch-translate-btn').on('click', function() {
739 - if (!currentSessionId) return;
740 -
741 - const $btn = $(this);
742 - const targetLang = $('#mxch-translate-lang').val();
743 -
744 - // Disable button and show loading state
745 - $btn.prop('disabled', true);
746 - $btn.find('.mxch-translate-text').text('Translating...');
747 - $btn.find('svg').addClass('mxch-translate-spinner');
748 -
749 - // Store original messages before translation
750 - if (!originalMessages) {
751 - originalMessages = [];
752 - $('#mxch-messages-area .mxch-message-bubble').each(function() {
753 - originalMessages.push($(this).html());
754 - });
755 - }
756 -
757 - // If already translated, restore originals first before re-translating
758 - if (isTranslated) {
759 - $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
760 - if (originalMessages[index]) {
761 - $(this).html(originalMessages[index]);
762 - $(this).removeClass('translated');
763 - }
764 - });
765 - }
766 -
767 - // Collect all message content (from originals)
768 - const messages = [];
769 - originalMessages.forEach(function(html, index) {
770 - // Create temp element to get text content
771 - const $temp = $('<div>').html(html);
772 - messages.push({
773 - index: index,
774 - content: $temp.text().trim()
775 - });
776 - });
777 -
778 - // Send translation request
779 - $.ajax({
780 - url: ajaxurl,
781 - type: 'POST',
782 - data: {
783 - action: 'mxchat_translate_messages',
784 - session_id: currentSessionId,
785 - target_lang: targetLang,
786 - messages: JSON.stringify(messages),
787 - security: mxchatAdmin.translate_nonce || ''
788 - },
789 - success: function(response) {
790 - if (response.success && response.translations) {
791 - currentTranslationLang = response.language;
792 - applyTranslations(response.translations);
793 - $btn.find('.mxch-translate-text').text('Translate');
794 - } else {
795 - alert(response.error || 'Translation failed. Please try again.');
796 - $btn.find('.mxch-translate-text').text('Translate');
797 - }
798 - },
799 - error: function() {
800 - alert('Translation request failed. Please try again.');
801 - $btn.find('.mxch-translate-text').text('Translate');
802 - },
803 - complete: function() {
804 - $btn.prop('disabled', false);
805 - $btn.find('svg').removeClass('mxch-translate-spinner');
806 - }
807 - });
808 - });
809 -
810 - // Show original button click handler
811 - $('#mxch-show-original-btn').on('click', function() {
812 - if (!originalMessages) return;
813 -
814 - // Restore original messages
815 - $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
816 - if (originalMessages[index]) {
817 - $(this).html(originalMessages[index]);
818 - $(this).removeClass('translated');
819 - }
820 - });
821 -
822 - isTranslated = false;
823 - $(this).hide();
824 - });
825 -
826 - // Reset translation state (called when selecting new chat)
827 - function resetTranslationState() {
828 - originalMessages = null;
829 - isTranslated = false;
830 - currentTranslationLang = null;
831 - $('#mxch-show-original-btn').hide();
832 - }
833 -
834 - // Make functions available to selectChat
835 - window.resetTranslationState = resetTranslationState;
836 - window.loadSavedTranslation = loadSavedTranslation;
837 -
838 - // ==========================================================================
839 - // RAG Context Modal (Sources & Actions Tabs)
840 - // ==========================================================================
841 -
842 637 function openRagContextModal(messageId) {
843 638 const $modal = $('#mxch-rag-modal');
844 639 const $loading = $modal.find('.mxch-rag-loading');
845 - const $sourcesContent = $modal.find('.mxch-rag-content');
846 - const $actionsContent = $modal.find('.mxch-actions-content');
640 + const $content = $modal.find('.mxch-rag-content');
847 641
848 - // Reset to Sources tab
849 - $modal.find('.mxch-context-tab').removeClass('active');
850 - $modal.find('.mxch-context-tab[data-tab="sources"]').addClass('active');
851 - $('#mxch-tab-sources').show();
852 - $('#mxch-tab-actions').hide();
853 -
854 - // Reset badge counts
855 - $('#mxch-sources-count, #mxch-actions-count').hide().text('0');
856 -
857 642 $modal.fadeIn(200);
858 643 $loading.show();
859 - $sourcesContent.html('');
860 - $actionsContent.html('');
644 + $content.html('');
861 645
862 646 $.ajax({
863 647 url: ajaxurl,
864 648 type: 'POST',
@@ -869,61 +653,23 @@
869 653 success: function(response) {
870 654 $loading.hide();
871 655
872 656 if (response.success && response.data) {
873 - // Render sources tab
874 - renderRagContext(response.data, $sourcesContent);
875 -
876 - // Render actions tab
877 - renderActionsContext(response.data, $actionsContent);
878 -
879 - // Update badge counts
880 - const sourcesCount = response.data.top_matches ? response.data.top_matches.length : 0;
881 - const actionsCount = response.data.action_analysis ? response.data.action_analysis.length : 0;
882 -
883 - if (sourcesCount > 0) {
884 - $('#mxch-sources-count').text(sourcesCount).show();
885 - }
886 - if (actionsCount > 0) {
887 - $('#mxch-actions-count').text(actionsCount).show();
888 - }
657 + renderRagContext(response.data, $content);
889 658 } else {
890 - $sourcesContent.html('<div class="mxch-rag-error">Unable to load document context.</div>');
891 - $actionsContent.html('<div class="mxch-rag-error">No action data available.</div>');
659 + $content.html('<div class="mxch-rag-error">Unable to load document context.</div>');
892 660 }
893 661 },
894 662 error: function() {
895 663 $loading.hide();
896 - $sourcesContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
897 - $actionsContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
664 + $content.html('<div class="mxch-rag-error">Error loading document context. Please try again.</div>');
898 665 }
899 666 });
900 667 }
901 668
902 - // Tab switching
903 - $(document).on('click', '.mxch-context-tab', function() {
904 - const $tab = $(this);
905 - const tabName = $tab.data('tab');
906 -
907 - // Update active tab
908 - $('.mxch-context-tab').removeClass('active');
909 - $tab.addClass('active');
910 -
911 - // Show/hide content
912 - $('.mxch-tab-content').hide();
913 - $('#mxch-tab-' + tabName).show();
914 - });
915 -
916 669 function renderRagContext(data, $container) {
917 670 let html = '';
918 671
919 - // Check if we have any source data
920 - if (!data.top_matches || data.top_matches.length === 0) {
921 - html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>';
922 - $container.html(html);
923 - return;
924 - }
925 -
926 672 html += '<div class="mxch-rag-summary">';
927 673 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>';
928 674 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>';
929 675 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>';
@@ -928,142 +674,76 @@
928 674 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>';
929 675 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>';
930 676 html += '</div>';
931 677
932 - const groupedByUrl = {};
678 + if (data.top_matches && data.top_matches.length > 0) {
679 + const groupedByUrl = {};
933 680
934 - data.top_matches.forEach(function(match) {
935 - const url = match.source_display || 'Unknown';
936 - if (!groupedByUrl[url]) {
937 - groupedByUrl[url] = {
938 - url: url,
939 - isUrl: url.startsWith('http'),
940 - bestScore: 0,
941 - usedForContext: false,
942 - matchedChunks: []
943 - };
944 - }
681 + data.top_matches.forEach(function(match) {
682 + const url = match.source_display || 'Unknown';
683 + if (!groupedByUrl[url]) {
684 + groupedByUrl[url] = {
685 + url: url,
686 + isUrl: url.startsWith('http'),
687 + bestScore: 0,
688 + usedForContext: false,
689 + matchedChunks: []
690 + };
691 + }
945 692
946 - if (match.similarity_percentage > groupedByUrl[url].bestScore) {
947 - groupedByUrl[url].bestScore = match.similarity_percentage;
948 - }
693 + if (match.similarity_percentage > groupedByUrl[url].bestScore) {
694 + groupedByUrl[url].bestScore = match.similarity_percentage;
695 + }
949 696
950 - if (match.used_for_context) {
951 - groupedByUrl[url].usedForContext = true;
952 - }
697 + if (match.used_for_context) {
698 + groupedByUrl[url].usedForContext = true;
699 + }
953 700
954 - groupedByUrl[url].matchedChunks.push({
955 - chunkIndex: match.chunk_index,
956 - score: match.similarity_percentage,
957 - usedForContext: match.used_for_context
701 + groupedByUrl[url].matchedChunks.push({
702 + chunkIndex: match.chunk_index,
703 + score: match.similarity_percentage,
704 + usedForContext: match.used_for_context
705 + });
958 706 });
959 - });
960 707
961 - const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore);
962 - const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length;
963 - const chunksInfo = data.total_chunks_used > 0 ? data.total_chunks_used + ' chunks sent to AI' : '';
708 + const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore);
709 + const usedUrlCount = urlGroups.filter(g => g.usedForContext).length;
964 710
965 - html += '<div class="mxch-rag-matches">';
966 - html += '<h3>Retrieved Documents</h3>';
967 - 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>';
711 + html += '<div class="mxch-rag-matches">';
712 + html += '<h3>Retrieved Documents</h3>';
713 + html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">' + usedUrlCount + ' entr' + (usedUrlCount === 1 ? 'y' : 'ies') + ' used for response</p>';
968 714
969 - urlGroups.forEach(function(group) {
970 - const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
971 - const statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
972 - const statusLabel = group.usedForContext ? 'Used' : 'Not Used';
715 + urlGroups.forEach(function(group) {
716 + const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
717 + const statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
718 + const statusLabel = group.usedForContext ? 'Used' : 'Not Used';
973 719
974 - html += '<div class="mxch-rag-match-card ' + cardClass + '">';
975 - html += '<div class="mxch-rag-match-header">';
976 - html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
720 + html += '<div class="mxch-rag-match-card ' + cardClass + '">';
721 + html += '<div class="mxch-rag-match-header">';
722 + html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
977 723
978 - if (group.matchedChunks.length > 1) {
979 - html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
980 - }
724 + if (group.matchedChunks.length > 1) {
725 + html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
726 + }
981 727
982 - html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
983 - html += '</div>';
728 + html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
729 + html += '</div>';
984 730
985 - html += '<div class="mxch-rag-match-source">';
986 - if (group.isUrl) {
987 - html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>';
988 - } else {
989 - html += escapeHtml(group.url);
990 - }
731 + html += '<div class="mxch-rag-match-source">';
732 + if (group.isUrl) {
733 + html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>';
734 + } else {
735 + html += escapeHtml(group.url);
736 + }
737 + html += '</div>';
738 + html += '</div>';
739 + });
740 +
991 741 html += '</div>';
992 - html += '</div>';
993 - });
994 -
995 - html += '</div>';
996 - $container.html(html);
997 - }
998 -
999 - function renderActionsContext(data, $container) {
1000 - let html = '';
1001 -
1002 - // Check if we have action analysis data
1003 - if (!data.action_analysis || data.action_analysis.length === 0) {
1004 - 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>';
1005 - $container.html(html);
1006 - return;
742 + } else {
743 + html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>';
1007 744 }
1008 745
1009 - const actions = data.action_analysis;
1010 - const triggeredAction = actions.find(a => a.triggered);
1011 - const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
1012 -
1013 - // Summary section
1014 - html += '<div class="mxch-rag-summary">';
1015 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Actions Evaluated:</span> <span class="mxch-rag-value">' + actions.length + '</span></div>';
1016 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Above Threshold:</span> <span class="mxch-rag-value">' + actionsAboveThreshold + '</span></div>';
1017 - if (triggeredAction) {
1018 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Triggered:</span> <span class="mxch-rag-value" style="color: #10b981; font-weight: 600;">' + escapeHtml(triggeredAction.intent_label) + '</span></div>';
1019 - }
1020 - html += '</div>';
1021 -
1022 - // Actions list
1023 - html += '<div class="mxch-rag-matches">';
1024 - html += '<h3>Action Scores</h3>';
1025 - html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">Showing all evaluated actions sorted by similarity score</p>';
1026 -
1027 - actions.forEach(function(action) {
1028 - let cardClass = 'mxch-rag-match-below';
1029 - let statusIcon = '&#10007;';
1030 - let statusLabel = 'Below Threshold';
1031 -
1032 - if (action.triggered) {
1033 - cardClass = 'mxch-action-triggered';
1034 - statusIcon = '&#9889;';
1035 - statusLabel = 'Triggered';
1036 - } else if (action.above_threshold) {
1037 - cardClass = 'mxch-rag-match-used';
1038 - statusIcon = '&#10003;';
1039 - statusLabel = 'Above Threshold';
1040 - }
1041 -
1042 - html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1043 - html += '<div class="mxch-rag-match-header">';
1044 - html += '<span class="mxch-rag-match-score">' + action.similarity_percentage + '%</span>';
1045 - html += '<span class="mxch-action-threshold-badge">Threshold: ' + action.threshold_percentage + '%</span>';
1046 - html += '<span class="mxch-rag-match-status ' + (action.triggered ? 'status-triggered' : (action.above_threshold ? 'status-used' : 'status-below')) + '">' + statusIcon + ' ' + statusLabel + '</span>';
1047 - html += '</div>';
1048 -
1049 - html += '<div class="mxch-action-details">';
1050 - html += '<div class="mxch-action-label">' + escapeHtml(action.intent_label) + '</div>';
1051 - html += '<div class="mxch-action-callback"><span class="mxch-action-callback-label">Callback:</span> ' + escapeHtml(action.callback_function) + '</div>';
1052 - html += '</div>';
1053 -
1054 - // Score bar visualization
1055 - const scoreBarWidth = Math.min(action.similarity_percentage, 100);
1056 - const thresholdPos = Math.min(action.threshold_percentage, 100);
1057 - html += '<div class="mxch-action-score-bar">';
1058 - html += '<div class="mxch-action-score-fill" style="width: ' + scoreBarWidth + '%;"></div>';
1059 - html += '<div class="mxch-action-threshold-marker" style="left: ' + thresholdPos + '%;"></div>';
1060 - html += '</div>';
1061 -
1062 - html += '</div>';
1063 - });
1064 -
1065 - html += '</div>';
1066 746 $container.html(html);
1067 747 }
1068 748
1069 749 function escapeHtml(text) {
@@ -1304,480 +984,6 @@
1304 984 clearTimeout(resizeTimeout);
1305 985 resizeTimeout = setTimeout(function() {
1306 986 initActivityChart();
1307 987 }, 250);
1308 - });
1309 -
1310 - // ==========================================================================
1311 - // Leads Tab
1312 - // ==========================================================================
1313 -
1314 - const leadsState = {
1315 - loaded: false,
1316 - page: 1,
1317 - perPage: 25,
1318 - totalPages: 1,
1319 - totalCount: 0,
1320 - selected: new Set(),
1321 - filters: {
1322 - search: '',
1323 - dateRange: 'all',
1324 - status: 'all',
1325 - pageUrl: '',
1326 - pageTitle: ''
1327 - },
1328 - pendingDelete: [],
1329 - leadsRows: [] // last-rendered rows for quick lookup
1330 - };
1331 -
1332 - function $leads() { return $('#leads'); }
1333 -
1334 - // Called after a transcript delete from the All Chats side. Marks the Leads tab
1335 - // data stale so the next tab visit re-fetches, and refreshes immediately if the
1336 - // Leads tab happens to already be visible.
1337 - function invalidateLeadsData() {
1338 - leadsState.loaded = false;
1339 - if ($('#leads').hasClass('active')) {
1340 - loadLeads(1);
1341 - }
1342 - }
1343 -
1344 - function escapeHtmlLeads(s) {
1345 - if (s === null || typeof s === 'undefined') return '';
1346 - return String(s)
1347 - .replace(/&/g, '&amp;')
1348 - .replace(/</g, '&lt;')
1349 - .replace(/>/g, '&gt;')
1350 - .replace(/"/g, '&quot;')
1351 - .replace(/'/g, '&#039;');
1352 - }
1353 -
1354 - function leadsFiltersActive() {
1355 - const f = leadsState.filters;
1356 - return f.search !== '' || f.dateRange !== 'all' || f.status !== 'all' || f.pageUrl !== '';
1357 - }
1358 -
1359 - function updateClearFiltersButton() {
1360 - if (leadsFiltersActive()) {
1361 - $('#mxch-leads-clear-filters').show();
1362 - } else {
1363 - $('#mxch-leads-clear-filters').hide();
1364 - }
1365 - }
1366 -
1367 - function setPageFilterChip(url, title) {
1368 - leadsState.filters.pageUrl = url || '';
1369 - leadsState.filters.pageTitle = title || url || '';
1370 - const $chip = $('#mxch-leads-active-page-filter');
1371 - if (url) {
1372 - $chip.find('.mxch-leads-page-chip-label').text('Page: ' + (title || url));
1373 - $chip.show();
1374 - } else {
1375 - $chip.hide();
1376 - }
1377 - updateClearFiltersButton();
1378 - }
1379 -
1380 - function loadLeads(page) {
1381 - if (typeof page === 'number') leadsState.page = page;
1382 -
1383 - const $tbody = $('#mxch-leads-tbody');
1384 - $tbody.html('<tr><td colspan="6" class="mxch-leads-loading"><span class="spinner is-active"></span></td></tr>');
1385 -
1386 - $.ajax({
1387 - url: ajaxurl,
1388 - type: 'POST',
1389 - data: {
1390 - action: 'mxchat_fetch_leads',
1391 - page: leadsState.page,
1392 - per_page: leadsState.perPage,
1393 - search: leadsState.filters.search,
1394 - date_range: leadsState.filters.dateRange,
1395 - status: leadsState.filters.status,
1396 - page_url: leadsState.filters.pageUrl
1397 - },
1398 - success: function(response) {
1399 - leadsState.loaded = true;
1400 - if (!response || !response.success) {
1401 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1402 - return;
1403 - }
1404 - leadsState.totalPages = response.total_pages || 1;
1405 - leadsState.totalCount = response.total_count || 0;
1406 - leadsState.leadsRows = response.leads || [];
1407 -
1408 - renderLeadsStats(response.stats || {});
1409 - renderLeadsTopPages(response.top_pages || []);
1410 - renderLeadsTable(response.leads || []);
1411 - renderLeadsCount(response.showing_start, response.showing_end, response.total_count);
1412 - renderLeadsPagination(response.page, response.total_pages);
1413 -
1414 - // Nav badge
1415 - if (response.stats && typeof response.stats.total_leads === 'number') {
1416 - const $badge = $('#mxch-leads-nav-badge');
1417 - if (response.stats.total_leads > 0) {
1418 - $badge.text(response.stats.total_leads).show();
1419 - } else {
1420 - $badge.hide();
1421 - }
1422 - }
1423 - },
1424 - error: function() {
1425 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1426 - }
1427 - });
1428 - }
1429 -
1430 - function renderLeadsStats(stats) {
1431 - $('#mxch-leads-stat-total').text(stats.total_leads || 0);
1432 - $('#mxch-leads-stat-new').text(stats.new_this_week || 0);
1433 - $('#mxch-leads-stat-avg').text(stats.avg_convos || 0);
1434 - const pct = stats.orphan_pct || 0;
1435 - $('#mxch-leads-stat-orphan').text(pct + '%');
1436 - const orphanCount = stats.orphan_count || 0;
1437 - $('#mxch-leads-stat-orphan-sub').text(orphanCount + (orphanCount === 1 ? ' lead captured but never chatted' : ' leads captured but never chatted'));
1438 - }
1439 -
1440 - function renderLeadsTopPages(pages) {
1441 - const $wrap = $('#mxch-leads-toppages-list');
1442 - if (!pages || pages.length === 0) {
1443 - $wrap.html('<div class="mxch-leads-empty-mini">No page data yet.</div>');
1444 - return;
1445 - }
1446 - let html = '';
1447 - pages.forEach(function(p) {
1448 - const isActive = leadsState.filters.pageUrl === p.url ? ' is-active' : '';
1449 - html += `
1450 - <button type="button" class="mxch-leads-toppage-row${isActive}" data-url="${escapeHtmlLeads(p.url)}" data-title="${escapeHtmlLeads(p.title)}">
1451 - <span class="mxch-leads-toppage-title">${escapeHtmlLeads(p.title || p.url)}</span>
1452 - <span class="mxch-leads-toppage-count">${p.lead_count}</span>
1453 - </button>
1454 - `;
1455 - });
1456 - $wrap.html(html);
1457 - }
1458 -
1459 - function renderLeadsTable(rows) {
1460 - const $tbody = $('#mxch-leads-tbody');
1461 - if (!rows || rows.length === 0) {
1462 - $tbody.html(`
1463 - <tr><td colspan="6" class="mxch-leads-empty">
1464 - <div class="mxch-leads-empty-wrap">
1465 - <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>
1466 - <p>No leads match the current filters.</p>
1467 - </div>
1468 - </td></tr>
1469 - `);
1470 - return;
1471 - }
1472 -
1473 - let html = '';
1474 - rows.forEach(function(r) {
1475 - const emailKey = (r.email || '').toLowerCase();
1476 - const isChecked = leadsState.selected.has(emailKey) ? ' checked' : '';
1477 - // Status: 'active' (has conversations), 'chat_deleted' (admin removed the chat), 'orphan' (no chat ever).
1478 - const status = r.status || (r.is_orphan ? 'orphan' : 'active');
1479 - const isOrphan = (status === 'orphan');
1480 - const isChatDeleted = (status === 'chat_deleted');
1481 - const nameLine = r.name
1482 - ? `<span class="mxch-leads-lead-name">${escapeHtmlLeads(r.name)}</span>`
1483 - : '';
1484 - const leadCell = `
1485 - <div class="mxch-leads-lead-cell">
1486 - <span class="mxch-leads-lead-email" title="${escapeHtmlLeads(r.email)}">${escapeHtmlLeads(r.email)}</span>
1487 - ${nameLine}
1488 - </div>`;
1489 - let countCell;
1490 - if (isOrphan) {
1491 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-orphan">Orphan</span>`;
1492 - } else if (isChatDeleted) {
1493 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-deleted" title="Chat was deleted by an admin">Chat deleted</span>`;
1494 - } else {
1495 - countCell = `<span class="mxch-leads-pill">${r.conversation_count}</span>`;
1496 - }
1497 - const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : ''));
1498 - const pageCell = r.top_page_url
1499 - ? `<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>`
1500 - : '<span class="mxch-leads-muted">—</span>';
1501 - // View Convo only for active leads (orphans and chat_deleted have no viewable session).
1502 - const viewBtn = (status === 'active' && r.latest_session_id)
1503 - ? `<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">
1504 - <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>
1505 - <span>View convo</span>
1506 - </button>`
1507 - : '';
1508 - 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">
1509 - <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>
1510 - </button>`;
1511 -
1512 - const rowStateClass = isOrphan ? ' is-orphan' : (isChatDeleted ? ' is-chat-deleted' : '');
1513 - html += `
1514 - <tr class="mxch-leads-row${rowStateClass}" data-email="${escapeHtmlLeads(r.email)}">
1515 - <td class="mxch-leads-col-check"><input type="checkbox" class="mxch-leads-rowcheck"${isChecked}></td>
1516 - <td class="mxch-leads-col-lead">${leadCell}</td>
1517 - <td class="mxch-leads-col-count">${countCell}</td>
1518 - <td class="mxch-leads-col-last">${lastCell}</td>
1519 - <td class="mxch-leads-col-page">${pageCell}</td>
1520 - <td class="mxch-leads-col-actions">${viewBtn}${deleteBtn}</td>
1521 - </tr>
1522 - `;
1523 - });
1524 -
1525 - $tbody.html(html);
1526 - updateLeadsSelectionUI();
1527 - }
1528 -
1529 - function renderLeadsCount(start, end, total) {
1530 - if (!total) {
1531 - $('#mxch-leads-count').text('0 leads');
1532 - } else {
1533 - $('#mxch-leads-count').text(start + '-' + end + ' / ' + total + ' leads');
1534 - }
1535 - }
1536 -
1537 - function renderLeadsPagination(currentPage, totalPages) {
1538 - const $c = $('#mxch-leads-pagination');
1539 - if (!totalPages || totalPages <= 1) { $c.html(''); return; }
1540 - let html = '<div class="mxch-pagination-btns">';
1541 - if (currentPage > 1) {
1542 - html += `<button class="mxch-page-btn" data-page="${currentPage - 1}">&laquo;</button>`;
1543 - }
1544 - html += `<span class="mxch-page-info">${currentPage} / ${totalPages}</span>`;
1545 - if (currentPage < totalPages) {
1546 - html += `<button class="mxch-page-btn" data-page="${currentPage + 1}">&raquo;</button>`;
1547 - }
1548 - html += '</div>';
1549 - $c.html(html);
1550 - }
1551 -
1552 - function updateLeadsSelectionUI() {
1553 - const count = leadsState.selected.size;
1554 - const $countEl = $('#mxch-leads-selected-count');
1555 - const $del = $('#mxch-leads-delete-selected');
1556 - if (count > 0) {
1557 - $countEl.text(count + ' selected').addClass('has-selection');
1558 - $del.prop('disabled', false);
1559 - } else {
1560 - $countEl.text('0').removeClass('has-selection');
1561 - $del.prop('disabled', true);
1562 - }
1563 - // Selected-scope export menu items
1564 - $('#mxch-leads-export-menu button[data-scope="selected"]').prop('disabled', count === 0);
1565 -
1566 - // Select-all checkbox state
1567 - const $checks = $('.mxch-leads-rowcheck');
1568 - const checked = $checks.filter(':checked').length;
1569 - const total = $checks.length;
1570 - $('#mxch-leads-select-all').prop('checked', total > 0 && checked === total);
1571 - $('#mxch-leads-select-all').prop('indeterminate', checked > 0 && checked < total);
1572 - }
1573 -
1574 - // Trigger leads load when switching to the tab (works alongside the main nav handler above).
1575 - $('.mxch-nav-link[data-target="leads"], .mxch-mobile-nav-link[data-target="leads"]').on('click', function() {
1576 - if (!leadsState.loaded) {
1577 - loadLeads(1);
1578 - }
1579 - });
1580 -
1581 - // Filter: search (debounced)
1582 - let leadsSearchTimer;
1583 - $('#mxch-leads-search').on('input', function() {
1584 - clearTimeout(leadsSearchTimer);
1585 - const val = $(this).val();
1586 - leadsSearchTimer = setTimeout(function() {
1587 - leadsState.filters.search = (val || '').trim();
1588 - updateClearFiltersButton();
1589 - loadLeads(1);
1590 - }, 300);
1591 - });
1592 -
1593 - // Filter: date range
1594 - $('#mxch-leads-date-range').on('change', function() {
1595 - leadsState.filters.dateRange = $(this).val();
1596 - updateClearFiltersButton();
1597 - loadLeads(1);
1598 - });
1599 -
1600 - // Filter: status
1601 - $('#mxch-leads-status').on('change', function() {
1602 - leadsState.filters.status = $(this).val();
1603 - updateClearFiltersButton();
1604 - loadLeads(1);
1605 - });
1606 -
1607 - // Clear filters
1608 - $('#mxch-leads-clear-filters').on('click', function() {
1609 - leadsState.filters = { search: '', dateRange: 'all', status: 'all', pageUrl: '', pageTitle: '' };
1610 - $('#mxch-leads-search').val('');
1611 - $('#mxch-leads-date-range').val('all');
1612 - $('#mxch-leads-status').val('all');
1613 - setPageFilterChip('', '');
1614 - loadLeads(1);
1615 - });
1616 -
1617 - // Remove page chip
1618 - $leads().on('click', '.mxch-leads-page-chip-remove', function() {
1619 - setPageFilterChip('', '');
1620 - loadLeads(1);
1621 - });
1622 -
1623 - // Top Pages click -> set filter
1624 - $leads().on('click', '.mxch-leads-toppage-row', function() {
1625 - const url = $(this).data('url') || '';
1626 - const title = $(this).data('title') || '';
1627 - setPageFilterChip(url, title);
1628 - loadLeads(1);
1629 - });
1630 -
1631 - // Pagination click
1632 - $leads().on('click', '#mxch-leads-pagination .mxch-page-btn', function() {
1633 - const p = parseInt($(this).data('page'), 10);
1634 - if (p > 0) loadLeads(p);
1635 - });
1636 -
1637 - // Select-all
1638 - $('#mxch-leads-select-all').on('change', function() {
1639 - const on = $(this).is(':checked');
1640 - $('.mxch-leads-rowcheck').prop('checked', on);
1641 - $('.mxch-leads-row').each(function() {
1642 - const email = ($(this).data('email') || '').toString().toLowerCase();
1643 - if (on) {
1644 - leadsState.selected.add(email);
1645 - } else {
1646 - leadsState.selected.delete(email);
1647 - }
1648 - });
1649 - updateLeadsSelectionUI();
1650 - });
1651 -
1652 - // Row checkbox
1653 - $leads().on('change', '.mxch-leads-rowcheck', function() {
1654 - const email = ($(this).closest('.mxch-leads-row').data('email') || '').toString().toLowerCase();
1655 - if ($(this).is(':checked')) {
1656 - leadsState.selected.add(email);
1657 - } else {
1658 - leadsState.selected.delete(email);
1659 - }
1660 - updateLeadsSelectionUI();
1661 - });
1662 -
1663 - // View convo -> jump to All Chats tab and open the session
1664 - $leads().on('click', '.mxch-leads-view', function() {
1665 - const sid = $(this).attr('data-session-id');
1666 - if (!sid) return;
1667 - $('.mxch-nav-link[data-target="all-chats"]').trigger('click');
1668 - // selectChat is defined earlier in this closure
1669 - if (typeof selectChat === 'function') {
1670 - setTimeout(function() { selectChat(sid); }, 30);
1671 - }
1672 - });
1673 -
1674 - // Row delete -> confirm for one
1675 - $leads().on('click', '.mxch-leads-delete-row', function() {
1676 - const email = $(this).data('email');
1677 - if (!email) return;
1678 - openLeadsConfirm([String(email)]);
1679 - });
1680 -
1681 - // Bulk delete -> confirm for N
1682 - $('#mxch-leads-delete-selected').on('click', function() {
1683 - if (leadsState.selected.size === 0) return;
1684 - openLeadsConfirm(Array.from(leadsState.selected));
1685 - });
1686 -
1687 - function openLeadsConfirm(emails) {
1688 - leadsState.pendingDelete = emails;
1689 - const count = emails.length;
1690 - const msg = count === 1
1691 - ? 'Delete lead "' + emails[0] + '" and all of their conversations?'
1692 - : 'Delete ' + count + ' leads and all of their conversations?';
1693 - $('#mxch-leads-confirm-body').text(msg);
1694 - $('#mxch-leads-confirm').fadeIn(120);
1695 - }
1696 -
1697 - function closeLeadsConfirm() {
1698 - $('#mxch-leads-confirm').fadeOut(120);
1699 - leadsState.pendingDelete = [];
1700 - }
1701 -
1702 - $leads().on('click', '[data-mxch-leads-close]', closeLeadsConfirm);
1703 -
1704 - $('#mxch-leads-confirm-go').on('click', function() {
1705 - const emails = leadsState.pendingDelete.slice();
1706 - if (!emails.length) { closeLeadsConfirm(); return; }
1707 -
1708 - const $btn = $(this).prop('disabled', true).text('Deleting...');
1709 -
1710 - $.ajax({
1711 - url: ajaxurl,
1712 - type: 'POST',
1713 - data: {
1714 - action: 'mxchat_delete_leads',
1715 - security: $('#mxchat_leads_delete_nonce').val(),
1716 - emails: emails
1717 - },
1718 - success: function(response) {
1719 - $btn.prop('disabled', false).text('Delete permanently');
1720 - closeLeadsConfirm();
1721 - if (response && response.success) {
1722 - emails.forEach(function(e) { leadsState.selected.delete(e.toLowerCase()); });
1723 - loadLeads(leadsState.page);
1724 - } else {
1725 - alert((response && response.data && response.data.message) || 'Failed to delete leads.');
1726 - }
1727 - },
1728 - error: function() {
1729 - $btn.prop('disabled', false).text('Delete permanently');
1730 - alert('Network error while deleting.');
1731 - }
1732 - });
1733 - });
1734 -
1735 - // Export dropdown
1736 - $('#mxch-leads-export-btn').on('click', function(e) {
1737 - e.stopPropagation();
1738 - $('#mxch-leads-export-menu').toggleClass('is-open');
1739 - });
1740 -
1741 - $(document).on('click', function() {
1742 - $('#mxch-leads-export-menu').removeClass('is-open');
1743 - });
1744 -
1745 - $('#mxch-leads-export-menu').on('click', function(e) { e.stopPropagation(); });
1746 -
1747 - $('#mxch-leads-export-menu button').on('click', function() {
1748 - if ($(this).prop('disabled')) return;
1749 - const scope = $(this).data('scope') || 'all';
1750 - const fields = $(this).data('fields') || 'email_and_name';
1751 - submitLeadsExport(scope, fields);
1752 - $('#mxch-leads-export-menu').removeClass('is-open');
1753 - });
1754 -
1755 - function submitLeadsExport(scope, fields) {
1756 - const $form = $('<form>', { method: 'POST', action: ajaxurl, style: 'display:none;' });
1757 - $form.append($('<input>', { type: 'hidden', name: 'action', value: 'mxchat_export_leads' }));
1758 - $form.append($('<input>', { type: 'hidden', name: 'security', value: $('#mxchat_leads_export_nonce').val() }));
1759 - $form.append($('<input>', { type: 'hidden', name: 'scope', value: scope }));
1760 - $form.append($('<input>', { type: 'hidden', name: 'fields', value: fields }));
1761 - if (scope === 'selected') {
1762 - Array.from(leadsState.selected).forEach(function(e) {
1763 - $form.append($('<input>', { type: 'hidden', name: 'emails[]', value: e }));
1764 - });
1765 - }
1766 - $form.appendTo('body').submit().remove();
1767 - }
1768 -
1769 - // Preload leads metadata on page load (for the nav badge count only) without rendering.
1770 - // We keep this light — the full fetch only runs when the tab is clicked.
1771 - $.ajax({
1772 - url: ajaxurl,
1773 - type: 'POST',
1774 - data: { action: 'mxchat_fetch_leads', page: 1, per_page: 1 },
1775 - success: function(response) {
1776 - if (response && response.success && response.stats) {
1777 - const total = response.stats.total_leads || 0;
1778 - const $badge = $('#mxch-leads-nav-badge');
1779 - if (total > 0) $badge.text(total).show();
1780 - }
1781 - }
1782 988 });
1783 989 });