/** * MxChat Transcripts Page JavaScript - v3.0 * Split-panel layout with chat list and conversation view */ jQuery(document).ready(function($) { // ========================================================================== // Sidebar Navigation // ========================================================================== // Desktop sidebar navigation $('.mxch-nav-link').on('click', function(e) { e.preventDefault(); const target = $(this).data('target'); // Update active states $('.mxch-nav-link').removeClass('active'); $(this).addClass('active'); // Show target section $('.mxch-section').removeClass('active'); $('#' + target).addClass('active'); // Reset scroll position of content area $('.mxch-content').scrollTop(0); // Also update mobile nav if open $('.mxch-mobile-nav-link').removeClass('active'); $('.mxch-mobile-nav-link[data-target="' + target + '"]').addClass('active'); // Load transcripts when switching to all-chats if (target === 'all-chats' && !transcriptsLoaded) { loadChatList(1, ''); } }); // Mobile menu toggle $('.mxch-mobile-menu-btn').on('click', function() { $('.mxch-mobile-menu').addClass('open'); $('.mxch-mobile-overlay').addClass('open'); }); // Close mobile menu $('.mxch-mobile-menu-close, .mxch-mobile-overlay').on('click', function() { $('.mxch-mobile-menu').removeClass('open'); $('.mxch-mobile-overlay').removeClass('open'); }); // Mobile navigation $('.mxch-mobile-nav-link').on('click', function(e) { e.preventDefault(); const target = $(this).data('target'); $('.mxch-mobile-nav-link').removeClass('active'); $(this).addClass('active'); $('.mxch-section').removeClass('active'); $('#' + target).addClass('active'); // Reset scroll position of content area $('.mxch-content').scrollTop(0); $('.mxch-nav-link').removeClass('active'); $('.mxch-nav-link[data-target="' + target + '"]').addClass('active'); $('.mxch-mobile-menu').removeClass('open'); $('.mxch-mobile-overlay').removeClass('open'); }); // Quick action buttons $('.mxch-quick-action-btn[data-action="view-chats"]').on('click', function() { $('.mxch-nav-link[data-target="all-chats"]').trigger('click'); }); $('.mxch-quick-action-btn[data-action="settings"]').on('click', function() { $('.mxch-nav-link[data-target="notifications"]').trigger('click'); }); // ========================================================================== // Mobile Panel Management // ========================================================================== function isMobile() { return window.innerWidth <= 782; } function showMobileConversationPanel() { if (isMobile()) { $('.mxch-chat-list-panel').addClass('panel-hidden'); $('#mxch-conversation-panel').addClass('panel-active'); $('#mxch-transcript-back-btn').show(); } } function hideMobileConversationPanel() { if (isMobile()) { $('#mxch-conversation-panel').removeClass('panel-active'); $('.mxch-chat-list-panel').removeClass('panel-hidden'); $('#mxch-transcript-back-btn').hide(); } } // Mobile back button handler $('#mxch-transcript-back-btn').on('click', function(e) { e.preventDefault(); hideMobileConversationPanel(); $('.mxch-chat-item').removeClass('active'); currentSessionId = null; }); // Handle window resize $(window).on('resize', function() { if (!isMobile()) { // Reset panel states when switching to desktop $('.mxch-chat-list-panel').removeClass('panel-hidden'); $('#mxch-conversation-panel').removeClass('panel-active'); $('#mxch-transcript-back-btn').hide(); } updateMobileViewportHeight(); }); // Fix for mobile browser address bar - sets CSS custom property for accurate viewport height function updateMobileViewportHeight() { if (isMobile()) { // Use visualViewport if available (most reliable for mobile) const vh = window.visualViewport ? window.visualViewport.height : window.innerHeight; document.documentElement.style.setProperty('--mxch-mobile-vh', vh + 'px'); } } // Update on load and viewport changes updateMobileViewportHeight(); if (window.visualViewport) { window.visualViewport.addEventListener('resize', updateMobileViewportHeight); } // ========================================================================== // Chat List - Split Panel // ========================================================================== let currentPage = 1; const perPage = 50; let totalPages = 1; let currentSessionId = null; let transcriptsLoaded = false; let selectedSessions = new Set(); let currentSortOrder = 'desc'; // newest first // Load chat list on page load loadChatList(1, ''); // Search functionality with debounce let searchTimeout; $('#mxch-search-transcripts').on('input', function() { clearTimeout(searchTimeout); const searchTerm = $(this).val().toLowerCase(); searchTimeout = setTimeout(function() { currentPage = 1; loadChatList(currentPage, searchTerm); }, 300); }); // Refresh button $('#mxch-refresh-list').on('click', function() { const $btn = $(this); $btn.addClass('spinning'); loadChatList(currentPage, $('#mxch-search-transcripts').val()); setTimeout(() => $btn.removeClass('spinning'), 500); }); // ========================================================================== // Bulk Selection & Actions // ========================================================================== // Select all checkbox $('#mxch-select-all').on('change', function() { const isChecked = $(this).is(':checked'); $('.mxch-chat-checkbox').prop('checked', isChecked); if (isChecked) { $('.mxch-chat-item').each(function() { // Use .attr() — jQuery's .data() coerces "null"/"true"/numeric strings to JS types, // which causes fetch/delete of those sessions to silently fail. selectedSessions.add($(this).attr('data-session-id')); $(this).addClass('selected'); }); $('#mxch-chat-list').addClass('selection-mode'); } else { selectedSessions.clear(); $('.mxch-chat-item').removeClass('selected'); $('#mxch-chat-list').removeClass('selection-mode'); } updateSelectionUI(); }); // Update selection UI function updateSelectionUI() { const count = selectedSessions.size; const $countEl = $('#mxch-selected-count'); const $deleteBtn = $('#mxch-delete-selected'); if (count > 0) { $countEl.text(count + ' selected').addClass('has-selection'); $deleteBtn.prop('disabled', false); $('#mxch-chat-list').addClass('selection-mode'); } else { $countEl.removeClass('has-selection'); $deleteBtn.prop('disabled', true); $('#mxch-chat-list').removeClass('selection-mode'); } // Update select all checkbox state const totalItems = $('.mxch-chat-checkbox').length; const checkedItems = $('.mxch-chat-checkbox:checked').length; $('#mxch-select-all').prop('checked', totalItems > 0 && checkedItems === totalItems); $('#mxch-select-all').prop('indeterminate', checkedItems > 0 && checkedItems < totalItems); } // Sort button — opens a small menu with 4 sort modes (plan-a5b006 adds rating sorts). $('#mxch-sort-btn').on('click', function(e) { e.stopPropagation(); const $btn = $(this); let $menu = $('#mxch-sort-menu'); if (!$menu.length) { $menu = $( '' ); $('body').append($menu); $menu.on('click', '.mxch-sort-option', function() { currentSortOrder = $(this).data('sort'); $menu.find('.mxch-sort-option').removeClass('active'); $(this).addClass('active'); $menu.hide(); loadChatList(1, $('#mxch-search-transcripts').val()); }); $(document).on('click.mxchSortMenu', function(ev) { if (!$(ev.target).closest('#mxch-sort-menu, #mxch-sort-btn').length) { $menu.hide(); } }); } $menu.find('.mxch-sort-option').removeClass('active'); $menu.find('[data-sort="' + currentSortOrder + '"]').addClass('active'); const offset = $btn.offset(); const btnHeight = $btn.outerHeight(); $menu.css({ position: 'absolute', top: (offset.top + btnHeight + 4) + 'px', left: offset.left + 'px' }).toggle(); }); // Render a rating badge for a session row. Uses inline SVG so the glyph // renders the same across OSes (emoji fonts vary). Tooltip surfaces the // optional feedback text. function renderRatingBadge(session) { const value = (session && typeof session.rating_value === 'number') ? session.rating_value : null; const feedback = (session && session.rating_feedback) ? session.rating_feedback : ''; if (value === 1) { const title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat positively'; return ''; } if (value === -1) { const title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat negatively'; return ''; } return ''; } // Delete selected button — opens the shared confirm modal with the "also delete lead" checkbox. $('#mxch-delete-selected').on('click', function() { const count = selectedSessions.size; if (count === 0) return; openTranscriptConfirm(Array.from(selectedSessions), count); }); // Transcript delete confirm (shared by bulk + individual) --------------------------- let transcriptConfirmSessionIds = []; function openTranscriptConfirm(sessionIds, count) { transcriptConfirmSessionIds = sessionIds.slice(); const n = count || sessionIds.length; $('#mxch-transcript-confirm-title').text(n === 1 ? 'Delete conversation?' : 'Delete ' + n + ' conversations?'); $('#mxch-transcript-confirm-body').text( n === 1 ? 'This removes the conversation and its messages.' : 'This removes ' + n + ' conversations and their messages.' ); $('#mxch-transcript-also-delete-lead').prop('checked', false); $('#mxch-transcript-confirm').fadeIn(120); } function closeTranscriptConfirm() { $('#mxch-transcript-confirm').fadeOut(120); transcriptConfirmSessionIds = []; } $(document).on('click', '[data-mxch-transcript-close]', closeTranscriptConfirm); $('#mxch-transcript-confirm-go').on('click', function() { const ids = transcriptConfirmSessionIds.slice(); if (!ids.length) { closeTranscriptConfirm(); return; } const alsoDeleteLead = $('#mxch-transcript-also-delete-lead').is(':checked'); closeTranscriptConfirm(); deleteMultipleSessions(ids, alsoDeleteLead); }); // Delete multiple sessions function deleteMultipleSessions(sessionIds, alsoDeleteLead) { $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mxchat_delete_chat_history', delete_session_ids: sessionIds, also_delete_lead: alsoDeleteLead ? '1' : '0', security: $('#mxchat_delete_chat_nonce').val() }, success: function(response) { try { const jsonResponse = typeof response === 'object' ? response : JSON.parse(response); if (jsonResponse.success) { // Clear selection selectedSessions.clear(); $('#mxch-select-all').prop('checked', false); updateSelectionUI(); // If current conversation was deleted, reset panel if (sessionIds.includes(currentSessionId)) { currentSessionId = null; $('#mxch-conversation-content').hide(); $('#mxch-conversation-empty').show(); $('#mxch-details-drawer').hide(); } // Reload list loadChatList(currentPage, $('#mxch-search-transcripts').val()); // The Leads tab shares this data (Chat deleted pill, nav badge, // stats) — invalidate it so switching tabs re-fetches instead of // showing stale "active lead" rows. invalidateLeadsData(); } else if (jsonResponse.error) { alert('Error: ' + jsonResponse.error); } } catch (e) { alert('An error occurred while processing the response.'); } }, error: function() { alert('An error occurred while deleting conversations.'); } }); } // Load chat list function function loadChatList(page, searchTerm) { const $container = $('#mxch-chat-list'); $container.html('
'); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mxchat_fetch_chat_history', page: page, per_page: perPage, search: searchTerm, sort_order: currentSortOrder }, success: function(response) { transcriptsLoaded = true; if (response.success && response.sessions && response.sessions.length > 0) { renderChatList(response.sessions); currentPage = response.page; totalPages = response.total_pages; updateChatCount(response.showing_start, response.showing_end, response.total_sessions); renderPagination(response.page, response.total_pages, searchTerm); } else { $container.html('

No chats found

'); updateChatCount(0, 0, 0); $('#mxch-pagination').html(''); } }, error: function() { $container.html('

Error loading chats

'); } }); } // Render chat list items function renderChatList(sessions) { const $container = $('#mxch-chat-list'); let html = ''; sessions.forEach(function(session) { const isActive = session.session_id === currentSessionId ? ' active' : ''; const isSelected = selectedSessions.has(session.session_id) ? ' selected' : ''; const isChecked = selectedSessions.has(session.session_id) ? ' checked' : ''; html += `
${escapeHtml(session.initials)}
${escapeHtml(session.display_name)}
${escapeHtml(session.preview)}
${renderRatingBadge(session)} ${escapeHtml(session.time_display)} ${session.message_count}
`; }); $container.html(html); // Attach checkbox handlers $('.mxch-chat-checkbox').on('click', function(e) { e.stopPropagation(); // Prevent triggering chat item click const $item = $(this).closest('.mxch-chat-item'); const sessionId = $item.attr('data-session-id'); if ($(this).is(':checked')) { selectedSessions.add(sessionId); $item.addClass('selected'); } else { selectedSessions.delete(sessionId); $item.removeClass('selected'); } updateSelectionUI(); }); // Attach click handlers for selecting chat $('.mxch-chat-item').on('click', function(e) { // Don't trigger if clicking on checkbox if ($(e.target).is('.mxch-chat-checkbox')) return; const sessionId = $(this).attr('data-session-id'); selectChat(sessionId); // Update active state $('.mxch-chat-item').removeClass('active'); $(this).addClass('active'); // Show conversation panel on mobile showMobileConversationPanel(); }); // Update selection UI after render updateSelectionUI(); } // Update chat count display function updateChatCount(start, end, total) { if (total === 0) { $('#mxch-chat-count').text('0 chats'); } else { $('#mxch-chat-count').text(`${start}-${end} / ${total} chats`); } } // Render pagination function renderPagination(currentPage, totalPages, searchTerm) { const $container = $('#mxch-pagination'); if (totalPages <= 1) { $container.html(''); return; } let html = '
'; if (currentPage > 1) { html += ``; } html += `${currentPage} / ${totalPages}`; if (currentPage < totalPages) { html += ``; } html += '
'; $container.html(html); // Pagination click handlers $('.mxch-page-btn').on('click', function() { const pageNum = $(this).data('page'); loadChatList(pageNum, searchTerm); }); } // ========================================================================== // Conversation Panel // ========================================================================== // Select and load a chat conversation function selectChat(sessionId) { currentSessionId = sessionId; // Reset translation state when selecting new chat if (typeof resetTranslationState === 'function') { resetTranslationState(); } // Show loading in conversation panel $('#mxch-conversation-empty').hide(); $('#mxch-conversation-content').show(); $('#mxch-messages-area').html('
Loading conversation...
'); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mxchat_fetch_conversation', session_id: sessionId }, success: function(response) { if (response.success) { renderConversation(response); // Load saved translation after rendering if (typeof loadSavedTranslation === 'function') { setTimeout(function() { loadSavedTranslation(sessionId); }, 100); } } else { $('#mxch-messages-area').html('
Failed to load conversation
'); } }, error: function() { $('#mxch-messages-area').html('
Error loading conversation
'); } }); } // Render conversation content function renderConversation(data) { // Update header $('#mxch-user-avatar span').text(data.user.initials); $('#mxch-user-name').text(data.user.name); $('#mxch-user-meta').text(data.user.sub); // Update details drawer $('#mxch-detail-messages').text(data.message_count); $('#mxch-detail-started').text(data.started); if (data.page.url) { $('#mxch-detail-page').html(`${escapeHtml(data.page.title || data.page.url)}`); } else { $('#mxch-detail-page').text('-'); } if (data.user.email) { $('#mxch-detail-email').text(data.user.email); $('#mxch-detail-email-row').show(); } else { $('#mxch-detail-email-row').hide(); } // Feedback row — .text() (not .html()) for XSS-safe display of user-submitted text. // rating_feedback is already sanitize_text_field()'d + mb_substr(200) on save. var feedback = (data && data.rating_feedback) ? String(data.rating_feedback).trim() : ''; if (feedback) { $('#mxch-detail-feedback').text(feedback); $('#mxch-detail-feedback-row').show(); } else { $('#mxch-detail-feedback-row').hide(); } // Clicked links if (data.clicked_urls && data.clicked_urls.length > 0) { let linksHtml = ''; data.clicked_urls.forEach(function(url) { linksHtml += `${escapeHtml(url)}`; }); $('#mxch-clicked-links').html(linksHtml); $('#mxch-clicked-section').show(); } else { $('#mxch-clicked-section').hide(); } // Render messages let messagesHtml = ''; data.messages.forEach(function(msg) { if (msg.is_user) { messagesHtml += `
${msg.content}
${escapeHtml(msg.timestamp)}
`; } else { // Live-agent replies (role 'agent') get their own label and never // carry a Sources link; bot/assistant rows render exactly as before. const isAgent = !!msg.is_agent; const ragLink = (!isAgent && msg.has_rag) ? `Sources` : ''; const senderLabel = isAgent ? 'Live Agent' : 'AI Assistant'; messagesHtml += `
${senderLabel} ${ragLink}
${msg.content}
${escapeHtml(msg.timestamp)}
`; } }); $('#mxch-messages-area').html(messagesHtml); // Scroll to bottom const $area = $('#mxch-messages-area'); $area.scrollTop($area[0].scrollHeight); // Attach RAG link handlers $('.mxch-rag-link').on('click', function(e) { e.preventDefault(); const messageId = $(this).data('message-id'); if (messageId) { openRagContextModal(messageId); } }); } // Toggle details drawer $('#mxch-toggle-details').on('click', function() { const $drawer = $('#mxch-details-drawer'); const $btn = $(this); if ($drawer.is(':visible')) { $drawer.slideUp(200); $btn.removeClass('active'); } else { $drawer.slideDown(200); $btn.addClass('active'); } }); // Delete current chat — opens the shared confirm modal. $('#mxch-delete-current').on('click', function() { if (!currentSessionId) return; openTranscriptConfirm([currentSessionId], 1); }); // Delete session function function deleteSession(sessionId, alsoDeleteLead) { $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mxchat_delete_chat_history', delete_session_ids: [sessionId], also_delete_lead: alsoDeleteLead ? '1' : '0', security: $('#mxchat_delete_chat_nonce').val() }, success: function(response) { try { const jsonResponse = typeof response === 'object' ? response : JSON.parse(response); if (jsonResponse.success) { // Reset conversation panel currentSessionId = null; $('#mxch-conversation-content').hide(); $('#mxch-conversation-empty').show(); $('#mxch-details-drawer').hide(); // Reload list loadChatList(currentPage, $('#mxch-search-transcripts').val()); } else if (jsonResponse.error) { alert('Error: ' + jsonResponse.error); } } catch (e) { alert('An error occurred while processing the response.'); } }, error: function() { alert('An error occurred while deleting the conversation.'); } }); } // ========================================================================== // Export Functionality // ========================================================================== $('#mxch-export-btn, #mxch-export-current').on('click', function() { const $button = $(this); $button.prop('disabled', true).addClass('loading'); const $form = $('
', { method: 'post', action: ajaxurl }); $form.append($('', { type: 'hidden', name: 'action', value: 'mxchat_export_transcripts' })); $form.append($('', { type: 'hidden', name: 'security', value: mxchatAdmin.export_nonce })); $form.appendTo('body').submit(); setTimeout(function() { $button.prop('disabled', false).removeClass('loading'); }, 2000); }); // ========================================================================== // Translation Functionality // ========================================================================== // Store original messages for reverting let originalMessages = null; let isTranslated = false; let currentTranslationLang = null; // Load saved language preference from localStorage const savedLang = localStorage.getItem('mxch_translate_lang'); if (savedLang) { $('#mxch-translate-lang').val(savedLang); } // Save language preference when changed $('#mxch-translate-lang').on('change', function() { localStorage.setItem('mxch_translate_lang', $(this).val()); }); // Apply translations to messages function applyTranslations(translations) { // Store original messages if not already stored if (!originalMessages) { originalMessages = []; $('#mxch-messages-area .mxch-message-bubble').each(function() { originalMessages.push($(this).html()); }); } // Apply translations translations.forEach(function(item) { const $bubble = $('#mxch-messages-area .mxch-message-bubble').eq(item.index); if ($bubble.length) { $bubble.html(item.translated); $bubble.addClass('translated'); } }); isTranslated = true; $('#mxch-show-original-btn').show(); } // Load saved translation for current session function loadSavedTranslation(sessionId) { $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mxchat_get_transcript_translation', session_id: sessionId }, success: function(response) { if (response.success && response.has_translation) { currentTranslationLang = response.language; applyTranslations(response.translations); // Update language selector to show saved language $('#mxch-translate-lang').val(response.language); } } }); } // Translate button click handler $('#mxch-translate-btn').on('click', function() { if (!currentSessionId) return; const $btn = $(this); const targetLang = $('#mxch-translate-lang').val(); // Disable button and show loading state $btn.prop('disabled', true); $btn.find('.mxch-translate-text').text('Translating...'); $btn.find('svg').addClass('mxch-translate-spinner'); // Store original messages before translation if (!originalMessages) { originalMessages = []; $('#mxch-messages-area .mxch-message-bubble').each(function() { originalMessages.push($(this).html()); }); } // If already translated, restore originals first before re-translating if (isTranslated) { $('#mxch-messages-area .mxch-message-bubble').each(function(index) { if (originalMessages[index]) { $(this).html(originalMessages[index]); $(this).removeClass('translated'); } }); } // Collect all message content (from originals) const messages = []; originalMessages.forEach(function(html, index) { // Create temp element to get text content const $temp = $('
').html(html); messages.push({ index: index, content: $temp.text().trim() }); }); // Send translation request $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mxchat_translate_messages', session_id: currentSessionId, target_lang: targetLang, messages: JSON.stringify(messages), security: mxchatAdmin.translate_nonce || '' }, success: function(response) { if (response.success && response.translations) { currentTranslationLang = response.language; applyTranslations(response.translations); $btn.find('.mxch-translate-text').text('Translate'); } else { alert(response.error || 'Translation failed. Please try again.'); $btn.find('.mxch-translate-text').text('Translate'); } }, error: function() { alert('Translation request failed. Please try again.'); $btn.find('.mxch-translate-text').text('Translate'); }, complete: function() { $btn.prop('disabled', false); $btn.find('svg').removeClass('mxch-translate-spinner'); } }); }); // Show original button click handler $('#mxch-show-original-btn').on('click', function() { if (!originalMessages) return; // Restore original messages $('#mxch-messages-area .mxch-message-bubble').each(function(index) { if (originalMessages[index]) { $(this).html(originalMessages[index]); $(this).removeClass('translated'); } }); isTranslated = false; $(this).hide(); }); // Reset translation state (called when selecting new chat) function resetTranslationState() { originalMessages = null; isTranslated = false; currentTranslationLang = null; $('#mxch-show-original-btn').hide(); } // Make functions available to selectChat window.resetTranslationState = resetTranslationState; window.loadSavedTranslation = loadSavedTranslation; // ========================================================================== // RAG Context Modal (Sources & Actions Tabs) // ========================================================================== function openRagContextModal(messageId) { const $modal = $('#mxch-rag-modal'); const $loading = $modal.find('.mxch-rag-loading'); const $sourcesContent = $modal.find('.mxch-rag-content'); const $actionsContent = $modal.find('.mxch-actions-content'); // Reset to Sources tab $modal.find('.mxch-context-tab').removeClass('active'); $modal.find('.mxch-context-tab[data-tab="sources"]').addClass('active'); $('#mxch-tab-sources').show(); $('#mxch-tab-actions').hide(); // Reset badge counts $('#mxch-sources-count, #mxch-actions-count').hide().text('0'); $modal.fadeIn(200); $loading.show(); $sourcesContent.html(''); $actionsContent.html(''); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mxchat_get_rag_context', message_id: messageId }, success: function(response) { $loading.hide(); if (response.success && response.data) { // Render sources tab renderRagContext(response.data, $sourcesContent); // Render actions tab renderActionsContext(response.data, $actionsContent); // Update badge counts const sourcesCount = response.data.top_matches ? response.data.top_matches.length : 0; // 470f68: the Actions tab now carries two mechanisms — tools // that ran and trigger phrases that scored. Badge counts both. const toolCallsCount = response.data.tool_calls ? response.data.tool_calls.length : 0; const actionsCount = (response.data.action_analysis ? response.data.action_analysis.length : 0) + toolCallsCount; if (sourcesCount > 0) { $('#mxch-sources-count').text(sourcesCount).show(); } if (actionsCount > 0) { $('#mxch-actions-count').text(actionsCount).show(); } } else { $sourcesContent.html('
Unable to load document context.
'); $actionsContent.html('
No action data available.
'); } }, error: function() { $loading.hide(); $sourcesContent.html('
Error loading context. Please try again.
'); $actionsContent.html('
Error loading context. Please try again.
'); } }); } // Tab switching $(document).on('click', '.mxch-context-tab', function() { const $tab = $(this); const tabName = $tab.data('tab'); // Update active tab $('.mxch-context-tab').removeClass('active'); $tab.addClass('active'); // Show/hide content $('.mxch-tab-content').hide(); $('#mxch-tab-' + tabName).show(); }); function renderRagContext(data, $container) { let html = ''; // 67fc92: "the KB was searched and nothing matched" and "nothing was // recorded for this row" are different facts. Retrieval keys present // means the search ran and was recorded (top_matches may be empty); // no retrieval keys means there is simply no record (older rows, or // turns that never consulted the knowledge base). const retrievalRecorded = typeof data.total_documents_checked !== 'undefined' || (data.top_matches && data.top_matches.length > 0); if (!retrievalRecorded) { html += '

No retrieval data was recorded for this response.

'; $container.html(html); return; } // Hybrid keyword boost (38ffa1): rows carry matched_via + fused_rank // when hybrid retrieval was on for this response. Cosine % stays the // anchor; the chip explains WHY a low-% row ranked high. const topMatches = data.top_matches || []; const hybridOn = topMatches.some(function(m) { return m.matched_via; }); html += '
'; html += '
Knowledge Base: ' + escapeHtml(data.knowledge_base_type || 'WordPress Database') + '
'; html += '
Similarity Threshold: ' + Math.round((data.similarity_threshold || 0.35) * 100) + '%
'; html += '
Documents Checked: ' + (data.total_documents_checked || 0) + '
'; if (hybridOn) { html += '
Retrieval: Hybrid
'; } html += '
'; if (topMatches.length === 0) { html += '

The knowledge base was searched — no documents matched this response.

'; $container.html(html); return; } const groupedByUrl = {}; data.top_matches.forEach(function(match) { const url = match.source_display || 'Unknown'; if (!groupedByUrl[url]) { groupedByUrl[url] = { url: url, isUrl: url.startsWith('http'), bestScore: 0, usedForContext: false, matchedChunks: [], bestFusedRank: Infinity, viaSet: {} }; } if (match.similarity_percentage > groupedByUrl[url].bestScore) { groupedByUrl[url].bestScore = match.similarity_percentage; } if (match.used_for_context) { groupedByUrl[url].usedForContext = true; } if (match.fused_rank && match.fused_rank < groupedByUrl[url].bestFusedRank) { groupedByUrl[url].bestFusedRank = match.fused_rank; } if (match.matched_via) { groupedByUrl[url].viaSet[match.matched_via] = true; } groupedByUrl[url].matchedChunks.push({ chunkIndex: match.chunk_index, score: match.similarity_percentage, usedForContext: match.used_for_context }); }); // Hybrid on: order by fused rank (rows without one sort last); // otherwise by cosine, exactly as before. const urlGroups = Object.values(groupedByUrl).sort(function(a, b) { if (hybridOn && a.bestFusedRank !== b.bestFusedRank) { return a.bestFusedRank - b.bestFusedRank; } return b.bestScore - a.bestScore; }); const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length; const chunksInfo = data.total_chunks_used > 0 ? data.total_chunks_used + ' chunks sent to AI' : ''; html += '
'; html += '

Retrieved Documents

'; html += '

' + usedUrlCount + ' source' + (usedUrlCount === 1 ? '' : 's') + ' used for response' + (chunksInfo ? ' · ' + chunksInfo : '') + '

'; urlGroups.forEach(function(group) { const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below'; const statusIcon = group.usedForContext ? '✓' : '✗'; const statusLabel = group.usedForContext ? 'Used' : 'Not Used'; html += '
'; html += '
'; html += '' + group.bestScore + '%'; if (hybridOn) { const vias = Object.keys(group.viaSet); if (vias.length) { const viaLabel = (vias.length > 1 || vias[0] === 'both') ? 'Both' : (vias[0] === 'keyword' ? 'Keyword' : 'Vector'); html += '' + viaLabel + ''; } } if (group.matchedChunks.length > 1) { html += '' + group.matchedChunks.length + ' chunks'; } html += '' + statusIcon + ' ' + statusLabel + ''; html += '
'; html += '
'; if (group.isUrl) { html += '' + escapeHtml(group.url) + ''; } else { html += escapeHtml(group.url); } html += '
'; html += '
'; }); html += '
'; $container.html(html); } // AI Tools trace (470f68): one card per tool EXECUTION for this message. // Rendered ABOVE the trigger-phrase scores — a tool that actually ran // outranks a phrase that merely scored. function renderToolCalls(toolCalls) { const failed = toolCalls.filter(function(t) { return !t.ok; }).length; let html = '
'; html += '
Tools Used: ' + toolCalls.length + '
'; if (failed > 0) { html += '
Failed: ' + failed + '
'; } html += '
'; html += '
'; html += '

AI Tools

'; html += '

Tools the assistant chose and ran for this message, in order.

'; toolCalls.forEach(function(tool) { const ok = !!tool.ok; const cardClass = ok ? 'mxch-rag-match-used' : 'mxch-tool-failed'; const statusClass = ok ? 'status-used' : 'status-failed'; const statusIcon = ok ? '✓' : '✗'; const statusLabel = ok ? 'Ran' : 'Failed'; html += '
'; html += '
'; html += ''; if (typeof tool.ms === 'number') { html += '' + tool.ms + ' ms'; } html += '' + statusIcon + ' ' + statusLabel + ''; html += '
'; html += '
'; html += '
' + escapeHtml(tool.label || tool.name) + '
'; html += '
Tool: ' + escapeHtml(tool.name) + '
'; html += '
'; if (!ok && tool.error) { html += '
' + escapeHtml(tool.error) + '
'; } if (tool.args_redacted === 'sensitive') { html += '
Arguments not recorded — this tool is marked sensitive.
'; } else if (tool.args_excerpt) { html += '
Arguments'; html += '
' + escapeHtml(tool.args_excerpt) + '
'; } html += '
'; }); html += '
'; return html; } function renderActionsContext(data, $container) { let html = ''; const toolCalls = (data.tool_calls && data.tool_calls.length) ? data.tool_calls : []; const actions = (data.action_analysis && data.action_analysis.length) ? data.action_analysis : []; // 95d79d's empty state now shows only when NEITHER mechanism produced // anything — the string itself is unchanged. if (toolCalls.length === 0 && actions.length === 0) { html += '

No trigger-phrase analysis for this message.

This panel shows how your Trigger Phrases scored — it stays empty if you have none enabled for this bot, or if the answer came from AI Tools, which don\'t produce similarity scores. It\'s recorded when the answer is generated, so it won\'t appear on older conversations.

'; $container.html(html); return; } if (toolCalls.length > 0) { html += renderToolCalls(toolCalls); } if (actions.length === 0) { $container.html(html); return; } const triggeredAction = actions.find(a => a.triggered); const actionsAboveThreshold = actions.filter(a => a.above_threshold).length; // Summary section html += '
'; html += '
Actions Evaluated: ' + actions.length + '
'; html += '
Above Threshold: ' + actionsAboveThreshold + '
'; if (triggeredAction) { html += '
Triggered: ' + escapeHtml(triggeredAction.intent_label) + '
'; } html += '
'; // Actions list html += '
'; html += '

Action Scores

'; html += '

Showing all evaluated actions sorted by similarity score

'; actions.forEach(function(action) { let cardClass = 'mxch-rag-match-below'; let statusIcon = '✗'; let statusLabel = 'Below Threshold'; if (action.triggered) { cardClass = 'mxch-action-triggered'; statusIcon = '⚡'; statusLabel = 'Triggered'; } else if (action.above_threshold) { cardClass = 'mxch-rag-match-used'; statusIcon = '✓'; statusLabel = 'Above Threshold'; } html += '
'; html += '
'; html += '' + action.similarity_percentage + '%'; html += 'Threshold: ' + action.threshold_percentage + '%'; html += '' + statusIcon + ' ' + statusLabel + ''; html += '
'; html += '
'; html += '
' + escapeHtml(action.intent_label) + '
'; html += '
Callback: ' + escapeHtml(action.callback_function) + '
'; html += '
'; // Score bar visualization const scoreBarWidth = Math.min(action.similarity_percentage, 100); const thresholdPos = Math.min(action.threshold_percentage, 100); html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; }); html += '
'; $container.html(html); } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Close RAG modal $('.mxch-modal-close').on('click', function() { $(this).closest('.mxch-modal-overlay').fadeOut(200); }); $('.mxch-modal-overlay').on('click', function(e) { if ($(e.target).is('.mxch-modal-overlay')) { $(this).fadeOut(200); } }); $(document).on('keydown', function(e) { if (e.key === 'Escape') { $('.mxch-modal-overlay').fadeOut(200); } }); // ========================================================================== // Activity Chart // ========================================================================== // Simple chart implementation (no external dependencies) class SimpleChart { constructor(canvas, config) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.config = config; this.padding = { top: 20, right: 20, bottom: 40, left: 50 }; this.render(); } destroy() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } render() { const dpr = window.devicePixelRatio || 1; const rect = this.canvas.getBoundingClientRect(); this.canvas.width = rect.width * dpr; this.canvas.height = rect.height * dpr; this.ctx.scale(dpr, dpr); this.canvas.style.width = rect.width + 'px'; this.canvas.style.height = rect.height + 'px'; const width = rect.width - this.padding.left - this.padding.right; const height = rect.height - this.padding.top - this.padding.bottom; // Find max value let maxValue = 0; this.config.datasets.forEach(dataset => { const max = Math.max(...dataset.data); if (max > maxValue) maxValue = max; }); // Add some padding to max value maxValue = Math.ceil(maxValue * 1.1); if (maxValue === 0) maxValue = 10; // Draw grid lines this.ctx.strokeStyle = '#e5e7eb'; this.ctx.lineWidth = 1; const gridLines = 5; for (let i = 0; i <= gridLines; i++) { const y = this.padding.top + (height / gridLines) * i; this.ctx.beginPath(); this.ctx.moveTo(this.padding.left, y); this.ctx.lineTo(this.padding.left + width, y); this.ctx.stroke(); // Draw y-axis labels const value = maxValue - (maxValue / gridLines) * i; this.ctx.fillStyle = '#6b7280'; this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; this.ctx.textAlign = 'right'; this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4); } // Draw datasets this.config.datasets.forEach(dataset => { const points = []; const xStep = width / (this.config.labels.length - 1 || 1); dataset.data.forEach((value, index) => { const x = this.padding.left + (xStep * index); const y = this.padding.top + height - (value / maxValue * height); points.push({ x, y, value }); }); // Draw filled area if (dataset.fill && dataset.backgroundColor) { this.ctx.fillStyle = dataset.backgroundColor; this.ctx.beginPath(); this.ctx.moveTo(points[0].x, this.padding.top + height); points.forEach(point => { this.ctx.lineTo(point.x, point.y); }); this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height); this.ctx.closePath(); this.ctx.fill(); } // Draw line this.ctx.strokeStyle = dataset.borderColor; this.ctx.lineWidth = 3; this.ctx.lineCap = 'round'; this.ctx.lineJoin = 'round'; this.ctx.beginPath(); points.forEach((point, index) => { if (index === 0) { this.ctx.moveTo(point.x, point.y); } else { this.ctx.lineTo(point.x, point.y); } }); this.ctx.stroke(); // Draw points points.forEach(point => { this.ctx.fillStyle = '#ffffff'; this.ctx.beginPath(); this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2); this.ctx.fill(); this.ctx.strokeStyle = dataset.borderColor; this.ctx.lineWidth = 2; this.ctx.stroke(); }); }); // Draw x-axis labels const xStep = width / (this.config.labels.length - 1 || 1); this.ctx.fillStyle = '#6b7280'; this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; this.ctx.textAlign = 'center'; this.config.labels.forEach((label, index) => { const x = this.padding.left + (xStep * index); this.ctx.fillText(label, x, this.padding.top + height + 20); }); } } // Initialize activity chart function initActivityChart() { console.log('[MxChat Chart] initActivityChart called'); const canvas = document.getElementById('mxchat-activity-chart'); console.log('[MxChat Chart] Canvas element:', canvas); if (!canvas) { console.log('[MxChat Chart] Canvas not found, aborting'); return; } console.log('[MxChat Chart] mxchatChartData exists:', typeof mxchatChartData !== 'undefined'); if (typeof mxchatChartData === 'undefined') { console.log('[MxChat Chart] mxchatChartData is undefined, aborting'); return; } console.log('[MxChat Chart] Raw mxchatChartData:', mxchatChartData); // Check if chart already exists and destroy it if (canvas.chartInstance) { canvas.chartInstance.destroy(); } const ctx = canvas.getContext('2d'); console.log('[MxChat Chart] Canvas context:', ctx); console.log('[MxChat Chart] Canvas dimensions:', canvas.getBoundingClientRect()); // Create gradient for chats line const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300); chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)'); chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)'); // Create gradient for messages line const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300); messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)'); messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)'); // Convert wp_localize_script objects to arrays (WordPress converts indexed arrays to objects) const labels = Object.values(mxchatChartData.labels); const chatsData = Object.values(mxchatChartData.chats).map(Number); const messagesData = Object.values(mxchatChartData.messages).map(Number); console.log('[MxChat Chart] Processed labels:', labels); console.log('[MxChat Chart] Processed chatsData:', chatsData); console.log('[MxChat Chart] Processed messagesData:', messagesData); // Create chart try { canvas.chartInstance = new SimpleChart(canvas, { labels: labels, datasets: [ { label: 'Chats', data: chatsData, borderColor: '#667eea', backgroundColor: chatsGradient, fill: true }, { label: 'Messages', data: messagesData, borderColor: '#764ba2', backgroundColor: messagesGradient, fill: true } ] }); console.log('[MxChat Chart] Chart created successfully'); } catch (error) { console.error('[MxChat Chart] Error creating chart:', error); } } // Initialize chart on page load (dashboard is shown by default) setTimeout(function() { initActivityChart(); }, 100); // Reinitialize chart on window resize let resizeTimeout; $(window).on('resize', function() { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(function() { initActivityChart(); }, 250); }); // ========================================================================== // Leads Tab // ========================================================================== const leadsState = { loaded: false, page: 1, perPage: 25, totalPages: 1, totalCount: 0, selected: new Set(), filters: { search: '', dateRange: 'all', status: 'all', pageUrl: '', pageTitle: '' }, pendingDelete: [], leadsRows: [] // last-rendered rows for quick lookup }; function $leads() { return $('#leads'); } // Called after a transcript delete from the All Chats side. Marks the Leads tab // data stale so the next tab visit re-fetches, and refreshes immediately if the // Leads tab happens to already be visible. function invalidateLeadsData() { leadsState.loaded = false; if ($('#leads').hasClass('active')) { loadLeads(1); } } function escapeHtmlLeads(s) { if (s === null || typeof s === 'undefined') return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function leadsFiltersActive() { const f = leadsState.filters; return f.search !== '' || f.dateRange !== 'all' || f.status !== 'all' || f.pageUrl !== ''; } function updateClearFiltersButton() { if (leadsFiltersActive()) { $('#mxch-leads-clear-filters').show(); } else { $('#mxch-leads-clear-filters').hide(); } } function setPageFilterChip(url, title) { leadsState.filters.pageUrl = url || ''; leadsState.filters.pageTitle = title || url || ''; const $chip = $('#mxch-leads-active-page-filter'); if (url) { $chip.find('.mxch-leads-page-chip-label').text('Page: ' + (title || url)); $chip.show(); } else { $chip.hide(); } updateClearFiltersButton(); } function loadLeads(page) { if (typeof page === 'number') leadsState.page = page; const $tbody = $('#mxch-leads-tbody'); $tbody.html(''); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mxchat_fetch_leads', page: leadsState.page, per_page: leadsState.perPage, search: leadsState.filters.search, date_range: leadsState.filters.dateRange, status: leadsState.filters.status, page_url: leadsState.filters.pageUrl }, success: function(response) { leadsState.loaded = true; if (!response || !response.success) { $tbody.html('Error loading leads'); return; } leadsState.totalPages = response.total_pages || 1; leadsState.totalCount = response.total_count || 0; leadsState.leadsRows = response.leads || []; renderLeadsStats(response.stats || {}); renderLeadsTopPages(response.top_pages || []); renderLeadsTable(response.leads || []); renderLeadsCount(response.showing_start, response.showing_end, response.total_count); renderLeadsPagination(response.page, response.total_pages); // Nav badge if (response.stats && typeof response.stats.total_leads === 'number') { const $badge = $('#mxch-leads-nav-badge'); if (response.stats.total_leads > 0) { $badge.text(response.stats.total_leads).show(); } else { $badge.hide(); } } }, error: function() { $tbody.html('Error loading leads'); } }); } function renderLeadsStats(stats) { $('#mxch-leads-stat-total').text(stats.total_leads || 0); $('#mxch-leads-stat-new').text(stats.new_this_week || 0); $('#mxch-leads-stat-avg').text(stats.avg_convos || 0); const pct = stats.orphan_pct || 0; $('#mxch-leads-stat-orphan').text(pct + '%'); const orphanCount = stats.orphan_count || 0; $('#mxch-leads-stat-orphan-sub').text(orphanCount + (orphanCount === 1 ? ' lead captured but never chatted' : ' leads captured but never chatted')); } function renderLeadsTopPages(pages) { const $wrap = $('#mxch-leads-toppages-list'); if (!pages || pages.length === 0) { $wrap.html('
No page data yet.
'); return; } let html = ''; pages.forEach(function(p) { const isActive = leadsState.filters.pageUrl === p.url ? ' is-active' : ''; html += ` `; }); $wrap.html(html); } function renderLeadsTable(rows) { const $tbody = $('#mxch-leads-tbody'); if (!rows || rows.length === 0) { $tbody.html(`

No leads match the current filters.

`); return; } let html = ''; rows.forEach(function(r) { const emailKey = (r.email || '').toLowerCase(); const isChecked = leadsState.selected.has(emailKey) ? ' checked' : ''; // Status: 'active' (has conversations), 'chat_deleted' (admin removed the chat), 'orphan' (no chat ever). const status = r.status || (r.is_orphan ? 'orphan' : 'active'); const isOrphan = (status === 'orphan'); const isChatDeleted = (status === 'chat_deleted'); const nameLine = r.name ? `${escapeHtmlLeads(r.name)}` : ''; const leadCell = `
${escapeHtmlLeads(r.email)} ${nameLine}
`; let countCell; if (isOrphan) { countCell = `Orphan`; } else if (isChatDeleted) { countCell = `Chat deleted`; } else { countCell = `${r.conversation_count}`; } const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : '')); const pageCell = r.top_page_url ? `${escapeHtmlLeads(r.top_page_title || r.top_page_url)}` : ''; // Consent (b062c4): 'yes'/'no' are recorded decisions; '' means the // capture predates the checkbox — "Not recorded", never "No". let consentCell; if (r.consent === 'yes' || r.consent === 'no') { const consentTitle = (r.consent_at ? 'Recorded ' + r.consent_at : '') + (r.consent_label ? (r.consent_at ? ' — ' : '') + '"' + r.consent_label + '"' : ''); consentCell = `${r.consent === 'yes' ? 'Yes' : 'No'}`; } else { consentCell = 'Not recorded'; } // View Convo only for active leads (orphans and chat_deleted have no viewable session). const viewBtn = (status === 'active' && r.latest_session_id) ? `` : ''; const deleteBtn = ``; const rowStateClass = isOrphan ? ' is-orphan' : (isChatDeleted ? ' is-chat-deleted' : ''); html += ` ${leadCell} ${countCell} ${lastCell} ${pageCell} ${consentCell} ${viewBtn}${deleteBtn} `; }); $tbody.html(html); updateLeadsSelectionUI(); } function renderLeadsCount(start, end, total) { if (!total) { $('#mxch-leads-count').text('0 leads'); } else { $('#mxch-leads-count').text(start + '-' + end + ' / ' + total + ' leads'); } } function renderLeadsPagination(currentPage, totalPages) { const $c = $('#mxch-leads-pagination'); if (!totalPages || totalPages <= 1) { $c.html(''); return; } let html = '
'; if (currentPage > 1) { html += ``; } html += `${currentPage} / ${totalPages}`; if (currentPage < totalPages) { html += ``; } html += '
'; $c.html(html); } function updateLeadsSelectionUI() { const count = leadsState.selected.size; const $countEl = $('#mxch-leads-selected-count'); const $del = $('#mxch-leads-delete-selected'); if (count > 0) { $countEl.text(count + ' selected').addClass('has-selection'); $del.prop('disabled', false); } else { $countEl.text('0').removeClass('has-selection'); $del.prop('disabled', true); } // Selected-scope export menu items $('#mxch-leads-export-menu button[data-scope="selected"]').prop('disabled', count === 0); // Select-all checkbox state const $checks = $('.mxch-leads-rowcheck'); const checked = $checks.filter(':checked').length; const total = $checks.length; $('#mxch-leads-select-all').prop('checked', total > 0 && checked === total); $('#mxch-leads-select-all').prop('indeterminate', checked > 0 && checked < total); } // Trigger leads load when switching to the tab (works alongside the main nav handler above). $('.mxch-nav-link[data-target="leads"], .mxch-mobile-nav-link[data-target="leads"]').on('click', function() { if (!leadsState.loaded) { loadLeads(1); } }); // Filter: search (debounced) let leadsSearchTimer; $('#mxch-leads-search').on('input', function() { clearTimeout(leadsSearchTimer); const val = $(this).val(); leadsSearchTimer = setTimeout(function() { leadsState.filters.search = (val || '').trim(); updateClearFiltersButton(); loadLeads(1); }, 300); }); // Filter: date range $('#mxch-leads-date-range').on('change', function() { leadsState.filters.dateRange = $(this).val(); updateClearFiltersButton(); loadLeads(1); }); // Filter: status $('#mxch-leads-status').on('change', function() { leadsState.filters.status = $(this).val(); updateClearFiltersButton(); loadLeads(1); }); // Clear filters $('#mxch-leads-clear-filters').on('click', function() { leadsState.filters = { search: '', dateRange: 'all', status: 'all', pageUrl: '', pageTitle: '' }; $('#mxch-leads-search').val(''); $('#mxch-leads-date-range').val('all'); $('#mxch-leads-status').val('all'); setPageFilterChip('', ''); loadLeads(1); }); // Remove page chip $leads().on('click', '.mxch-leads-page-chip-remove', function() { setPageFilterChip('', ''); loadLeads(1); }); // Top Pages click -> set filter $leads().on('click', '.mxch-leads-toppage-row', function() { const url = $(this).data('url') || ''; const title = $(this).data('title') || ''; setPageFilterChip(url, title); loadLeads(1); }); // Pagination click $leads().on('click', '#mxch-leads-pagination .mxch-page-btn', function() { const p = parseInt($(this).data('page'), 10); if (p > 0) loadLeads(p); }); // Select-all $('#mxch-leads-select-all').on('change', function() { const on = $(this).is(':checked'); $('.mxch-leads-rowcheck').prop('checked', on); $('.mxch-leads-row').each(function() { const email = ($(this).data('email') || '').toString().toLowerCase(); if (on) { leadsState.selected.add(email); } else { leadsState.selected.delete(email); } }); updateLeadsSelectionUI(); }); // Row checkbox $leads().on('change', '.mxch-leads-rowcheck', function() { const email = ($(this).closest('.mxch-leads-row').data('email') || '').toString().toLowerCase(); if ($(this).is(':checked')) { leadsState.selected.add(email); } else { leadsState.selected.delete(email); } updateLeadsSelectionUI(); }); // View convo -> jump to All Chats tab and open the session $leads().on('click', '.mxch-leads-view', function() { const sid = $(this).attr('data-session-id'); if (!sid) return; $('.mxch-nav-link[data-target="all-chats"]').trigger('click'); // selectChat is defined earlier in this closure if (typeof selectChat === 'function') { setTimeout(function() { selectChat(sid); }, 30); } }); // Row delete -> confirm for one $leads().on('click', '.mxch-leads-delete-row', function() { const email = $(this).data('email'); if (!email) return; openLeadsConfirm([String(email)]); }); // Bulk delete -> confirm for N $('#mxch-leads-delete-selected').on('click', function() { if (leadsState.selected.size === 0) return; openLeadsConfirm(Array.from(leadsState.selected)); }); function openLeadsConfirm(emails) { leadsState.pendingDelete = emails; const count = emails.length; const msg = count === 1 ? 'Delete lead "' + emails[0] + '" and all of their conversations?' : 'Delete ' + count + ' leads and all of their conversations?'; $('#mxch-leads-confirm-body').text(msg); $('#mxch-leads-confirm').fadeIn(120); } function closeLeadsConfirm() { $('#mxch-leads-confirm').fadeOut(120); leadsState.pendingDelete = []; } $leads().on('click', '[data-mxch-leads-close]', closeLeadsConfirm); $('#mxch-leads-confirm-go').on('click', function() { const emails = leadsState.pendingDelete.slice(); if (!emails.length) { closeLeadsConfirm(); return; } const $btn = $(this).prop('disabled', true).text('Deleting...'); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mxchat_delete_leads', security: $('#mxchat_leads_delete_nonce').val(), emails: emails }, success: function(response) { $btn.prop('disabled', false).text('Delete permanently'); closeLeadsConfirm(); if (response && response.success) { emails.forEach(function(e) { leadsState.selected.delete(e.toLowerCase()); }); loadLeads(leadsState.page); } else { alert((response && response.data && response.data.message) || 'Failed to delete leads.'); } }, error: function() { $btn.prop('disabled', false).text('Delete permanently'); alert('Network error while deleting.'); } }); }); // Export dropdown $('#mxch-leads-export-btn').on('click', function(e) { e.stopPropagation(); $('#mxch-leads-export-menu').toggleClass('is-open'); }); $(document).on('click', function() { $('#mxch-leads-export-menu').removeClass('is-open'); }); $('#mxch-leads-export-menu').on('click', function(e) { e.stopPropagation(); }); $('#mxch-leads-export-menu button').on('click', function() { if ($(this).prop('disabled')) return; const scope = $(this).data('scope') || 'all'; const fields = $(this).data('fields') || 'email_and_name'; submitLeadsExport(scope, fields); $('#mxch-leads-export-menu').removeClass('is-open'); }); function submitLeadsExport(scope, fields) { const $form = $('', { method: 'POST', action: ajaxurl, style: 'display:none;' }); $form.append($('', { type: 'hidden', name: 'action', value: 'mxchat_export_leads' })); $form.append($('', { type: 'hidden', name: 'security', value: $('#mxchat_leads_export_nonce').val() })); $form.append($('', { type: 'hidden', name: 'scope', value: scope })); $form.append($('', { type: 'hidden', name: 'fields', value: fields })); if (scope === 'selected') { Array.from(leadsState.selected).forEach(function(e) { $form.append($('', { type: 'hidden', name: 'emails[]', value: e })); }); } $form.appendTo('body').submit().remove(); } // Preload leads metadata on page load (for the nav badge count only) without rendering. // We keep this light — the full fetch only runs when the tab is clicked. $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mxchat_fetch_leads', page: 1, per_page: 1 }, success: function(response) { if (response && response.success && response.stats) { const total = response.stats.total_leads || 0; const $badge = $('#mxch-leads-nav-badge'); if (total > 0) $badge.text(total).show(); } } }); });