PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.2
MxChat – AI Chatbot & Content Generation for WordPress v3.0.2
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 +81 -935 3.2.73.0.2 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');
@@ -510,13 +426,8 @@
510 426 // Select and load a chat conversation
511 427 function selectChat(sessionId) {
512 428 currentSessionId = sessionId;
513 429
514 - // Reset translation state when selecting new chat
515 - if (typeof resetTranslationState === 'function') {
516 - resetTranslationState();
517 - }
518 -
519 430 // Show loading in conversation panel
520 431 $('#mxch-conversation-empty').hide();
521 432 $('#mxch-conversation-content').show();
522 433 $('#mxch-messages-area').html('<div class="mxch-messages-loading"><span class="spinner is-active"></span> Loading conversation...</div>');
@@ -530,14 +441,8 @@
530 441 },
531 442 success: function(response) {
532 443 if (response.success) {
533 444 renderConversation(response);
534 - // Load saved translation after rendering
535 - if (typeof loadSavedTranslation === 'function') {
536 - setTimeout(function() {
537 - loadSavedTranslation(sessionId);
538 - }, 100);
539 - }
540 445 } else {
541 446 $('#mxch-messages-area').html('<div class="mxch-messages-error">Failed to load conversation</div>');
542 447 }
543 448 },
@@ -570,18 +475,8 @@
570 475 } else {
571 476 $('#mxch-detail-email-row').hide();
572 477 }
573 478
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 479 // Clicked links
585 480 if (data.clicked_urls && data.clicked_urls.length > 0) {
586 481 let linksHtml = '';
587 482 data.clicked_urls.forEach(function(url) {
@@ -655,16 +550,21 @@
655 550 $btn.addClass('active');
656 551 }
657 552 });
658 553
659 - // Delete current chat — opens the shared confirm modal.
554 + // Delete current chat
660 555 $('#mxch-delete-current').on('click', function() {
661 556 if (!currentSessionId) return;
662 - 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);
663 563 });
664 564
665 565 // Delete session function
666 - function deleteSession(sessionId, alsoDeleteLead) {
566 + function deleteSession(sessionId) {
667 567 $.ajax({
668 568 url: ajaxurl,
669 569 type: 'POST',
670 570 data: {
@@ -669,9 +569,8 @@
669 569 type: 'POST',
670 570 data: {
671 571 action: 'mxchat_delete_chat_history',
672 572 delete_session_ids: [sessionId],
673 - also_delete_lead: alsoDeleteLead ? '1' : '0',
674 573 security: $('#mxchat_delete_chat_nonce').val()
675 574 },
676 575 success: function(response) {
677 576 try {
@@ -731,194 +630,19 @@
731 630 }, 2000);
732 631 });
733 632
734 633 // ==========================================================================
735 - // Translation Functionality
634 + // RAG Context Modal
736 635 // ==========================================================================
737 636
738 - // Store original messages for reverting
739 - let originalMessages = null;
740 - let isTranslated = false;
741 - let currentTranslationLang = null;
742 -
743 - // Load saved language preference from localStorage
744 - const savedLang = localStorage.getItem('mxch_translate_lang');
745 - if (savedLang) {
746 - $('#mxch-translate-lang').val(savedLang);
747 - }
748 -
749 - // Save language preference when changed
750 - $('#mxch-translate-lang').on('change', function() {
751 - localStorage.setItem('mxch_translate_lang', $(this).val());
752 - });
753 -
754 - // Apply translations to messages
755 - function applyTranslations(translations) {
756 - // Store original messages if not already stored
757 - if (!originalMessages) {
758 - originalMessages = [];
759 - $('#mxch-messages-area .mxch-message-bubble').each(function() {
760 - originalMessages.push($(this).html());
761 - });
762 - }
763 -
764 - // Apply translations
765 - translations.forEach(function(item) {
766 - const $bubble = $('#mxch-messages-area .mxch-message-bubble').eq(item.index);
767 - if ($bubble.length) {
768 - $bubble.html(item.translated);
769 - $bubble.addClass('translated');
770 - }
771 - });
772 -
773 - isTranslated = true;
774 - $('#mxch-show-original-btn').show();
775 - }
776 -
777 - // Load saved translation for current session
778 - function loadSavedTranslation(sessionId) {
779 - $.ajax({
780 - url: ajaxurl,
781 - type: 'POST',
782 - data: {
783 - action: 'mxchat_get_transcript_translation',
784 - session_id: sessionId
785 - },
786 - success: function(response) {
787 - if (response.success && response.has_translation) {
788 - currentTranslationLang = response.language;
789 - applyTranslations(response.translations);
790 - // Update language selector to show saved language
791 - $('#mxch-translate-lang').val(response.language);
792 - }
793 - }
794 - });
795 - }
796 -
797 - // Translate button click handler
798 - $('#mxch-translate-btn').on('click', function() {
799 - if (!currentSessionId) return;
800 -
801 - const $btn = $(this);
802 - const targetLang = $('#mxch-translate-lang').val();
803 -
804 - // Disable button and show loading state
805 - $btn.prop('disabled', true);
806 - $btn.find('.mxch-translate-text').text('Translating...');
807 - $btn.find('svg').addClass('mxch-translate-spinner');
808 -
809 - // Store original messages before translation
810 - if (!originalMessages) {
811 - originalMessages = [];
812 - $('#mxch-messages-area .mxch-message-bubble').each(function() {
813 - originalMessages.push($(this).html());
814 - });
815 - }
816 -
817 - // If already translated, restore originals first before re-translating
818 - if (isTranslated) {
819 - $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
820 - if (originalMessages[index]) {
821 - $(this).html(originalMessages[index]);
822 - $(this).removeClass('translated');
823 - }
824 - });
825 - }
826 -
827 - // Collect all message content (from originals)
828 - const messages = [];
829 - originalMessages.forEach(function(html, index) {
830 - // Create temp element to get text content
831 - const $temp = $('<div>').html(html);
832 - messages.push({
833 - index: index,
834 - content: $temp.text().trim()
835 - });
836 - });
837 -
838 - // Send translation request
839 - $.ajax({
840 - url: ajaxurl,
841 - type: 'POST',
842 - data: {
843 - action: 'mxchat_translate_messages',
844 - session_id: currentSessionId,
845 - target_lang: targetLang,
846 - messages: JSON.stringify(messages),
847 - security: mxchatAdmin.translate_nonce || ''
848 - },
849 - success: function(response) {
850 - if (response.success && response.translations) {
851 - currentTranslationLang = response.language;
852 - applyTranslations(response.translations);
853 - $btn.find('.mxch-translate-text').text('Translate');
854 - } else {
855 - alert(response.error || 'Translation failed. Please try again.');
856 - $btn.find('.mxch-translate-text').text('Translate');
857 - }
858 - },
859 - error: function() {
860 - alert('Translation request failed. Please try again.');
861 - $btn.find('.mxch-translate-text').text('Translate');
862 - },
863 - complete: function() {
864 - $btn.prop('disabled', false);
865 - $btn.find('svg').removeClass('mxch-translate-spinner');
866 - }
867 - });
868 - });
869 -
870 - // Show original button click handler
871 - $('#mxch-show-original-btn').on('click', function() {
872 - if (!originalMessages) return;
873 -
874 - // Restore original messages
875 - $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
876 - if (originalMessages[index]) {
877 - $(this).html(originalMessages[index]);
878 - $(this).removeClass('translated');
879 - }
880 - });
881 -
882 - isTranslated = false;
883 - $(this).hide();
884 - });
885 -
886 - // Reset translation state (called when selecting new chat)
887 - function resetTranslationState() {
888 - originalMessages = null;
889 - isTranslated = false;
890 - currentTranslationLang = null;
891 - $('#mxch-show-original-btn').hide();
892 - }
893 -
894 - // Make functions available to selectChat
895 - window.resetTranslationState = resetTranslationState;
896 - window.loadSavedTranslation = loadSavedTranslation;
897 -
898 - // ==========================================================================
899 - // RAG Context Modal (Sources & Actions Tabs)
900 - // ==========================================================================
901 -
902 637 function openRagContextModal(messageId) {
903 638 const $modal = $('#mxch-rag-modal');
904 639 const $loading = $modal.find('.mxch-rag-loading');
905 - const $sourcesContent = $modal.find('.mxch-rag-content');
906 - const $actionsContent = $modal.find('.mxch-actions-content');
640 + const $content = $modal.find('.mxch-rag-content');
907 641
908 - // Reset to Sources tab
909 - $modal.find('.mxch-context-tab').removeClass('active');
910 - $modal.find('.mxch-context-tab[data-tab="sources"]').addClass('active');
911 - $('#mxch-tab-sources').show();
912 - $('#mxch-tab-actions').hide();
913 -
914 - // Reset badge counts
915 - $('#mxch-sources-count, #mxch-actions-count').hide().text('0');
916 -
917 642 $modal.fadeIn(200);
918 643 $loading.show();
919 - $sourcesContent.html('');
920 - $actionsContent.html('');
644 + $content.html('');
921 645
922 646 $.ajax({
923 647 url: ajaxurl,
924 648 type: 'POST',
@@ -929,61 +653,23 @@
929 653 success: function(response) {
930 654 $loading.hide();
931 655
932 656 if (response.success && response.data) {
933 - // Render sources tab
934 - renderRagContext(response.data, $sourcesContent);
935 -
936 - // Render actions tab
937 - renderActionsContext(response.data, $actionsContent);
938 -
939 - // Update badge counts
940 - 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;
942 -
943 - if (sourcesCount > 0) {
944 - $('#mxch-sources-count').text(sourcesCount).show();
945 - }
946 - if (actionsCount > 0) {
947 - $('#mxch-actions-count').text(actionsCount).show();
948 - }
657 + renderRagContext(response.data, $content);
949 658 } else {
950 - $sourcesContent.html('<div class="mxch-rag-error">Unable to load document context.</div>');
951 - $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>');
952 660 }
953 661 },
954 662 error: function() {
955 663 $loading.hide();
956 - $sourcesContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
957 - $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>');
958 665 }
959 666 });
960 667 }
961 668
962 - // Tab switching
963 - $(document).on('click', '.mxch-context-tab', function() {
964 - const $tab = $(this);
965 - const tabName = $tab.data('tab');
966 -
967 - // Update active tab
968 - $('.mxch-context-tab').removeClass('active');
969 - $tab.addClass('active');
970 -
971 - // Show/hide content
972 - $('.mxch-tab-content').hide();
973 - $('#mxch-tab-' + tabName).show();
974 - });
975 -
976 669 function renderRagContext(data, $container) {
977 670 let html = '';
978 671
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>';
982 - $container.html(html);
983 - return;
984 - }
985 -
986 672 html += '<div class="mxch-rag-summary">';
987 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>';
988 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>';
989 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>';
@@ -988,142 +674,76 @@
988 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>';
989 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>';
990 676 html += '</div>';
991 677
992 - const groupedByUrl = {};
678 + if (data.top_matches && data.top_matches.length > 0) {
679 + const groupedByUrl = {};
993 680
994 - data.top_matches.forEach(function(match) {
995 - const url = match.source_display || 'Unknown';
996 - if (!groupedByUrl[url]) {
997 - groupedByUrl[url] = {
998 - url: url,
999 - isUrl: url.startsWith('http'),
1000 - bestScore: 0,
1001 - usedForContext: false,
1002 - matchedChunks: []
1003 - };
1004 - }
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 + }
1005 692
1006 - if (match.similarity_percentage > groupedByUrl[url].bestScore) {
1007 - groupedByUrl[url].bestScore = match.similarity_percentage;
1008 - }
693 + if (match.similarity_percentage > groupedByUrl[url].bestScore) {
694 + groupedByUrl[url].bestScore = match.similarity_percentage;
695 + }
1009 696
1010 - if (match.used_for_context) {
1011 - groupedByUrl[url].usedForContext = true;
1012 - }
697 + if (match.used_for_context) {
698 + groupedByUrl[url].usedForContext = true;
699 + }
1013 700
1014 - groupedByUrl[url].matchedChunks.push({
1015 - chunkIndex: match.chunk_index,
1016 - score: match.similarity_percentage,
1017 - 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 + });
1018 706 });
1019 - });
1020 707
1021 - const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore);
1022 - const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length;
1023 - 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;
1024 710
1025 - html += '<div class="mxch-rag-matches">';
1026 - html += '<h3>Retrieved Documents</h3>';
1027 - 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>';
1028 714
1029 - urlGroups.forEach(function(group) {
1030 - const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
1031 - const statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
1032 - 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';
1033 719
1034 - html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1035 - html += '<div class="mxch-rag-match-header">';
1036 - 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>';
1037 723
1038 - if (group.matchedChunks.length > 1) {
1039 - html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
1040 - }
724 + if (group.matchedChunks.length > 1) {
725 + html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
726 + }
1041 727
1042 - html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
1043 - html += '</div>';
728 + html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
729 + html += '</div>';
1044 730
1045 - html += '<div class="mxch-rag-match-source">';
1046 - if (group.isUrl) {
1047 - html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>';
1048 - } else {
1049 - html += escapeHtml(group.url);
1050 - }
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 +
1051 741 html += '</div>';
1052 - html += '</div>';
1053 - });
1054 -
1055 - html += '</div>';
1056 - $container.html(html);
1057 - }
1058 -
1059 - function renderActionsContext(data, $container) {
1060 - let html = '';
1061 -
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>';
1065 - $container.html(html);
1066 - return;
742 + } else {
743 + html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>';
1067 744 }
1068 745
1069 - const actions = data.action_analysis;
1070 - const triggeredAction = actions.find(a => a.triggered);
1071 - const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
1072 -
1073 - // Summary section
1074 - html += '<div class="mxch-rag-summary">';
1075 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Actions Evaluated:</span> <span class="mxch-rag-value">' + actions.length + '</span></div>';
1076 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Above Threshold:</span> <span class="mxch-rag-value">' + actionsAboveThreshold + '</span></div>';
1077 - if (triggeredAction) {
1078 - 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>';
1079 - }
1080 - html += '</div>';
1081 -
1082 - // Actions list
1083 - html += '<div class="mxch-rag-matches">';
1084 - html += '<h3>Action Scores</h3>';
1085 - html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">Showing all evaluated actions sorted by similarity score</p>';
1086 -
1087 - actions.forEach(function(action) {
1088 - let cardClass = 'mxch-rag-match-below';
1089 - let statusIcon = '&#10007;';
1090 - let statusLabel = 'Below Threshold';
1091 -
1092 - if (action.triggered) {
1093 - cardClass = 'mxch-action-triggered';
1094 - statusIcon = '&#9889;';
1095 - statusLabel = 'Triggered';
1096 - } else if (action.above_threshold) {
1097 - cardClass = 'mxch-rag-match-used';
1098 - statusIcon = '&#10003;';
1099 - statusLabel = 'Above Threshold';
1100 - }
1101 -
1102 - html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1103 - html += '<div class="mxch-rag-match-header">';
1104 - html += '<span class="mxch-rag-match-score">' + action.similarity_percentage + '%</span>';
1105 - html += '<span class="mxch-action-threshold-badge">Threshold: ' + action.threshold_percentage + '%</span>';
1106 - html += '<span class="mxch-rag-match-status ' + (action.triggered ? 'status-triggered' : (action.above_threshold ? 'status-used' : 'status-below')) + '">' + statusIcon + ' ' + statusLabel + '</span>';
1107 - html += '</div>';
1108 -
1109 - html += '<div class="mxch-action-details">';
1110 - html += '<div class="mxch-action-label">' + escapeHtml(action.intent_label) + '</div>';
1111 - html += '<div class="mxch-action-callback"><span class="mxch-action-callback-label">Callback:</span> ' + escapeHtml(action.callback_function) + '</div>';
1112 - html += '</div>';
1113 -
1114 - // Score bar visualization
1115 - const scoreBarWidth = Math.min(action.similarity_percentage, 100);
1116 - const thresholdPos = Math.min(action.threshold_percentage, 100);
1117 - html += '<div class="mxch-action-score-bar">';
1118 - html += '<div class="mxch-action-score-fill" style="width: ' + scoreBarWidth + '%;"></div>';
1119 - html += '<div class="mxch-action-threshold-marker" style="left: ' + thresholdPos + '%;"></div>';
1120 - html += '</div>';
1121 -
1122 - html += '</div>';
1123 - });
1124 -
1125 - html += '</div>';
1126 746 $container.html(html);
1127 747 }
1128 748
1129 749 function escapeHtml(text) {
@@ -1364,480 +984,6 @@
1364 984 clearTimeout(resizeTimeout);
1365 985 resizeTimeout = setTimeout(function() {
1366 986 initActivityChart();
1367 987 }, 250);
1368 - });
1369 -
1370 - // ==========================================================================
1371 - // Leads Tab
1372 - // ==========================================================================
1373 -
1374 - const leadsState = {
1375 - loaded: false,
1376 - page: 1,
1377 - perPage: 25,
1378 - totalPages: 1,
1379 - totalCount: 0,
1380 - selected: new Set(),
1381 - filters: {
1382 - search: '',
1383 - dateRange: 'all',
1384 - status: 'all',
1385 - pageUrl: '',
1386 - pageTitle: ''
1387 - },
1388 - pendingDelete: [],
1389 - leadsRows: [] // last-rendered rows for quick lookup
1390 - };
1391 -
1392 - function $leads() { return $('#leads'); }
1393 -
1394 - // Called after a transcript delete from the All Chats side. Marks the Leads tab
1395 - // data stale so the next tab visit re-fetches, and refreshes immediately if the
1396 - // Leads tab happens to already be visible.
1397 - function invalidateLeadsData() {
1398 - leadsState.loaded = false;
1399 - if ($('#leads').hasClass('active')) {
1400 - loadLeads(1);
1401 - }
1402 - }
1403 -
1404 - function escapeHtmlLeads(s) {
1405 - if (s === null || typeof s === 'undefined') return '';
1406 - return String(s)
1407 - .replace(/&/g, '&amp;')
1408 - .replace(/</g, '&lt;')
1409 - .replace(/>/g, '&gt;')
1410 - .replace(/"/g, '&quot;')
1411 - .replace(/'/g, '&#039;');
1412 - }
1413 -
1414 - function leadsFiltersActive() {
1415 - const f = leadsState.filters;
1416 - return f.search !== '' || f.dateRange !== 'all' || f.status !== 'all' || f.pageUrl !== '';
1417 - }
1418 -
1419 - function updateClearFiltersButton() {
1420 - if (leadsFiltersActive()) {
1421 - $('#mxch-leads-clear-filters').show();
1422 - } else {
1423 - $('#mxch-leads-clear-filters').hide();
1424 - }
1425 - }
1426 -
1427 - function setPageFilterChip(url, title) {
1428 - leadsState.filters.pageUrl = url || '';
1429 - leadsState.filters.pageTitle = title || url || '';
1430 - const $chip = $('#mxch-leads-active-page-filter');
1431 - if (url) {
1432 - $chip.find('.mxch-leads-page-chip-label').text('Page: ' + (title || url));
1433 - $chip.show();
1434 - } else {
1435 - $chip.hide();
1436 - }
1437 - updateClearFiltersButton();
1438 - }
1439 -
1440 - function loadLeads(page) {
1441 - if (typeof page === 'number') leadsState.page = page;
1442 -
1443 - const $tbody = $('#mxch-leads-tbody');
1444 - $tbody.html('<tr><td colspan="6" class="mxch-leads-loading"><span class="spinner is-active"></span></td></tr>');
1445 -
1446 - $.ajax({
1447 - url: ajaxurl,
1448 - type: 'POST',
1449 - data: {
1450 - action: 'mxchat_fetch_leads',
1451 - page: leadsState.page,
1452 - per_page: leadsState.perPage,
1453 - search: leadsState.filters.search,
1454 - date_range: leadsState.filters.dateRange,
1455 - status: leadsState.filters.status,
1456 - page_url: leadsState.filters.pageUrl
1457 - },
1458 - success: function(response) {
1459 - leadsState.loaded = true;
1460 - if (!response || !response.success) {
1461 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1462 - return;
1463 - }
1464 - leadsState.totalPages = response.total_pages || 1;
1465 - leadsState.totalCount = response.total_count || 0;
1466 - leadsState.leadsRows = response.leads || [];
1467 -
1468 - renderLeadsStats(response.stats || {});
1469 - renderLeadsTopPages(response.top_pages || []);
1470 - renderLeadsTable(response.leads || []);
1471 - renderLeadsCount(response.showing_start, response.showing_end, response.total_count);
1472 - renderLeadsPagination(response.page, response.total_pages);
1473 -
1474 - // Nav badge
1475 - if (response.stats && typeof response.stats.total_leads === 'number') {
1476 - const $badge = $('#mxch-leads-nav-badge');
1477 - if (response.stats.total_leads > 0) {
1478 - $badge.text(response.stats.total_leads).show();
1479 - } else {
1480 - $badge.hide();
1481 - }
1482 - }
1483 - },
1484 - error: function() {
1485 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1486 - }
1487 - });
1488 - }
1489 -
1490 - function renderLeadsStats(stats) {
1491 - $('#mxch-leads-stat-total').text(stats.total_leads || 0);
1492 - $('#mxch-leads-stat-new').text(stats.new_this_week || 0);
1493 - $('#mxch-leads-stat-avg').text(stats.avg_convos || 0);
1494 - const pct = stats.orphan_pct || 0;
1495 - $('#mxch-leads-stat-orphan').text(pct + '%');
1496 - const orphanCount = stats.orphan_count || 0;
1497 - $('#mxch-leads-stat-orphan-sub').text(orphanCount + (orphanCount === 1 ? ' lead captured but never chatted' : ' leads captured but never chatted'));
1498 - }
1499 -
1500 - function renderLeadsTopPages(pages) {
1501 - const $wrap = $('#mxch-leads-toppages-list');
1502 - if (!pages || pages.length === 0) {
1503 - $wrap.html('<div class="mxch-leads-empty-mini">No page data yet.</div>');
1504 - return;
1505 - }
1506 - let html = '';
1507 - pages.forEach(function(p) {
1508 - const isActive = leadsState.filters.pageUrl === p.url ? ' is-active' : '';
1509 - html += `
1510 - <button type="button" class="mxch-leads-toppage-row${isActive}" data-url="${escapeHtmlLeads(p.url)}" data-title="${escapeHtmlLeads(p.title)}">
1511 - <span class="mxch-leads-toppage-title">${escapeHtmlLeads(p.title || p.url)}</span>
1512 - <span class="mxch-leads-toppage-count">${p.lead_count}</span>
1513 - </button>
1514 - `;
1515 - });
1516 - $wrap.html(html);
1517 - }
1518 -
1519 - function renderLeadsTable(rows) {
1520 - const $tbody = $('#mxch-leads-tbody');
1521 - if (!rows || rows.length === 0) {
1522 - $tbody.html(`
1523 - <tr><td colspan="6" class="mxch-leads-empty">
1524 - <div class="mxch-leads-empty-wrap">
1525 - <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 - <p>No leads match the current filters.</p>
1527 - </div>
1528 - </td></tr>
1529 - `);
1530 - return;
1531 - }
1532 -
1533 - let html = '';
1534 - rows.forEach(function(r) {
1535 - const emailKey = (r.email || '').toLowerCase();
1536 - const isChecked = leadsState.selected.has(emailKey) ? ' checked' : '';
1537 - // Status: 'active' (has conversations), 'chat_deleted' (admin removed the chat), 'orphan' (no chat ever).
1538 - const status = r.status || (r.is_orphan ? 'orphan' : 'active');
1539 - const isOrphan = (status === 'orphan');
1540 - const isChatDeleted = (status === 'chat_deleted');
1541 - const nameLine = r.name
1542 - ? `<span class="mxch-leads-lead-name">${escapeHtmlLeads(r.name)}</span>`
1543 - : '';
1544 - const leadCell = `
1545 - <div class="mxch-leads-lead-cell">
1546 - <span class="mxch-leads-lead-email" title="${escapeHtmlLeads(r.email)}">${escapeHtmlLeads(r.email)}</span>
1547 - ${nameLine}
1548 - </div>`;
1549 - let countCell;
1550 - if (isOrphan) {
1551 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-orphan">Orphan</span>`;
1552 - } else if (isChatDeleted) {
1553 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-deleted" title="Chat was deleted by an admin">Chat deleted</span>`;
1554 - } else {
1555 - countCell = `<span class="mxch-leads-pill">${r.conversation_count}</span>`;
1556 - }
1557 - const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : ''));
1558 - const pageCell = r.top_page_url
1559 - ? `<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 - : '<span class="mxch-leads-muted">—</span>';
1561 - // View Convo only for active leads (orphans and chat_deleted have no viewable session).
1562 - const viewBtn = (status === 'active' && r.latest_session_id)
1563 - ? `<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 - <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>
1565 - <span>View convo</span>
1566 - </button>`
1567 - : '';
1568 - 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">
1569 - <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>
1570 - </button>`;
1571 -
1572 - const rowStateClass = isOrphan ? ' is-orphan' : (isChatDeleted ? ' is-chat-deleted' : '');
1573 - html += `
1574 - <tr class="mxch-leads-row${rowStateClass}" data-email="${escapeHtmlLeads(r.email)}">
1575 - <td class="mxch-leads-col-check"><input type="checkbox" class="mxch-leads-rowcheck"${isChecked}></td>
1576 - <td class="mxch-leads-col-lead">${leadCell}</td>
1577 - <td class="mxch-leads-col-count">${countCell}</td>
1578 - <td class="mxch-leads-col-last">${lastCell}</td>
1579 - <td class="mxch-leads-col-page">${pageCell}</td>
1580 - <td class="mxch-leads-col-actions">${viewBtn}${deleteBtn}</td>
1581 - </tr>
1582 - `;
1583 - });
1584 -
1585 - $tbody.html(html);
1586 - updateLeadsSelectionUI();
1587 - }
1588 -
1589 - function renderLeadsCount(start, end, total) {
1590 - if (!total) {
1591 - $('#mxch-leads-count').text('0 leads');
1592 - } else {
1593 - $('#mxch-leads-count').text(start + '-' + end + ' / ' + total + ' leads');
1594 - }
1595 - }
1596 -
1597 - function renderLeadsPagination(currentPage, totalPages) {
1598 - const $c = $('#mxch-leads-pagination');
1599 - if (!totalPages || totalPages <= 1) { $c.html(''); return; }
1600 - let html = '<div class="mxch-pagination-btns">';
1601 - if (currentPage > 1) {
1602 - html += `<button class="mxch-page-btn" data-page="${currentPage - 1}">&laquo;</button>`;
1603 - }
1604 - html += `<span class="mxch-page-info">${currentPage} / ${totalPages}</span>`;
1605 - if (currentPage < totalPages) {
1606 - html += `<button class="mxch-page-btn" data-page="${currentPage + 1}">&raquo;</button>`;
1607 - }
1608 - html += '</div>';
1609 - $c.html(html);
1610 - }
1611 -
1612 - function updateLeadsSelectionUI() {
1613 - const count = leadsState.selected.size;
1614 - const $countEl = $('#mxch-leads-selected-count');
1615 - const $del = $('#mxch-leads-delete-selected');
1616 - if (count > 0) {
1617 - $countEl.text(count + ' selected').addClass('has-selection');
1618 - $del.prop('disabled', false);
1619 - } else {
1620 - $countEl.text('0').removeClass('has-selection');
1621 - $del.prop('disabled', true);
1622 - }
1623 - // Selected-scope export menu items
1624 - $('#mxch-leads-export-menu button[data-scope="selected"]').prop('disabled', count === 0);
1625 -
1626 - // Select-all checkbox state
1627 - const $checks = $('.mxch-leads-rowcheck');
1628 - const checked = $checks.filter(':checked').length;
1629 - const total = $checks.length;
1630 - $('#mxch-leads-select-all').prop('checked', total > 0 && checked === total);
1631 - $('#mxch-leads-select-all').prop('indeterminate', checked > 0 && checked < total);
1632 - }
1633 -
1634 - // Trigger leads load when switching to the tab (works alongside the main nav handler above).
1635 - $('.mxch-nav-link[data-target="leads"], .mxch-mobile-nav-link[data-target="leads"]').on('click', function() {
1636 - if (!leadsState.loaded) {
1637 - loadLeads(1);
1638 - }
1639 - });
1640 -
1641 - // Filter: search (debounced)
1642 - let leadsSearchTimer;
1643 - $('#mxch-leads-search').on('input', function() {
1644 - clearTimeout(leadsSearchTimer);
1645 - const val = $(this).val();
1646 - leadsSearchTimer = setTimeout(function() {
1647 - leadsState.filters.search = (val || '').trim();
1648 - updateClearFiltersButton();
1649 - loadLeads(1);
1650 - }, 300);
1651 - });
1652 -
1653 - // Filter: date range
1654 - $('#mxch-leads-date-range').on('change', function() {
1655 - leadsState.filters.dateRange = $(this).val();
1656 - updateClearFiltersButton();
1657 - loadLeads(1);
1658 - });
1659 -
1660 - // Filter: status
1661 - $('#mxch-leads-status').on('change', function() {
1662 - leadsState.filters.status = $(this).val();
1663 - updateClearFiltersButton();
1664 - loadLeads(1);
1665 - });
1666 -
1667 - // Clear filters
1668 - $('#mxch-leads-clear-filters').on('click', function() {
1669 - leadsState.filters = { search: '', dateRange: 'all', status: 'all', pageUrl: '', pageTitle: '' };
1670 - $('#mxch-leads-search').val('');
1671 - $('#mxch-leads-date-range').val('all');
1672 - $('#mxch-leads-status').val('all');
1673 - setPageFilterChip('', '');
1674 - loadLeads(1);
1675 - });
1676 -
1677 - // Remove page chip
1678 - $leads().on('click', '.mxch-leads-page-chip-remove', function() {
1679 - setPageFilterChip('', '');
1680 - loadLeads(1);
1681 - });
1682 -
1683 - // Top Pages click -> set filter
1684 - $leads().on('click', '.mxch-leads-toppage-row', function() {
1685 - const url = $(this).data('url') || '';
1686 - const title = $(this).data('title') || '';
1687 - setPageFilterChip(url, title);
1688 - loadLeads(1);
1689 - });
1690 -
1691 - // Pagination click
1692 - $leads().on('click', '#mxch-leads-pagination .mxch-page-btn', function() {
1693 - const p = parseInt($(this).data('page'), 10);
1694 - if (p > 0) loadLeads(p);
1695 - });
1696 -
1697 - // Select-all
1698 - $('#mxch-leads-select-all').on('change', function() {
1699 - const on = $(this).is(':checked');
1700 - $('.mxch-leads-rowcheck').prop('checked', on);
1701 - $('.mxch-leads-row').each(function() {
1702 - const email = ($(this).data('email') || '').toString().toLowerCase();
1703 - if (on) {
1704 - leadsState.selected.add(email);
1705 - } else {
1706 - leadsState.selected.delete(email);
1707 - }
1708 - });
1709 - updateLeadsSelectionUI();
1710 - });
1711 -
1712 - // Row checkbox
1713 - $leads().on('change', '.mxch-leads-rowcheck', function() {
1714 - const email = ($(this).closest('.mxch-leads-row').data('email') || '').toString().toLowerCase();
1715 - if ($(this).is(':checked')) {
1716 - leadsState.selected.add(email);
1717 - } else {
1718 - leadsState.selected.delete(email);
1719 - }
1720 - updateLeadsSelectionUI();
1721 - });
1722 -
1723 - // View convo -> jump to All Chats tab and open the session
1724 - $leads().on('click', '.mxch-leads-view', function() {
1725 - const sid = $(this).attr('data-session-id');
1726 - if (!sid) return;
1727 - $('.mxch-nav-link[data-target="all-chats"]').trigger('click');
1728 - // selectChat is defined earlier in this closure
1729 - if (typeof selectChat === 'function') {
1730 - setTimeout(function() { selectChat(sid); }, 30);
1731 - }
1732 - });
1733 -
1734 - // Row delete -> confirm for one
1735 - $leads().on('click', '.mxch-leads-delete-row', function() {
1736 - const email = $(this).data('email');
1737 - if (!email) return;
1738 - openLeadsConfirm([String(email)]);
1739 - });
1740 -
1741 - // Bulk delete -> confirm for N
1742 - $('#mxch-leads-delete-selected').on('click', function() {
1743 - if (leadsState.selected.size === 0) return;
1744 - openLeadsConfirm(Array.from(leadsState.selected));
1745 - });
1746 -
1747 - function openLeadsConfirm(emails) {
1748 - leadsState.pendingDelete = emails;
1749 - const count = emails.length;
1750 - const msg = count === 1
1751 - ? 'Delete lead "' + emails[0] + '" and all of their conversations?'
1752 - : 'Delete ' + count + ' leads and all of their conversations?';
1753 - $('#mxch-leads-confirm-body').text(msg);
1754 - $('#mxch-leads-confirm').fadeIn(120);
1755 - }
1756 -
1757 - function closeLeadsConfirm() {
1758 - $('#mxch-leads-confirm').fadeOut(120);
1759 - leadsState.pendingDelete = [];
1760 - }
1761 -
1762 - $leads().on('click', '[data-mxch-leads-close]', closeLeadsConfirm);
1763 -
1764 - $('#mxch-leads-confirm-go').on('click', function() {
1765 - const emails = leadsState.pendingDelete.slice();
1766 - if (!emails.length) { closeLeadsConfirm(); return; }
1767 -
1768 - const $btn = $(this).prop('disabled', true).text('Deleting...');
1769 -
1770 - $.ajax({
1771 - url: ajaxurl,
1772 - type: 'POST',
1773 - data: {
1774 - action: 'mxchat_delete_leads',
1775 - security: $('#mxchat_leads_delete_nonce').val(),
1776 - emails: emails
1777 - },
1778 - success: function(response) {
1779 - $btn.prop('disabled', false).text('Delete permanently');
1780 - closeLeadsConfirm();
1781 - if (response && response.success) {
1782 - emails.forEach(function(e) { leadsState.selected.delete(e.toLowerCase()); });
1783 - loadLeads(leadsState.page);
1784 - } else {
1785 - alert((response && response.data && response.data.message) || 'Failed to delete leads.');
1786 - }
1787 - },
1788 - error: function() {
1789 - $btn.prop('disabled', false).text('Delete permanently');
1790 - alert('Network error while deleting.');
1791 - }
1792 - });
1793 - });
1794 -
1795 - // Export dropdown
1796 - $('#mxch-leads-export-btn').on('click', function(e) {
1797 - e.stopPropagation();
1798 - $('#mxch-leads-export-menu').toggleClass('is-open');
1799 - });
1800 -
1801 - $(document).on('click', function() {
1802 - $('#mxch-leads-export-menu').removeClass('is-open');
1803 - });
1804 -
1805 - $('#mxch-leads-export-menu').on('click', function(e) { e.stopPropagation(); });
1806 -
1807 - $('#mxch-leads-export-menu button').on('click', function() {
1808 - if ($(this).prop('disabled')) return;
1809 - const scope = $(this).data('scope') || 'all';
1810 - const fields = $(this).data('fields') || 'email_and_name';
1811 - submitLeadsExport(scope, fields);
1812 - $('#mxch-leads-export-menu').removeClass('is-open');
1813 - });
1814 -
1815 - function submitLeadsExport(scope, fields) {
1816 - const $form = $('<form>', { method: 'POST', action: ajaxurl, style: 'display:none;' });
1817 - $form.append($('<input>', { type: 'hidden', name: 'action', value: 'mxchat_export_leads' }));
1818 - $form.append($('<input>', { type: 'hidden', name: 'security', value: $('#mxchat_leads_export_nonce').val() }));
1819 - $form.append($('<input>', { type: 'hidden', name: 'scope', value: scope }));
1820 - $form.append($('<input>', { type: 'hidden', name: 'fields', value: fields }));
1821 - if (scope === 'selected') {
1822 - Array.from(leadsState.selected).forEach(function(e) {
1823 - $form.append($('<input>', { type: 'hidden', name: 'emails[]', value: e }));
1824 - });
1825 - }
1826 - $form.appendTo('body').submit().remove();
1827 - }
1828 -
1829 - // Preload leads metadata on page load (for the nav badge count only) without rendering.
1830 - // We keep this light — the full fetch only runs when the tab is clicked.
1831 - $.ajax({
1832 - url: ajaxurl,
1833 - type: 'POST',
1834 - data: { action: 'mxchat_fetch_leads', page: 1, per_page: 1 },
1835 - success: function(response) {
1836 - if (response && response.success && response.stats) {
1837 - const total = response.stats.total_leads || 0;
1838 - const $badge = $('#mxch-leads-nav-badge');
1839 - if (total > 0) $badge.text(total).show();
1840 - }
1841 - }
1842 988 });
1843 989 });