PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.6.0
MxChat – AI Chatbot & Content Generation for WordPress v2.6.0
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 +298 -1643 3.2.22.6.0 View file →
@@ -1,865 +1,306 @@
1 -/**
2 - * MxChat Transcripts Page JavaScript - v3.0
3 - * Split-panel layout with chat list and conversation view
4 - */
5 1 jQuery(document).ready(function($) {
6 - // ==========================================================================
7 - // Sidebar Navigation
8 - // ==========================================================================
9 -
10 - // Desktop sidebar navigation
11 - $('.mxch-nav-link').on('click', function(e) {
12 - e.preventDefault();
13 - const target = $(this).data('target');
14 -
15 - // Update active states
16 - $('.mxch-nav-link').removeClass('active');
17 - $(this).addClass('active');
18 -
19 - // Show target section
20 - $('.mxch-section').removeClass('active');
21 - $('#' + target).addClass('active');
22 -
23 - // Reset scroll position of content area
24 - $('.mxch-content').scrollTop(0);
25 -
26 - // Also update mobile nav if open
27 - $('.mxch-mobile-nav-link').removeClass('active');
28 - $('.mxch-mobile-nav-link[data-target="' + target + '"]').addClass('active');
29 -
30 - // Load transcripts when switching to all-chats
31 - if (target === 'all-chats' && !transcriptsLoaded) {
32 - loadChatList(1, '');
33 - }
34 - });
35 -
36 - // Mobile menu toggle
37 - $('.mxch-mobile-menu-btn').on('click', function() {
38 - $('.mxch-mobile-menu').addClass('open');
39 - $('.mxch-mobile-overlay').addClass('open');
40 - });
41 -
42 - // Close mobile menu
43 - $('.mxch-mobile-menu-close, .mxch-mobile-overlay').on('click', function() {
44 - $('.mxch-mobile-menu').removeClass('open');
45 - $('.mxch-mobile-overlay').removeClass('open');
46 - });
47 -
48 - // Mobile navigation
49 - $('.mxch-mobile-nav-link').on('click', function(e) {
50 - e.preventDefault();
51 - const target = $(this).data('target');
52 -
53 - $('.mxch-mobile-nav-link').removeClass('active');
54 - $(this).addClass('active');
55 -
56 - $('.mxch-section').removeClass('active');
57 - $('#' + target).addClass('active');
58 -
59 - // Reset scroll position of content area
60 - $('.mxch-content').scrollTop(0);
61 -
62 - $('.mxch-nav-link').removeClass('active');
63 - $('.mxch-nav-link[data-target="' + target + '"]').addClass('active');
64 -
65 - $('.mxch-mobile-menu').removeClass('open');
66 - $('.mxch-mobile-overlay').removeClass('open');
67 - });
68 -
69 - // Quick action buttons
70 - $('.mxch-quick-action-btn[data-action="view-chats"]').on('click', function() {
71 - $('.mxch-nav-link[data-target="all-chats"]').trigger('click');
72 - });
73 -
74 - $('.mxch-quick-action-btn[data-action="settings"]').on('click', function() {
75 - $('.mxch-nav-link[data-target="notifications"]').trigger('click');
76 - });
77 -
78 - // ==========================================================================
79 - // Mobile Panel Management
80 - // ==========================================================================
81 -
82 - function isMobile() {
83 - return window.innerWidth <= 782;
84 - }
85 -
86 - function showMobileConversationPanel() {
87 - if (isMobile()) {
88 - $('.mxch-chat-list-panel').addClass('panel-hidden');
89 - $('#mxch-conversation-panel').addClass('panel-active');
90 - $('#mxch-transcript-back-btn').show();
91 - }
92 - }
93 -
94 - function hideMobileConversationPanel() {
95 - if (isMobile()) {
96 - $('#mxch-conversation-panel').removeClass('panel-active');
97 - $('.mxch-chat-list-panel').removeClass('panel-hidden');
98 - $('#mxch-transcript-back-btn').hide();
99 - }
100 - }
101 -
102 - // Mobile back button handler
103 - $('#mxch-transcript-back-btn').on('click', function(e) {
104 - e.preventDefault();
105 - hideMobileConversationPanel();
106 - $('.mxch-chat-item').removeClass('active');
107 - currentSessionId = null;
108 - });
109 -
110 - // Handle window resize
111 - $(window).on('resize', function() {
112 - if (!isMobile()) {
113 - // Reset panel states when switching to desktop
114 - $('.mxch-chat-list-panel').removeClass('panel-hidden');
115 - $('#mxch-conversation-panel').removeClass('panel-active');
116 - $('#mxch-transcript-back-btn').hide();
117 - }
118 - updateMobileViewportHeight();
119 - });
120 -
121 - // Fix for mobile browser address bar - sets CSS custom property for accurate viewport height
122 - function updateMobileViewportHeight() {
123 - if (isMobile()) {
124 - // Use visualViewport if available (most reliable for mobile)
125 - const vh = window.visualViewport ? window.visualViewport.height : window.innerHeight;
126 - document.documentElement.style.setProperty('--mxch-mobile-vh', vh + 'px');
127 - }
128 - }
129 -
130 - // Update on load and viewport changes
131 - updateMobileViewportHeight();
132 - if (window.visualViewport) {
133 - window.visualViewport.addEventListener('resize', updateMobileViewportHeight);
134 - }
135 -
136 - // ==========================================================================
137 - // Chat List - Split Panel
138 - // ==========================================================================
139 -
2 + // Current page state
140 3 let currentPage = 1;
141 - const perPage = 50;
4 + const perPage = 50; // Display 50 sessions per page
142 5 let totalPages = 1;
143 - let currentSessionId = null;
144 - let transcriptsLoaded = false;
6 +
7 + // Track selected sessions for bulk delete
145 8 let selectedSessions = new Set();
146 - let currentSortOrder = 'desc'; // newest first
147 9
148 - // Load chat list on page load
149 - loadChatList(1, '');
10 + // Select/Deselect All functionality
11 + const selectButton = $('#mxchat-select-all-transcripts');
12 + let isSelected = false;
150 13
151 - // Search functionality with debounce
152 - let searchTimeout;
153 - $('#mxch-search-transcripts').on('input', function() {
154 - clearTimeout(searchTimeout);
155 - const searchTerm = $(this).val().toLowerCase();
14 + selectButton.click(function() {
15 + isSelected = !isSelected;
16 + $(this).toggleClass('selected');
17 +
18 + // Update button text
19 + const buttonText = $(this).find('.button-text');
20 + buttonText.text(isSelected ? 'Deselect All' : 'Select All');
21 +
22 + // Update session selection (for current page)
23 + $('.mxchat-session-header').each(function() {
24 + const sessionId = $(this).data('session-id');
25 + const sessionContainer = $(this).closest('.mxchat-session');
26 + if (isSelected) {
27 + selectedSessions.add(sessionId);
28 + sessionContainer.addClass('selected');
29 + } else {
30 + selectedSessions.delete(sessionId);
31 + sessionContainer.removeClass('selected');
32 + }
33 + });
34 +
35 + updateDeleteButtonState();
36 + });
156 37
157 - searchTimeout = setTimeout(function() {
38 + // Search functionality
39 + $('#mxchat-search-transcripts').on('input', function() {
40 + var searchTerm = $(this).val().toLowerCase();
41 +
42 + if (searchTerm.length > 0) {
43 + // Reset to first page when searching
158 44 currentPage = 1;
159 - loadChatList(currentPage, searchTerm);
160 - }, 300);
161 - });
162 -
163 - // Refresh button
164 - $('#mxch-refresh-list').on('click', function() {
165 - const $btn = $(this);
166 - $btn.addClass('spinning');
167 - loadChatList(currentPage, $('#mxch-search-transcripts').val());
168 - setTimeout(() => $btn.removeClass('spinning'), 500);
169 - });
170 -
171 - // ==========================================================================
172 - // Bulk Selection & Actions
173 - // ==========================================================================
174 -
175 - // Select all checkbox
176 - $('#mxch-select-all').on('change', function() {
177 - const isChecked = $(this).is(':checked');
178 - $('.mxch-chat-checkbox').prop('checked', isChecked);
179 -
180 - if (isChecked) {
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'));
185 - $(this).addClass('selected');
186 - });
187 - $('#mxch-chat-list').addClass('selection-mode');
45 +
46 + // Load with search filter
47 + loadTranscripts(currentPage, searchTerm);
188 48 } else {
189 - selectedSessions.clear();
190 - $('.mxch-chat-item').removeClass('selected');
191 - $('#mxch-chat-list').removeClass('selection-mode');
49 + // Reset to first page with no search term
50 + currentPage = 1;
51 + loadTranscripts(currentPage, '');
192 52 }
193 -
194 - updateSelectionUI();
195 53 });
196 54
197 - // Update selection UI
198 - function updateSelectionUI() {
199 - const count = selectedSessions.size;
200 - const $countEl = $('#mxch-selected-count');
201 - const $deleteBtn = $('#mxch-delete-selected');
55 + // Initial load of transcripts
56 + loadTranscripts(currentPage, '');
202 57
203 - if (count > 0) {
204 - $countEl.text(count + ' selected').addClass('has-selection');
205 - $deleteBtn.prop('disabled', false);
206 - $('#mxch-chat-list').addClass('selection-mode');
207 - } else {
208 - $countEl.removeClass('has-selection');
209 - $deleteBtn.prop('disabled', true);
210 - $('#mxch-chat-list').removeClass('selection-mode');
211 - }
212 -
213 - // Update select all checkbox state
214 - const totalItems = $('.mxch-chat-checkbox').length;
215 - const checkedItems = $('.mxch-chat-checkbox:checked').length;
216 - $('#mxch-select-all').prop('checked', totalItems > 0 && checkedItems === totalItems);
217 - $('#mxch-select-all').prop('indeterminate', checkedItems > 0 && checkedItems < totalItems);
218 - }
219 -
220 - // Sort button
221 - $('#mxch-sort-btn').on('click', function() {
222 - currentSortOrder = currentSortOrder === 'desc' ? 'asc' : 'desc';
223 - $(this).find('svg').css('transform', currentSortOrder === 'asc' ? 'rotate(180deg)' : 'rotate(0deg)');
224 - loadChatList(currentPage, $('#mxch-search-transcripts').val());
225 - });
226 -
227 - // Delete selected button — opens the shared confirm modal with the "also delete lead" checkbox.
228 - $('#mxch-delete-selected').on('click', function() {
229 - const count = selectedSessions.size;
230 - if (count === 0) return;
231 - openTranscriptConfirm(Array.from(selectedSessions), count);
232 - });
233 -
234 - // Transcript delete confirm (shared by bulk + individual) ---------------------------
235 - let transcriptConfirmSessionIds = [];
236 -
237 - function openTranscriptConfirm(sessionIds, count) {
238 - transcriptConfirmSessionIds = sessionIds.slice();
239 - const n = count || sessionIds.length;
240 - $('#mxch-transcript-confirm-title').text(n === 1 ? 'Delete conversation?' : 'Delete ' + n + ' conversations?');
241 - $('#mxch-transcript-confirm-body').text(
242 - n === 1
243 - ? 'This removes the conversation and its messages.'
244 - : 'This removes ' + n + ' conversations and their messages.'
245 - );
246 - $('#mxch-transcript-also-delete-lead').prop('checked', false);
247 - $('#mxch-transcript-confirm').fadeIn(120);
248 - }
249 -
250 - function closeTranscriptConfirm() {
251 - $('#mxch-transcript-confirm').fadeOut(120);
252 - transcriptConfirmSessionIds = [];
253 - }
254 -
255 - $(document).on('click', '[data-mxch-transcript-close]', closeTranscriptConfirm);
256 -
257 - $('#mxch-transcript-confirm-go').on('click', function() {
258 - const ids = transcriptConfirmSessionIds.slice();
259 - if (!ids.length) { closeTranscriptConfirm(); return; }
260 - const alsoDeleteLead = $('#mxch-transcript-also-delete-lead').is(':checked');
261 - closeTranscriptConfirm();
262 - deleteMultipleSessions(ids, alsoDeleteLead);
263 - });
264 -
265 - // Delete multiple sessions
266 - function deleteMultipleSessions(sessionIds, alsoDeleteLead) {
58 + // Function to load transcripts with pagination
59 + function loadTranscripts(page, searchTerm = '') {
60 + $('#mxchat-transcripts').html('<div class="mxchat-loading">Loading transcripts...</div>');
61 +
267 62 $.ajax({
268 63 url: ajaxurl,
269 64 type: 'POST',
270 65 data: {
271 - action: 'mxchat_delete_chat_history',
272 - delete_session_ids: sessionIds,
273 - also_delete_lead: alsoDeleteLead ? '1' : '0',
274 - security: $('#mxchat_delete_chat_nonce').val()
275 - },
276 - success: function(response) {
277 - try {
278 - const jsonResponse = typeof response === 'object' ? response : JSON.parse(response);
279 -
280 - if (jsonResponse.success) {
281 - // Clear selection
282 - selectedSessions.clear();
283 - $('#mxch-select-all').prop('checked', false);
284 - updateSelectionUI();
285 -
286 - // If current conversation was deleted, reset panel
287 - if (sessionIds.includes(currentSessionId)) {
288 - currentSessionId = null;
289 - $('#mxch-conversation-content').hide();
290 - $('#mxch-conversation-empty').show();
291 - $('#mxch-details-drawer').hide();
292 - }
293 -
294 - // Reload list
295 - loadChatList(currentPage, $('#mxch-search-transcripts').val());
296 -
297 - // The Leads tab shares this data (Chat deleted pill, nav badge,
298 - // stats) — invalidate it so switching tabs re-fetches instead of
299 - // showing stale "active lead" rows.
300 - invalidateLeadsData();
301 - } else if (jsonResponse.error) {
302 - alert('Error: ' + jsonResponse.error);
303 - }
304 - } catch (e) {
305 - alert('An error occurred while processing the response.');
306 - }
307 - },
308 - error: function() {
309 - alert('An error occurred while deleting conversations.');
310 - }
311 - });
312 - }
313 -
314 - // Load chat list function
315 - function loadChatList(page, searchTerm) {
316 - const $container = $('#mxch-chat-list');
317 - $container.html('<div class="mxch-list-loading"><span class="spinner is-active"></span></div>');
318 -
319 - $.ajax({
320 - url: ajaxurl,
321 - type: 'POST',
322 - data: {
323 66 action: 'mxchat_fetch_chat_history',
324 67 page: page,
325 68 per_page: perPage,
326 - search: searchTerm,
327 - sort_order: currentSortOrder
69 + search: searchTerm
328 70 },
329 71 success: function(response) {
330 - transcriptsLoaded = true;
331 -
332 - if (response.success && response.sessions && response.sessions.length > 0) {
333 - renderChatList(response.sessions);
334 - currentPage = response.page;
335 - totalPages = response.total_pages;
336 - updateChatCount(response.showing_start, response.showing_end, response.total_sessions);
337 - renderPagination(response.page, response.total_pages, searchTerm);
338 - } else {
339 - $container.html('<div class="mxch-list-empty"><p>No chats found</p></div>');
340 - updateChatCount(0, 0, 0);
341 - $('#mxch-pagination').html('');
342 - }
72 + $('#mxchat-transcripts').html(response.html);
73 + currentPage = response.page;
74 + totalPages = response.total_pages;
75 +
76 + // Reset selection state when page changes (but keep selectedSessions for bulk operations)
77 + isSelected = false;
78 + selectButton.removeClass('selected');
79 + selectButton.find('.button-text').text('Select All');
80 +
81 + // Restore selection state for sessions on this page
82 + $('.mxchat-session-header').each(function() {
83 + const sessionId = $(this).data('session-id');
84 + const sessionContainer = $(this).closest('.mxchat-session');
85 + if (selectedSessions.has(sessionId)) {
86 + sessionContainer.addClass('selected');
87 + }
88 + });
89 +
90 + updateDeleteButtonState();
91 +
92 + // Add click handlers to pagination buttons
93 + $('.mxchat-pagination-button').on('click', function() {
94 + var pageNum = $(this).data('page');
95 + loadTranscripts(pageNum, searchTerm);
96 +
97 + // Scroll to top of transcripts
98 + $('html, body').animate({
99 + scrollTop: $('#mxchat-transcripts').offset().top - 50
100 + }, 300);
101 + });
102 +
103 + // Re-attach event handlers for newly loaded content
104 + attachDynamicEventHandlers();
343 105 },
344 - error: function() {
345 - $container.html('<div class="mxch-list-empty"><p>Error loading chats</p></div>');
106 + error: function(xhr, status, error) {
107 + $('#mxchat-transcripts').html('<div class="mxchat-error">Error loading chat transcripts. Please try again.</div>');
108 + console.error("AJAX Error: " + status + " - " + error);
346 109 }
347 110 });
348 111 }
349 -
350 - // Render chat list items
351 - function renderChatList(sessions) {
352 - const $container = $('#mxch-chat-list');
353 - let html = '';
354 -
355 - sessions.forEach(function(session) {
356 - const isActive = session.session_id === currentSessionId ? ' active' : '';
357 - const isSelected = selectedSessions.has(session.session_id) ? ' selected' : '';
358 - const isChecked = selectedSessions.has(session.session_id) ? ' checked' : '';
359 - html += `
360 - <div class="mxch-chat-item${isActive}${isSelected}" data-session-id="${escapeHtml(session.session_id)}">
361 - <input type="checkbox" class="mxch-chat-checkbox"${isChecked}>
362 - <div class="mxch-chat-avatar">
363 - <span>${escapeHtml(session.initials)}</span>
364 - </div>
365 - <div class="mxch-chat-info">
366 - <div class="mxch-chat-name">${escapeHtml(session.display_name)}</div>
367 - <div class="mxch-chat-preview">${escapeHtml(session.preview)}</div>
368 - </div>
369 - <div class="mxch-chat-meta">
370 - <span class="mxch-chat-time">${escapeHtml(session.time_display)}</span>
371 - <span class="mxch-chat-count">${session.message_count}</span>
372 - </div>
373 - </div>
374 - `;
375 - });
376 -
377 - $container.html(html);
378 -
379 - // Attach checkbox handlers
380 - $('.mxch-chat-checkbox').on('click', function(e) {
381 - e.stopPropagation(); // Prevent triggering chat item click
382 - const $item = $(this).closest('.mxch-chat-item');
383 - const sessionId = $item.attr('data-session-id');
384 -
385 - if ($(this).is(':checked')) {
386 - selectedSessions.add(sessionId);
387 - $item.addClass('selected');
388 - } else {
389 - selectedSessions.delete(sessionId);
390 - $item.removeClass('selected');
112 +
113 + // Attach event handlers to dynamically loaded content
114 + function attachDynamicEventHandlers() {
115 + // Handle individual delete button clicks
116 + $('.mxchat-delete-btn').off('click').on('click', function(e) {
117 + e.preventDefault();
118 + e.stopPropagation();
119 +
120 + const sessionId = $(this).data('session-id');
121 +
122 + if (!confirm("Are you sure you want to delete this chat session? This action cannot be undone.")) {
123 + return;
391 124 }
392 -
393 - updateSelectionUI();
125 +
126 + // Delete single session
127 + deleteSessions([sessionId]);
394 128 });
395 -
396 - // Attach click handlers for selecting chat
397 - $('.mxch-chat-item').on('click', function(e) {
398 - // Don't trigger if clicking on checkbox
399 - if ($(e.target).is('.mxch-chat-checkbox')) return;
400 -
401 - const sessionId = $(this).attr('data-session-id');
402 - selectChat(sessionId);
403 -
404 - // Update active state
405 - $('.mxch-chat-item').removeClass('active');
406 - $(this).addClass('active');
407 -
408 - // Show conversation panel on mobile
409 - showMobileConversationPanel();
410 - });
411 -
412 - // Update selection UI after render
413 - updateSelectionUI();
414 - }
415 -
416 - // Update chat count display
417 - function updateChatCount(start, end, total) {
418 - if (total === 0) {
419 - $('#mxch-chat-count').text('0 chats');
420 - } else {
421 - $('#mxch-chat-count').text(`${start}-${end} / ${total} chats`);
422 - }
423 - }
424 -
425 - // Render pagination
426 - function renderPagination(currentPage, totalPages, searchTerm) {
427 - const $container = $('#mxch-pagination');
428 -
429 - if (totalPages <= 1) {
430 - $container.html('');
431 - return;
432 - }
433 -
434 - let html = '<div class="mxch-pagination-btns">';
435 -
436 - if (currentPage > 1) {
437 - html += `<button class="mxch-page-btn" data-page="${currentPage - 1}">&laquo;</button>`;
438 - }
439 -
440 - html += `<span class="mxch-page-info">${currentPage} / ${totalPages}</span>`;
441 -
442 - if (currentPage < totalPages) {
443 - html += `<button class="mxch-page-btn" data-page="${currentPage + 1}">&raquo;</button>`;
444 - }
445 -
446 - html += '</div>';
447 - $container.html(html);
448 -
449 - // Pagination click handlers
450 - $('.mxch-page-btn').on('click', function() {
451 - const pageNum = $(this).data('page');
452 - loadChatList(pageNum, searchTerm);
453 - });
454 - }
455 -
456 - // ==========================================================================
457 - // Conversation Panel
458 - // ==========================================================================
459 -
460 - // Select and load a chat conversation
461 - function selectChat(sessionId) {
462 - currentSessionId = sessionId;
463 -
464 - // Reset translation state when selecting new chat
465 - if (typeof resetTranslationState === 'function') {
466 - resetTranslationState();
467 - }
468 -
469 - // Show loading in conversation panel
470 - $('#mxch-conversation-empty').hide();
471 - $('#mxch-conversation-content').show();
472 - $('#mxch-messages-area').html('<div class="mxch-messages-loading"><span class="spinner is-active"></span> Loading conversation...</div>');
473 -
474 - $.ajax({
475 - url: ajaxurl,
476 - type: 'POST',
477 - data: {
478 - action: 'mxchat_fetch_conversation',
479 - session_id: sessionId
480 - },
481 - success: function(response) {
482 - if (response.success) {
483 - renderConversation(response);
484 - // Load saved translation after rendering
485 - if (typeof loadSavedTranslation === 'function') {
486 - setTimeout(function() {
487 - loadSavedTranslation(sessionId);
488 - }, 100);
489 - }
490 - } else {
491 - $('#mxch-messages-area').html('<div class="mxch-messages-error">Failed to load conversation</div>');
492 - }
493 - },
494 - error: function() {
495 - $('#mxch-messages-area').html('<div class="mxch-messages-error">Error loading conversation</div>');
129 +
130 + // Handle session header clicks for selection (bulk delete)
131 + $('.mxchat-session-header').off('click').on('click', function(e) {
132 + // Don't trigger if clicking the delete button
133 + if ($(e.target).hasClass('mxchat-delete-btn') || $(e.target).closest('.mxchat-delete-btn').length) {
134 + return;
496 135 }
497 - });
498 - }
499 136
500 - // Render conversation content
501 - function renderConversation(data) {
502 - // Update header
503 - $('#mxch-user-avatar span').text(data.user.initials);
504 - $('#mxch-user-name').text(data.user.name);
505 - $('#mxch-user-meta').text(data.user.sub);
137 + const sessionId = $(this).data('session-id');
138 + const sessionContainer = $(this).closest('.mxchat-session');
506 139
507 - // Update details drawer
508 - $('#mxch-detail-messages').text(data.message_count);
509 - $('#mxch-detail-started').text(data.started);
510 -
511 - if (data.page.url) {
512 - $('#mxch-detail-page').html(`<a href="${escapeHtml(data.page.url)}" target="_blank">${escapeHtml(data.page.title || data.page.url)}</a>`);
513 - } else {
514 - $('#mxch-detail-page').text('-');
515 - }
516 -
517 - if (data.user.email) {
518 - $('#mxch-detail-email').text(data.user.email);
519 - $('#mxch-detail-email-row').show();
520 - } else {
521 - $('#mxch-detail-email-row').hide();
522 - }
523 -
524 - // Clicked links
525 - if (data.clicked_urls && data.clicked_urls.length > 0) {
526 - let linksHtml = '';
527 - data.clicked_urls.forEach(function(url) {
528 - linksHtml += `<a href="${escapeHtml(url)}" target="_blank" class="mxch-clicked-link">${escapeHtml(url)}</a>`;
529 - });
530 - $('#mxch-clicked-links').html(linksHtml);
531 - $('#mxch-clicked-section').show();
532 - } else {
533 - $('#mxch-clicked-section').hide();
534 - }
535 -
536 - // Render messages
537 - let messagesHtml = '';
538 - data.messages.forEach(function(msg) {
539 - if (msg.is_user) {
540 - messagesHtml += `
541 - <div class="mxch-message mxch-message-user" data-message-id="${msg.id}">
542 - <div class="mxch-message-row">
543 - <div class="mxch-message-bubble">
544 - ${msg.content}
545 - </div>
546 - </div>
547 - <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>
548 - </div>
549 - `;
140 + if (selectedSessions.has(sessionId)) {
141 + selectedSessions.delete(sessionId);
142 + sessionContainer.removeClass('selected');
550 143 } else {
551 - const ragLink = msg.has_rag ? `<a href="#" class="mxch-rag-link" data-message-id="${msg.id}">Sources</a>` : '';
552 - messagesHtml += `
553 - <div class="mxch-message mxch-message-bot" data-message-id="${msg.id}">
554 - <div class="mxch-message-header">
555 - <span class="mxch-bot-label">AI Assistant</span>
556 - ${ragLink}
557 - </div>
558 - <div class="mxch-message-row">
559 - <div class="mxch-message-bubble">
560 - ${msg.content}
561 - </div>
562 - </div>
563 - <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>
564 - </div>
565 - `;
144 + selectedSessions.add(sessionId);
145 + sessionContainer.addClass('selected');
566 146 }
147 +
148 + updateDeleteButtonState();
567 149 });
568 150
569 - $('#mxch-messages-area').html(messagesHtml);
151 + // Handle clicks on Sources link for RAG context
152 + $('.mxchat-rag-link').off('click').on('click', function(e) {
153 + e.preventDefault();
154 + e.stopPropagation();
570 155
571 - // Scroll to bottom
572 - const $area = $('#mxch-messages-area');
573 - $area.scrollTop($area[0].scrollHeight);
574 -
575 - // Attach RAG link handlers
576 - $('.mxch-rag-link').on('click', function(e) {
577 - e.preventDefault();
578 - const messageId = $(this).data('message-id');
156 + const messageId = $(this).closest('.mxchat-message').data('message-id');
579 157 if (messageId) {
580 158 openRagContextModal(messageId);
581 159 }
582 160 });
583 161 }
584 -
585 - // Toggle details drawer
586 - $('#mxch-toggle-details').on('click', function() {
587 - const $drawer = $('#mxch-details-drawer');
588 - const $btn = $(this);
589 -
590 - if ($drawer.is(':visible')) {
591 - $drawer.slideUp(200);
592 - $btn.removeClass('active');
162 +
163 + // Update delete button state based on selections
164 + function updateDeleteButtonState() {
165 + const deleteButton = $('.delete-chats-button');
166 + if (selectedSessions.size > 0) {
167 + deleteButton.prop('disabled', false);
593 168 } else {
594 - $drawer.slideDown(200);
595 - $btn.addClass('active');
169 + deleteButton.prop('disabled', true);
596 170 }
597 - });
598 -
599 - // Delete current chat — opens the shared confirm modal.
600 - $('#mxch-delete-current').on('click', function() {
601 - if (!currentSessionId) return;
602 - openTranscriptConfirm([currentSessionId], 1);
603 - });
604 -
605 - // Delete session function
606 - function deleteSession(sessionId, alsoDeleteLead) {
171 + }
172 +
173 + // Function to delete sessions
174 + function deleteSessions(sessionIds) {
607 175 $.ajax({
608 176 url: ajaxurl,
609 177 type: 'POST',
610 178 data: {
611 179 action: 'mxchat_delete_chat_history',
612 - delete_session_ids: [sessionId],
613 - also_delete_lead: alsoDeleteLead ? '1' : '0',
180 + delete_session_ids: sessionIds,
614 181 security: $('#mxchat_delete_chat_nonce').val()
615 182 },
616 183 success: function(response) {
617 - try {
618 - const jsonResponse = typeof response === 'object' ? response : JSON.parse(response);
619 -
620 - if (jsonResponse.success) {
621 - // Reset conversation panel
622 - currentSessionId = null;
623 - $('#mxch-conversation-content').hide();
624 - $('#mxch-conversation-empty').show();
625 - $('#mxch-details-drawer').hide();
626 -
627 - // Reload list
628 - loadChatList(currentPage, $('#mxch-search-transcripts').val());
629 - } else if (jsonResponse.error) {
630 - alert('Error: ' + jsonResponse.error);
631 - }
632 - } catch (e) {
633 - alert('An error occurred while processing the response.');
184 + var jsonResponse = JSON.parse(response);
185 + if (jsonResponse.success) {
186 + alert("Success: " + jsonResponse.success);
187 +
188 + // Remove deleted sessions from selectedSessions
189 + sessionIds.forEach(id => selectedSessions.delete(id));
190 +
191 + } else if (jsonResponse.error) {
192 + alert("Error: " + jsonResponse.error);
193 + } else {
194 + //console.log("Unexpected response format.");
634 195 }
196 +
197 + // Reload the current page of transcripts
198 + loadTranscripts(currentPage);
635 199 },
636 - error: function() {
637 - alert('An error occurred while deleting the conversation.');
200 + error: function(xhr, status, error) {
201 + //console.error("AJAX Error: " + status + " - " + error);
202 + //console.log(xhr.responseText);
203 + alert("An error occurred while deleting chat sessions. Please try again.");
638 204 }
639 205 });
640 206 }
641 207
642 - // ==========================================================================
643 - // Export Functionality
644 - // ==========================================================================
645 -
646 - $('#mxch-export-btn, #mxch-export-current').on('click', function() {
647 - const $button = $(this);
208 + // Delete form submission (bulk delete)
209 + $('#mxchat-delete-form').submit(function(e) {
210 + e.preventDefault();
211 +
212 + if (selectedSessions.size === 0) {
213 + alert("Please select at least one chat session to delete.");
214 + return;
215 + }
216 +
217 + // Confirm deletion
218 + if (!confirm(`Are you sure you want to delete the selected ${selectedSessions.size} chat session(s)? This action cannot be undone.`)) {
219 + return;
220 + }
221 +
222 + // Convert Set to Array and delete
223 + deleteSessions(Array.from(selectedSessions));
224 + });
225 +
226 + // Export functionality - this remains unchanged as it should export all transcripts
227 + $('#mxchat-export-transcripts').on('click', function() {
228 + var $button = $(this);
648 229 $button.prop('disabled', true).addClass('loading');
649 230
650 - const $form = $('<form>', {
651 - method: 'post',
652 - action: ajaxurl
231 + // Create a form and submit it
232 + var $form = $('<form>', {
233 + 'method': 'post',
234 + 'action': ajaxurl
653 235 });
654 236
655 237 $form.append($('<input>', {
656 - type: 'hidden',
657 - name: 'action',
658 - value: 'mxchat_export_transcripts'
238 + 'type': 'hidden',
239 + 'name': 'action',
240 + 'value': 'mxchat_export_transcripts'
659 241 }));
660 242
661 243 $form.append($('<input>', {
662 - type: 'hidden',
663 - name: 'security',
664 - value: mxchatAdmin.export_nonce
244 + 'type': 'hidden',
245 + 'name': 'security',
246 + 'value': mxchatAdmin.export_nonce
665 247 }));
666 248
667 249 $form.appendTo('body').submit();
668 250
251 + // Re-enable the button after a short delay
669 252 setTimeout(function() {
670 253 $button.prop('disabled', false).removeClass('loading');
671 254 }, 2000);
672 255 });
673 -
674 - // ==========================================================================
675 - // Translation Functionality
676 - // ==========================================================================
677 -
678 - // Store original messages for reverting
679 - let originalMessages = null;
680 - let isTranslated = false;
681 - let currentTranslationLang = null;
682 -
683 - // Load saved language preference from localStorage
684 - const savedLang = localStorage.getItem('mxch_translate_lang');
685 - if (savedLang) {
686 - $('#mxch-translate-lang').val(savedLang);
687 - }
688 -
689 - // Save language preference when changed
690 - $('#mxch-translate-lang').on('change', function() {
691 - localStorage.setItem('mxch_translate_lang', $(this).val());
256 +
257 + // Chat Email Notification Modal functionality
258 +
259 + // Open modal
260 + $('#mxchat-chat-email-notification-btn').on('click', function(e) {
261 + e.preventDefault();
262 + $('#mxchat-chat-email-notification-modal').fadeIn(300);
692 263 });
693 -
694 - // Apply translations to messages
695 - function applyTranslations(translations) {
696 - // Store original messages if not already stored
697 - if (!originalMessages) {
698 - originalMessages = [];
699 - $('#mxch-messages-area .mxch-message-bubble').each(function() {
700 - originalMessages.push($(this).html());
701 - });
264 +
265 + // Close modal
266 + $('.mxchat-chat-notification-modal-close, .mxchat-chat-notification-modal-cancel').on('click', function() {
267 + $('#mxchat-chat-email-notification-modal').fadeOut(300);
268 + });
269 +
270 + // Close modal on outside click
271 + $('#mxchat-chat-email-notification-modal').on('click', function(e) {
272 + if ($(e.target).is('#mxchat-chat-email-notification-modal')) {
273 + $(this).fadeOut(300);
702 274 }
703 -
704 - // Apply translations
705 - translations.forEach(function(item) {
706 - const $bubble = $('#mxch-messages-area .mxch-message-bubble').eq(item.index);
707 - if ($bubble.length) {
708 - $bubble.html(item.translated);
709 - $bubble.addClass('translated');
710 - }
711 - });
712 -
713 - isTranslated = true;
714 - $('#mxch-show-original-btn').show();
715 - }
716 -
717 - // Load saved translation for current session
718 - function loadSavedTranslation(sessionId) {
719 - $.ajax({
720 - url: ajaxurl,
721 - type: 'POST',
722 - data: {
723 - action: 'mxchat_get_transcript_translation',
724 - session_id: sessionId
725 - },
726 - success: function(response) {
727 - if (response.success && response.has_translation) {
728 - currentTranslationLang = response.language;
729 - applyTranslations(response.translations);
730 - // Update language selector to show saved language
731 - $('#mxch-translate-lang').val(response.language);
732 - }
733 - }
734 - });
735 - }
736 -
737 - // Translate button click handler
738 - $('#mxch-translate-btn').on('click', function() {
739 - if (!currentSessionId) return;
740 -
741 - const $btn = $(this);
742 - const targetLang = $('#mxch-translate-lang').val();
743 -
744 - // Disable button and show loading state
745 - $btn.prop('disabled', true);
746 - $btn.find('.mxch-translate-text').text('Translating...');
747 - $btn.find('svg').addClass('mxch-translate-spinner');
748 -
749 - // Store original messages before translation
750 - if (!originalMessages) {
751 - originalMessages = [];
752 - $('#mxch-messages-area .mxch-message-bubble').each(function() {
753 - originalMessages.push($(this).html());
754 - });
755 - }
756 -
757 - // If already translated, restore originals first before re-translating
758 - if (isTranslated) {
759 - $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
760 - if (originalMessages[index]) {
761 - $(this).html(originalMessages[index]);
762 - $(this).removeClass('translated');
763 - }
764 - });
765 - }
766 -
767 - // Collect all message content (from originals)
768 - const messages = [];
769 - originalMessages.forEach(function(html, index) {
770 - // Create temp element to get text content
771 - const $temp = $('<div>').html(html);
772 - messages.push({
773 - index: index,
774 - content: $temp.text().trim()
775 - });
776 - });
777 -
778 - // Send translation request
779 - $.ajax({
780 - url: ajaxurl,
781 - type: 'POST',
782 - data: {
783 - action: 'mxchat_translate_messages',
784 - session_id: currentSessionId,
785 - target_lang: targetLang,
786 - messages: JSON.stringify(messages),
787 - security: mxchatAdmin.translate_nonce || ''
788 - },
789 - success: function(response) {
790 - if (response.success && response.translations) {
791 - currentTranslationLang = response.language;
792 - applyTranslations(response.translations);
793 - $btn.find('.mxch-translate-text').text('Translate');
794 - } else {
795 - alert(response.error || 'Translation failed. Please try again.');
796 - $btn.find('.mxch-translate-text').text('Translate');
797 - }
798 - },
799 - error: function() {
800 - alert('Translation request failed. Please try again.');
801 - $btn.find('.mxch-translate-text').text('Translate');
802 - },
803 - complete: function() {
804 - $btn.prop('disabled', false);
805 - $btn.find('svg').removeClass('mxch-translate-spinner');
806 - }
807 - });
808 275 });
276 +
277 + // Handle form submission - Let WordPress handle it normally for settings
278 + $('#mxchat-chat-email-notification-form').on('submit', function(e) {
279 + // Don't prevent default - let the form submit normally to WordPress options.php
280 + var $submitButton = $(this).find('button[type="submit"]');
281 + var originalText = $submitButton.text();
809 282
810 - // Show original button click handler
811 - $('#mxch-show-original-btn').on('click', function() {
812 - if (!originalMessages) return;
283 + // Just show a loading state
284 + $submitButton.text('Saving...').prop('disabled', true);
813 285
814 - // Restore original messages
815 - $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
816 - if (originalMessages[index]) {
817 - $(this).html(originalMessages[index]);
818 - $(this).removeClass('translated');
819 - }
820 - });
821 -
822 - isTranslated = false;
823 - $(this).hide();
286 + // The form will submit normally and reload the page
824 287 });
825 288
826 - // Reset translation state (called when selecting new chat)
827 - function resetTranslationState() {
828 - originalMessages = null;
829 - isTranslated = false;
830 - currentTranslationLang = null;
831 - $('#mxch-show-original-btn').hide();
832 - }
289 + // ========== RAG Context Modal Functions ==========
833 290
834 - // Make functions available to selectChat
835 - window.resetTranslationState = resetTranslationState;
836 - window.loadSavedTranslation = loadSavedTranslation;
837 -
838 - // ==========================================================================
839 - // RAG Context Modal (Sources & Actions Tabs)
840 - // ==========================================================================
841 -
291 + // Open RAG context modal and fetch data
842 292 function openRagContextModal(messageId) {
843 - const $modal = $('#mxch-rag-modal');
844 - const $loading = $modal.find('.mxch-rag-loading');
845 - const $sourcesContent = $modal.find('.mxch-rag-content');
846 - const $actionsContent = $modal.find('.mxch-actions-content');
293 + const $modal = $('#mxchat-rag-context-modal');
294 + const $loading = $modal.find('.mxchat-rag-loading');
295 + const $content = $modal.find('.mxchat-rag-content');
847 296
848 - // Reset to Sources tab
849 - $modal.find('.mxch-context-tab').removeClass('active');
850 - $modal.find('.mxch-context-tab[data-tab="sources"]').addClass('active');
851 - $('#mxch-tab-sources').show();
852 - $('#mxch-tab-actions').hide();
853 -
854 - // Reset badge counts
855 - $('#mxch-sources-count, #mxch-actions-count').hide().text('0');
856 -
857 - $modal.fadeIn(200);
297 + // Show modal with loading state
298 + $modal.fadeIn(300);
858 299 $loading.show();
859 - $sourcesContent.html('');
860 - $actionsContent.html('');
300 + $content.html('');
861 301
302 + // Fetch RAG context via AJAX
862 303 $.ajax({
863 304 url: ajaxurl,
864 305 type: 'POST',
865 306 data: {
@@ -867,206 +308,109 @@
867 308 message_id: messageId
868 309 },
869 310 success: function(response) {
870 311 $loading.hide();
871 -
872 312 if (response.success && response.data) {
873 - // Render sources tab
874 - renderRagContext(response.data, $sourcesContent);
875 -
876 - // Render actions tab
877 - renderActionsContext(response.data, $actionsContent);
878 -
879 - // Update badge counts
880 - const sourcesCount = response.data.top_matches ? response.data.top_matches.length : 0;
881 - const actionsCount = response.data.action_analysis ? response.data.action_analysis.length : 0;
882 -
883 - if (sourcesCount > 0) {
884 - $('#mxch-sources-count').text(sourcesCount).show();
885 - }
886 - if (actionsCount > 0) {
887 - $('#mxch-actions-count').text(actionsCount).show();
888 - }
313 + renderRagContext(response.data, $content);
889 314 } else {
890 - $sourcesContent.html('<div class="mxch-rag-error">Unable to load document context.</div>');
891 - $actionsContent.html('<div class="mxch-rag-error">No action data available.</div>');
315 + $content.html('<div class="mxchat-rag-error">Unable to load document context.</div>');
892 316 }
893 317 },
894 318 error: function() {
895 319 $loading.hide();
896 - $sourcesContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
897 - $actionsContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
320 + $content.html('<div class="mxchat-rag-error">Error loading document context. Please try again.</div>');
898 321 }
899 322 });
900 323 }
901 324
902 - // Tab switching
903 - $(document).on('click', '.mxch-context-tab', function() {
904 - const $tab = $(this);
905 - const tabName = $tab.data('tab');
906 -
907 - // Update active tab
908 - $('.mxch-context-tab').removeClass('active');
909 - $tab.addClass('active');
910 -
911 - // Show/hide content
912 - $('.mxch-tab-content').hide();
913 - $('#mxch-tab-' + tabName).show();
914 - });
915 -
325 + // Render RAG context data in the modal
916 326 function renderRagContext(data, $container) {
917 327 let html = '';
918 328
919 - // Check if we have any source data
920 - if (!data.top_matches || data.top_matches.length === 0) {
921 - html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>';
922 - $container.html(html);
923 - return;
924 - }
925 -
926 - html += '<div class="mxch-rag-summary">';
927 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Knowledge Base:</span> <span class="mxch-rag-value">' + escapeHtml(data.knowledge_base_type || 'WordPress Database') + '</span></div>';
928 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Similarity Threshold:</span> <span class="mxch-rag-value">' + Math.round((data.similarity_threshold || 0.35) * 100) + '%</span></div>';
929 - 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>';
329 + // Summary section
330 + html += '<div class="mxchat-rag-summary">';
331 + html += '<div class="mxchat-rag-summary-item">';
332 + html += '<span class="mxchat-rag-label">Knowledge Base:</span> ';
333 + html += '<span class="mxchat-rag-value">' + escapeHtml(data.knowledge_base_type || 'WordPress Database') + '</span>';
930 334 html += '</div>';
335 + html += '<div class="mxchat-rag-summary-item">';
336 + html += '<span class="mxchat-rag-label">Similarity Threshold:</span> ';
337 + html += '<span class="mxchat-rag-value">' + Math.round((data.similarity_threshold || 0.35) * 100) + '%</span>';
338 + html += '</div>';
339 + html += '<div class="mxchat-rag-summary-item">';
340 + html += '<span class="mxchat-rag-label">Documents Checked:</span> ';
341 + html += '<span class="mxchat-rag-value">' + (data.total_documents_checked || 0) + '</span>';
342 + html += '</div>';
343 + html += '</div>';
931 344
932 - const groupedByUrl = {};
345 + // Top matches section
346 + if (data.top_matches && data.top_matches.length > 0) {
347 + html += '<div class="mxchat-rag-matches">';
348 + html += '<h3>Retrieved Documents</h3>';
933 349
934 - data.top_matches.forEach(function(match) {
935 - const url = match.source_display || 'Unknown';
936 - if (!groupedByUrl[url]) {
937 - groupedByUrl[url] = {
938 - url: url,
939 - isUrl: url.startsWith('http'),
940 - bestScore: 0,
941 - usedForContext: false,
942 - matchedChunks: []
943 - };
944 - }
350 + data.top_matches.forEach(function(match, index) {
351 + const isUsed = match.used_for_context;
352 + const isAboveThreshold = match.above_threshold;
353 + const cardClass = isUsed ? 'mxchat-rag-match-used' : (isAboveThreshold ? 'mxchat-rag-match-above' : 'mxchat-rag-match-below');
354 + const statusIcon = isUsed ? '✅' : (isAboveThreshold ? '⚠️' : '❌');
355 + const statusLabel = isUsed ? 'Used for Response' : (isAboveThreshold ? 'Above Threshold' : 'Below Threshold');
945 356
946 - if (match.similarity_percentage > groupedByUrl[url].bestScore) {
947 - groupedByUrl[url].bestScore = match.similarity_percentage;
948 - }
357 + html += '<div class="mxchat-rag-match-card ' + cardClass + '">';
358 + html += '<div class="mxchat-rag-match-header">';
359 + html += '<span class="mxchat-rag-match-rank">#' + (index + 1) + '</span>';
360 + html += '<span class="mxchat-rag-match-score">' + match.similarity_percentage + '%</span>';
361 + html += '<span class="mxchat-rag-match-status ' + (isUsed ? 'status-used' : (isAboveThreshold ? 'status-above' : 'status-below')) + '">';
362 + html += statusIcon + ' ' + statusLabel;
363 + html += '</span>';
364 + html += '</div>';
949 365
950 - if (match.used_for_context) {
951 - groupedByUrl[url].usedForContext = true;
952 - }
366 + html += '<div class="mxchat-rag-match-source">';
367 + if (match.source_display && match.source_display.startsWith('http')) {
368 + html += '<a href="' + escapeHtml(match.source_display) + '" target="_blank" rel="noopener noreferrer">';
369 + html += '🔗 ' + escapeHtml(match.source_display);
370 + html += '</a>';
371 + } else {
372 + html += '📄 ' + escapeHtml(match.source_display || 'Content snippet');
373 + }
374 + html += '</div>';
953 375
954 - groupedByUrl[url].matchedChunks.push({
955 - chunkIndex: match.chunk_index,
956 - score: match.similarity_percentage,
957 - usedForContext: match.used_for_context
958 - });
959 - });
376 + if (match.content_preview) {
377 + html += '<div class="mxchat-rag-match-preview">';
378 + html += escapeHtml(match.content_preview);
379 + html += '</div>';
380 + }
960 381
961 - const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore);
962 - const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length;
963 - const chunksInfo = data.total_chunks_used > 0 ? data.total_chunks_used + ' chunks sent to AI' : '';
382 + // Show role restriction if not public
383 + if (match.role_restriction && match.role_restriction !== 'public') {
384 + html += '<div class="mxchat-rag-match-meta">';
385 + html += '<span class="mxchat-rag-role-badge">🔒 ' + escapeHtml(match.role_restriction) + '</span>';
386 + html += '</div>';
387 + }
964 388
965 - html += '<div class="mxch-rag-matches">';
966 - html += '<h3>Retrieved Documents</h3>';
967 - html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">' + usedUrlCount + ' source' + (usedUrlCount === 1 ? '' : 's') + ' used for response' + (chunksInfo ? ' &middot; ' + chunksInfo : '') + '</p>';
389 + html += '</div>';
390 + });
968 391
969 - urlGroups.forEach(function(group) {
970 - const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
971 - const statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
972 - const statusLabel = group.usedForContext ? 'Used' : 'Not Used';
973 -
974 - html += '<div class="mxch-rag-match-card ' + cardClass + '">';
975 - html += '<div class="mxch-rag-match-header">';
976 - html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
977 -
978 - if (group.matchedChunks.length > 1) {
979 - html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
980 - }
981 -
982 - html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
983 392 html += '</div>';
393 + } else {
394 + html += '<div class="mxchat-rag-no-matches">No document matches found for this response.</div>';
395 + }
984 396
985 - html += '<div class="mxch-rag-match-source">';
986 - if (group.isUrl) {
987 - html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>';
988 - } else {
989 - html += escapeHtml(group.url);
990 - }
397 + // Approved URLs section
398 + if (data.approved_urls && data.approved_urls.length > 0) {
399 + html += '<div class="mxchat-rag-urls">';
400 + html += '<h3>Approved URLs for Citations (' + data.approved_urls.length + ')</h3>';
401 + html += '<ul class="mxchat-rag-url-list">';
402 + data.approved_urls.forEach(function(url) {
403 + html += '<li><a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(url) + '</a></li>';
404 + });
405 + html += '</ul>';
991 406 html += '</div>';
992 - html += '</div>';
993 - });
994 -
995 - html += '</div>';
996 - $container.html(html);
997 - }
998 -
999 - function renderActionsContext(data, $container) {
1000 - let html = '';
1001 -
1002 - // Check if we have action analysis data
1003 - if (!data.action_analysis || data.action_analysis.length === 0) {
1004 - html += '<div class="mxch-no-results"><p>No action analysis available for this message.</p><p style="color: var(--mxch-text-secondary); font-size: 13px; margin-top: 8px;">Actions are only evaluated when enabled in your bot configuration.</p></div>';
1005 - $container.html(html);
1006 - return;
1007 407 }
1008 408
1009 - const actions = data.action_analysis;
1010 - const triggeredAction = actions.find(a => a.triggered);
1011 - const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
1012 -
1013 - // Summary section
1014 - html += '<div class="mxch-rag-summary">';
1015 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Actions Evaluated:</span> <span class="mxch-rag-value">' + actions.length + '</span></div>';
1016 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Above Threshold:</span> <span class="mxch-rag-value">' + actionsAboveThreshold + '</span></div>';
1017 - if (triggeredAction) {
1018 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Triggered:</span> <span class="mxch-rag-value" style="color: #10b981; font-weight: 600;">' + escapeHtml(triggeredAction.intent_label) + '</span></div>';
1019 - }
1020 - html += '</div>';
1021 -
1022 - // Actions list
1023 - html += '<div class="mxch-rag-matches">';
1024 - html += '<h3>Action Scores</h3>';
1025 - html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">Showing all evaluated actions sorted by similarity score</p>';
1026 -
1027 - actions.forEach(function(action) {
1028 - let cardClass = 'mxch-rag-match-below';
1029 - let statusIcon = '&#10007;';
1030 - let statusLabel = 'Below Threshold';
1031 -
1032 - if (action.triggered) {
1033 - cardClass = 'mxch-action-triggered';
1034 - statusIcon = '&#9889;';
1035 - statusLabel = 'Triggered';
1036 - } else if (action.above_threshold) {
1037 - cardClass = 'mxch-rag-match-used';
1038 - statusIcon = '&#10003;';
1039 - statusLabel = 'Above Threshold';
1040 - }
1041 -
1042 - html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1043 - html += '<div class="mxch-rag-match-header">';
1044 - html += '<span class="mxch-rag-match-score">' + action.similarity_percentage + '%</span>';
1045 - html += '<span class="mxch-action-threshold-badge">Threshold: ' + action.threshold_percentage + '%</span>';
1046 - html += '<span class="mxch-rag-match-status ' + (action.triggered ? 'status-triggered' : (action.above_threshold ? 'status-used' : 'status-below')) + '">' + statusIcon + ' ' + statusLabel + '</span>';
1047 - html += '</div>';
1048 -
1049 - html += '<div class="mxch-action-details">';
1050 - html += '<div class="mxch-action-label">' + escapeHtml(action.intent_label) + '</div>';
1051 - html += '<div class="mxch-action-callback"><span class="mxch-action-callback-label">Callback:</span> ' + escapeHtml(action.callback_function) + '</div>';
1052 - html += '</div>';
1053 -
1054 - // Score bar visualization
1055 - const scoreBarWidth = Math.min(action.similarity_percentage, 100);
1056 - const thresholdPos = Math.min(action.threshold_percentage, 100);
1057 - html += '<div class="mxch-action-score-bar">';
1058 - html += '<div class="mxch-action-score-fill" style="width: ' + scoreBarWidth + '%;"></div>';
1059 - html += '<div class="mxch-action-threshold-marker" style="left: ' + thresholdPos + '%;"></div>';
1060 - html += '</div>';
1061 -
1062 - html += '</div>';
1063 - });
1064 -
1065 - html += '</div>';
1066 409 $container.html(html);
1067 410 }
1068 411
412 + // Helper function to escape HTML
1069 413 function escapeHtml(text) {
1070 414 if (!text) return '';
1071 415 const div = document.createElement('div');
1072 416 div.textContent = text;
@@ -1072,712 +416,23 @@
1072 416 div.textContent = text;
1073 417 return div.innerHTML;
1074 418 }
1075 419
1076 - // Close RAG modal
1077 - $('.mxch-modal-close').on('click', function() {
1078 - $(this).closest('.mxch-modal-overlay').fadeOut(200);
420 + // Close RAG context modal
421 + $('.mxchat-rag-modal-close').on('click', function() {
422 + $('#mxchat-rag-context-modal').fadeOut(300);
1079 423 });
1080 424
1081 - $('.mxch-modal-overlay').on('click', function(e) {
1082 - if ($(e.target).is('.mxch-modal-overlay')) {
1083 - $(this).fadeOut(200);
425 + // Close modal on outside click
426 + $('#mxchat-rag-context-modal').on('click', function(e) {
427 + if ($(e.target).is('#mxchat-rag-context-modal')) {
428 + $(this).fadeOut(300);
1084 429 }
1085 430 });
1086 431
432 + // Close modal on Escape key
1087 433 $(document).on('keydown', function(e) {
1088 434 if (e.key === 'Escape') {
1089 - $('.mxch-modal-overlay').fadeOut(200);
435 + $('#mxchat-rag-context-modal').fadeOut(300);
1090 436 }
1091 437 });
1092 -
1093 - // ==========================================================================
1094 - // Activity Chart
1095 - // ==========================================================================
1096 -
1097 - // Simple chart implementation (no external dependencies)
1098 - class SimpleChart {
1099 - constructor(canvas, config) {
1100 - this.canvas = canvas;
1101 - this.ctx = canvas.getContext('2d');
1102 - this.config = config;
1103 - this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
1104 - this.render();
1105 - }
1106 -
1107 - destroy() {
1108 - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1109 - }
1110 -
1111 - render() {
1112 - const dpr = window.devicePixelRatio || 1;
1113 - const rect = this.canvas.getBoundingClientRect();
1114 -
1115 - this.canvas.width = rect.width * dpr;
1116 - this.canvas.height = rect.height * dpr;
1117 - this.ctx.scale(dpr, dpr);
1118 -
1119 - this.canvas.style.width = rect.width + 'px';
1120 - this.canvas.style.height = rect.height + 'px';
1121 -
1122 - const width = rect.width - this.padding.left - this.padding.right;
1123 - const height = rect.height - this.padding.top - this.padding.bottom;
1124 -
1125 - // Find max value
1126 - let maxValue = 0;
1127 - this.config.datasets.forEach(dataset => {
1128 - const max = Math.max(...dataset.data);
1129 - if (max > maxValue) maxValue = max;
1130 - });
1131 -
1132 - // Add some padding to max value
1133 - maxValue = Math.ceil(maxValue * 1.1);
1134 - if (maxValue === 0) maxValue = 10;
1135 -
1136 - // Draw grid lines
1137 - this.ctx.strokeStyle = '#e5e7eb';
1138 - this.ctx.lineWidth = 1;
1139 - const gridLines = 5;
1140 -
1141 - for (let i = 0; i <= gridLines; i++) {
1142 - const y = this.padding.top + (height / gridLines) * i;
1143 - this.ctx.beginPath();
1144 - this.ctx.moveTo(this.padding.left, y);
1145 - this.ctx.lineTo(this.padding.left + width, y);
1146 - this.ctx.stroke();
1147 -
1148 - // Draw y-axis labels
1149 - const value = maxValue - (maxValue / gridLines) * i;
1150 - this.ctx.fillStyle = '#6b7280';
1151 - this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1152 - this.ctx.textAlign = 'right';
1153 - this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
1154 - }
1155 -
1156 - // Draw datasets
1157 - this.config.datasets.forEach(dataset => {
1158 - const points = [];
1159 - const xStep = width / (this.config.labels.length - 1 || 1);
1160 -
1161 - dataset.data.forEach((value, index) => {
1162 - const x = this.padding.left + (xStep * index);
1163 - const y = this.padding.top + height - (value / maxValue * height);
1164 - points.push({ x, y, value });
1165 - });
1166 -
1167 - // Draw filled area
1168 - if (dataset.fill && dataset.backgroundColor) {
1169 - this.ctx.fillStyle = dataset.backgroundColor;
1170 - this.ctx.beginPath();
1171 - this.ctx.moveTo(points[0].x, this.padding.top + height);
1172 - points.forEach(point => {
1173 - this.ctx.lineTo(point.x, point.y);
1174 - });
1175 - this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
1176 - this.ctx.closePath();
1177 - this.ctx.fill();
1178 - }
1179 -
1180 - // Draw line
1181 - this.ctx.strokeStyle = dataset.borderColor;
1182 - this.ctx.lineWidth = 3;
1183 - this.ctx.lineCap = 'round';
1184 - this.ctx.lineJoin = 'round';
1185 -
1186 - this.ctx.beginPath();
1187 - points.forEach((point, index) => {
1188 - if (index === 0) {
1189 - this.ctx.moveTo(point.x, point.y);
1190 - } else {
1191 - this.ctx.lineTo(point.x, point.y);
1192 - }
1193 - });
1194 - this.ctx.stroke();
1195 -
1196 - // Draw points
1197 - points.forEach(point => {
1198 - this.ctx.fillStyle = '#ffffff';
1199 - this.ctx.beginPath();
1200 - this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
1201 - this.ctx.fill();
1202 - this.ctx.strokeStyle = dataset.borderColor;
1203 - this.ctx.lineWidth = 2;
1204 - this.ctx.stroke();
1205 - });
1206 - });
1207 -
1208 - // Draw x-axis labels
1209 - const xStep = width / (this.config.labels.length - 1 || 1);
1210 - this.ctx.fillStyle = '#6b7280';
1211 - this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1212 - this.ctx.textAlign = 'center';
1213 -
1214 - this.config.labels.forEach((label, index) => {
1215 - const x = this.padding.left + (xStep * index);
1216 - this.ctx.fillText(label, x, this.padding.top + height + 20);
1217 - });
1218 - }
1219 - }
1220 -
1221 - // Initialize activity chart
1222 - function initActivityChart() {
1223 - console.log('[MxChat Chart] initActivityChart called');
1224 -
1225 - const canvas = document.getElementById('mxchat-activity-chart');
1226 - console.log('[MxChat Chart] Canvas element:', canvas);
1227 -
1228 - if (!canvas) {
1229 - console.log('[MxChat Chart] Canvas not found, aborting');
1230 - return;
1231 - }
1232 -
1233 - console.log('[MxChat Chart] mxchatChartData exists:', typeof mxchatChartData !== 'undefined');
1234 - if (typeof mxchatChartData === 'undefined') {
1235 - console.log('[MxChat Chart] mxchatChartData is undefined, aborting');
1236 - return;
1237 - }
1238 -
1239 - console.log('[MxChat Chart] Raw mxchatChartData:', mxchatChartData);
1240 -
1241 - // Check if chart already exists and destroy it
1242 - if (canvas.chartInstance) {
1243 - canvas.chartInstance.destroy();
1244 - }
1245 -
1246 - const ctx = canvas.getContext('2d');
1247 - console.log('[MxChat Chart] Canvas context:', ctx);
1248 - console.log('[MxChat Chart] Canvas dimensions:', canvas.getBoundingClientRect());
1249 -
1250 - // Create gradient for chats line
1251 - const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
1252 - chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
1253 - chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
1254 -
1255 - // Create gradient for messages line
1256 - const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
1257 - messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
1258 - messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
1259 -
1260 - // Convert wp_localize_script objects to arrays (WordPress converts indexed arrays to objects)
1261 - const labels = Object.values(mxchatChartData.labels);
1262 - const chatsData = Object.values(mxchatChartData.chats).map(Number);
1263 - const messagesData = Object.values(mxchatChartData.messages).map(Number);
1264 -
1265 - console.log('[MxChat Chart] Processed labels:', labels);
1266 - console.log('[MxChat Chart] Processed chatsData:', chatsData);
1267 - console.log('[MxChat Chart] Processed messagesData:', messagesData);
1268 -
1269 - // Create chart
1270 - try {
1271 - canvas.chartInstance = new SimpleChart(canvas, {
1272 - labels: labels,
1273 - datasets: [
1274 - {
1275 - label: 'Chats',
1276 - data: chatsData,
1277 - borderColor: '#667eea',
1278 - backgroundColor: chatsGradient,
1279 - fill: true
1280 - },
1281 - {
1282 - label: 'Messages',
1283 - data: messagesData,
1284 - borderColor: '#764ba2',
1285 - backgroundColor: messagesGradient,
1286 - fill: true
1287 - }
1288 - ]
1289 - });
1290 - console.log('[MxChat Chart] Chart created successfully');
1291 - } catch (error) {
1292 - console.error('[MxChat Chart] Error creating chart:', error);
1293 - }
1294 - }
1295 -
1296 - // Initialize chart on page load (dashboard is shown by default)
1297 - setTimeout(function() {
1298 - initActivityChart();
1299 - }, 100);
1300 -
1301 - // Reinitialize chart on window resize
1302 - let resizeTimeout;
1303 - $(window).on('resize', function() {
1304 - clearTimeout(resizeTimeout);
1305 - resizeTimeout = setTimeout(function() {
1306 - initActivityChart();
1307 - }, 250);
1308 - });
1309 -
1310 - // ==========================================================================
1311 - // Leads Tab
1312 - // ==========================================================================
1313 -
1314 - const leadsState = {
1315 - loaded: false,
1316 - page: 1,
1317 - perPage: 25,
1318 - totalPages: 1,
1319 - totalCount: 0,
1320 - selected: new Set(),
1321 - filters: {
1322 - search: '',
1323 - dateRange: 'all',
1324 - status: 'all',
1325 - pageUrl: '',
1326 - pageTitle: ''
1327 - },
1328 - pendingDelete: [],
1329 - leadsRows: [] // last-rendered rows for quick lookup
1330 - };
1331 -
1332 - function $leads() { return $('#leads'); }
1333 -
1334 - // Called after a transcript delete from the All Chats side. Marks the Leads tab
1335 - // data stale so the next tab visit re-fetches, and refreshes immediately if the
1336 - // Leads tab happens to already be visible.
1337 - function invalidateLeadsData() {
1338 - leadsState.loaded = false;
1339 - if ($('#leads').hasClass('active')) {
1340 - loadLeads(1);
1341 - }
1342 - }
1343 -
1344 - function escapeHtmlLeads(s) {
1345 - if (s === null || typeof s === 'undefined') return '';
1346 - return String(s)
1347 - .replace(/&/g, '&amp;')
1348 - .replace(/</g, '&lt;')
1349 - .replace(/>/g, '&gt;')
1350 - .replace(/"/g, '&quot;')
1351 - .replace(/'/g, '&#039;');
1352 - }
1353 -
1354 - function leadsFiltersActive() {
1355 - const f = leadsState.filters;
1356 - return f.search !== '' || f.dateRange !== 'all' || f.status !== 'all' || f.pageUrl !== '';
1357 - }
1358 -
1359 - function updateClearFiltersButton() {
1360 - if (leadsFiltersActive()) {
1361 - $('#mxch-leads-clear-filters').show();
1362 - } else {
1363 - $('#mxch-leads-clear-filters').hide();
1364 - }
1365 - }
1366 -
1367 - function setPageFilterChip(url, title) {
1368 - leadsState.filters.pageUrl = url || '';
1369 - leadsState.filters.pageTitle = title || url || '';
1370 - const $chip = $('#mxch-leads-active-page-filter');
1371 - if (url) {
1372 - $chip.find('.mxch-leads-page-chip-label').text('Page: ' + (title || url));
1373 - $chip.show();
1374 - } else {
1375 - $chip.hide();
1376 - }
1377 - updateClearFiltersButton();
1378 - }
1379 -
1380 - function loadLeads(page) {
1381 - if (typeof page === 'number') leadsState.page = page;
1382 -
1383 - const $tbody = $('#mxch-leads-tbody');
1384 - $tbody.html('<tr><td colspan="6" class="mxch-leads-loading"><span class="spinner is-active"></span></td></tr>');
1385 -
1386 - $.ajax({
1387 - url: ajaxurl,
1388 - type: 'POST',
1389 - data: {
1390 - action: 'mxchat_fetch_leads',
1391 - page: leadsState.page,
1392 - per_page: leadsState.perPage,
1393 - search: leadsState.filters.search,
1394 - date_range: leadsState.filters.dateRange,
1395 - status: leadsState.filters.status,
1396 - page_url: leadsState.filters.pageUrl
1397 - },
1398 - success: function(response) {
1399 - leadsState.loaded = true;
1400 - if (!response || !response.success) {
1401 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1402 - return;
1403 - }
1404 - leadsState.totalPages = response.total_pages || 1;
1405 - leadsState.totalCount = response.total_count || 0;
1406 - leadsState.leadsRows = response.leads || [];
1407 -
1408 - renderLeadsStats(response.stats || {});
1409 - renderLeadsTopPages(response.top_pages || []);
1410 - renderLeadsTable(response.leads || []);
1411 - renderLeadsCount(response.showing_start, response.showing_end, response.total_count);
1412 - renderLeadsPagination(response.page, response.total_pages);
1413 -
1414 - // Nav badge
1415 - if (response.stats && typeof response.stats.total_leads === 'number') {
1416 - const $badge = $('#mxch-leads-nav-badge');
1417 - if (response.stats.total_leads > 0) {
1418 - $badge.text(response.stats.total_leads).show();
1419 - } else {
1420 - $badge.hide();
1421 - }
1422 - }
1423 - },
1424 - error: function() {
1425 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1426 - }
1427 - });
1428 - }
1429 -
1430 - function renderLeadsStats(stats) {
1431 - $('#mxch-leads-stat-total').text(stats.total_leads || 0);
1432 - $('#mxch-leads-stat-new').text(stats.new_this_week || 0);
1433 - $('#mxch-leads-stat-avg').text(stats.avg_convos || 0);
1434 - const pct = stats.orphan_pct || 0;
1435 - $('#mxch-leads-stat-orphan').text(pct + '%');
1436 - const orphanCount = stats.orphan_count || 0;
1437 - $('#mxch-leads-stat-orphan-sub').text(orphanCount + (orphanCount === 1 ? ' lead captured but never chatted' : ' leads captured but never chatted'));
1438 - }
1439 -
1440 - function renderLeadsTopPages(pages) {
1441 - const $wrap = $('#mxch-leads-toppages-list');
1442 - if (!pages || pages.length === 0) {
1443 - $wrap.html('<div class="mxch-leads-empty-mini">No page data yet.</div>');
1444 - return;
1445 - }
1446 - let html = '';
1447 - pages.forEach(function(p) {
1448 - const isActive = leadsState.filters.pageUrl === p.url ? ' is-active' : '';
1449 - html += `
1450 - <button type="button" class="mxch-leads-toppage-row${isActive}" data-url="${escapeHtmlLeads(p.url)}" data-title="${escapeHtmlLeads(p.title)}">
1451 - <span class="mxch-leads-toppage-title">${escapeHtmlLeads(p.title || p.url)}</span>
1452 - <span class="mxch-leads-toppage-count">${p.lead_count}</span>
1453 - </button>
1454 - `;
1455 - });
1456 - $wrap.html(html);
1457 - }
1458 -
1459 - function renderLeadsTable(rows) {
1460 - const $tbody = $('#mxch-leads-tbody');
1461 - if (!rows || rows.length === 0) {
1462 - $tbody.html(`
1463 - <tr><td colspan="6" class="mxch-leads-empty">
1464 - <div class="mxch-leads-empty-wrap">
1465 - <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
1466 - <p>No leads match the current filters.</p>
1467 - </div>
1468 - </td></tr>
1469 - `);
1470 - return;
1471 - }
1472 -
1473 - let html = '';
1474 - rows.forEach(function(r) {
1475 - const emailKey = (r.email || '').toLowerCase();
1476 - const isChecked = leadsState.selected.has(emailKey) ? ' checked' : '';
1477 - // Status: 'active' (has conversations), 'chat_deleted' (admin removed the chat), 'orphan' (no chat ever).
1478 - const status = r.status || (r.is_orphan ? 'orphan' : 'active');
1479 - const isOrphan = (status === 'orphan');
1480 - const isChatDeleted = (status === 'chat_deleted');
1481 - const nameLine = r.name
1482 - ? `<span class="mxch-leads-lead-name">${escapeHtmlLeads(r.name)}</span>`
1483 - : '';
1484 - const leadCell = `
1485 - <div class="mxch-leads-lead-cell">
1486 - <span class="mxch-leads-lead-email" title="${escapeHtmlLeads(r.email)}">${escapeHtmlLeads(r.email)}</span>
1487 - ${nameLine}
1488 - </div>`;
1489 - let countCell;
1490 - if (isOrphan) {
1491 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-orphan">Orphan</span>`;
1492 - } else if (isChatDeleted) {
1493 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-deleted" title="Chat was deleted by an admin">Chat deleted</span>`;
1494 - } else {
1495 - countCell = `<span class="mxch-leads-pill">${r.conversation_count}</span>`;
1496 - }
1497 - const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : ''));
1498 - const pageCell = r.top_page_url
1499 - ? `<a href="${escapeHtmlLeads(r.top_page_url)}" target="_blank" rel="noopener" class="mxch-leads-page-link" title="${escapeHtmlLeads(r.top_page_url)}">${escapeHtmlLeads(r.top_page_title || r.top_page_url)}</a>`
1500 - : '<span class="mxch-leads-muted">—</span>';
1501 - // View Convo only for active leads (orphans and chat_deleted have no viewable session).
1502 - const viewBtn = (status === 'active' && r.latest_session_id)
1503 - ? `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-leads-view" data-session-id="${escapeHtmlLeads(r.latest_session_id)}" title="View latest conversation">
1504 - <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
1505 - <span>View convo</span>
1506 - </button>`
1507 - : '';
1508 - const deleteBtn = `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-btn-danger-ghost mxch-leads-delete-row" data-email="${escapeHtmlLeads(r.email)}" title="Delete lead">
1509 - <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
1510 - </button>`;
1511 -
1512 - const rowStateClass = isOrphan ? ' is-orphan' : (isChatDeleted ? ' is-chat-deleted' : '');
1513 - html += `
1514 - <tr class="mxch-leads-row${rowStateClass}" data-email="${escapeHtmlLeads(r.email)}">
1515 - <td class="mxch-leads-col-check"><input type="checkbox" class="mxch-leads-rowcheck"${isChecked}></td>
1516 - <td class="mxch-leads-col-lead">${leadCell}</td>
1517 - <td class="mxch-leads-col-count">${countCell}</td>
1518 - <td class="mxch-leads-col-last">${lastCell}</td>
1519 - <td class="mxch-leads-col-page">${pageCell}</td>
1520 - <td class="mxch-leads-col-actions">${viewBtn}${deleteBtn}</td>
1521 - </tr>
1522 - `;
1523 - });
1524 -
1525 - $tbody.html(html);
1526 - updateLeadsSelectionUI();
1527 - }
1528 -
1529 - function renderLeadsCount(start, end, total) {
1530 - if (!total) {
1531 - $('#mxch-leads-count').text('0 leads');
1532 - } else {
1533 - $('#mxch-leads-count').text(start + '-' + end + ' / ' + total + ' leads');
1534 - }
1535 - }
1536 -
1537 - function renderLeadsPagination(currentPage, totalPages) {
1538 - const $c = $('#mxch-leads-pagination');
1539 - if (!totalPages || totalPages <= 1) { $c.html(''); return; }
1540 - let html = '<div class="mxch-pagination-btns">';
1541 - if (currentPage > 1) {
1542 - html += `<button class="mxch-page-btn" data-page="${currentPage - 1}">&laquo;</button>`;
1543 - }
1544 - html += `<span class="mxch-page-info">${currentPage} / ${totalPages}</span>`;
1545 - if (currentPage < totalPages) {
1546 - html += `<button class="mxch-page-btn" data-page="${currentPage + 1}">&raquo;</button>`;
1547 - }
1548 - html += '</div>';
1549 - $c.html(html);
1550 - }
1551 -
1552 - function updateLeadsSelectionUI() {
1553 - const count = leadsState.selected.size;
1554 - const $countEl = $('#mxch-leads-selected-count');
1555 - const $del = $('#mxch-leads-delete-selected');
1556 - if (count > 0) {
1557 - $countEl.text(count + ' selected').addClass('has-selection');
1558 - $del.prop('disabled', false);
1559 - } else {
1560 - $countEl.text('0').removeClass('has-selection');
1561 - $del.prop('disabled', true);
1562 - }
1563 - // Selected-scope export menu items
1564 - $('#mxch-leads-export-menu button[data-scope="selected"]').prop('disabled', count === 0);
1565 -
1566 - // Select-all checkbox state
1567 - const $checks = $('.mxch-leads-rowcheck');
1568 - const checked = $checks.filter(':checked').length;
1569 - const total = $checks.length;
1570 - $('#mxch-leads-select-all').prop('checked', total > 0 && checked === total);
1571 - $('#mxch-leads-select-all').prop('indeterminate', checked > 0 && checked < total);
1572 - }
1573 -
1574 - // Trigger leads load when switching to the tab (works alongside the main nav handler above).
1575 - $('.mxch-nav-link[data-target="leads"], .mxch-mobile-nav-link[data-target="leads"]').on('click', function() {
1576 - if (!leadsState.loaded) {
1577 - loadLeads(1);
1578 - }
1579 - });
1580 -
1581 - // Filter: search (debounced)
1582 - let leadsSearchTimer;
1583 - $('#mxch-leads-search').on('input', function() {
1584 - clearTimeout(leadsSearchTimer);
1585 - const val = $(this).val();
1586 - leadsSearchTimer = setTimeout(function() {
1587 - leadsState.filters.search = (val || '').trim();
1588 - updateClearFiltersButton();
1589 - loadLeads(1);
1590 - }, 300);
1591 - });
1592 -
1593 - // Filter: date range
1594 - $('#mxch-leads-date-range').on('change', function() {
1595 - leadsState.filters.dateRange = $(this).val();
1596 - updateClearFiltersButton();
1597 - loadLeads(1);
1598 - });
1599 -
1600 - // Filter: status
1601 - $('#mxch-leads-status').on('change', function() {
1602 - leadsState.filters.status = $(this).val();
1603 - updateClearFiltersButton();
1604 - loadLeads(1);
1605 - });
1606 -
1607 - // Clear filters
1608 - $('#mxch-leads-clear-filters').on('click', function() {
1609 - leadsState.filters = { search: '', dateRange: 'all', status: 'all', pageUrl: '', pageTitle: '' };
1610 - $('#mxch-leads-search').val('');
1611 - $('#mxch-leads-date-range').val('all');
1612 - $('#mxch-leads-status').val('all');
1613 - setPageFilterChip('', '');
1614 - loadLeads(1);
1615 - });
1616 -
1617 - // Remove page chip
1618 - $leads().on('click', '.mxch-leads-page-chip-remove', function() {
1619 - setPageFilterChip('', '');
1620 - loadLeads(1);
1621 - });
1622 -
1623 - // Top Pages click -> set filter
1624 - $leads().on('click', '.mxch-leads-toppage-row', function() {
1625 - const url = $(this).data('url') || '';
1626 - const title = $(this).data('title') || '';
1627 - setPageFilterChip(url, title);
1628 - loadLeads(1);
1629 - });
1630 -
1631 - // Pagination click
1632 - $leads().on('click', '#mxch-leads-pagination .mxch-page-btn', function() {
1633 - const p = parseInt($(this).data('page'), 10);
1634 - if (p > 0) loadLeads(p);
1635 - });
1636 -
1637 - // Select-all
1638 - $('#mxch-leads-select-all').on('change', function() {
1639 - const on = $(this).is(':checked');
1640 - $('.mxch-leads-rowcheck').prop('checked', on);
1641 - $('.mxch-leads-row').each(function() {
1642 - const email = ($(this).data('email') || '').toString().toLowerCase();
1643 - if (on) {
1644 - leadsState.selected.add(email);
1645 - } else {
1646 - leadsState.selected.delete(email);
1647 - }
1648 - });
1649 - updateLeadsSelectionUI();
1650 - });
1651 -
1652 - // Row checkbox
1653 - $leads().on('change', '.mxch-leads-rowcheck', function() {
1654 - const email = ($(this).closest('.mxch-leads-row').data('email') || '').toString().toLowerCase();
1655 - if ($(this).is(':checked')) {
1656 - leadsState.selected.add(email);
1657 - } else {
1658 - leadsState.selected.delete(email);
1659 - }
1660 - updateLeadsSelectionUI();
1661 - });
1662 -
1663 - // View convo -> jump to All Chats tab and open the session
1664 - $leads().on('click', '.mxch-leads-view', function() {
1665 - const sid = $(this).attr('data-session-id');
1666 - if (!sid) return;
1667 - $('.mxch-nav-link[data-target="all-chats"]').trigger('click');
1668 - // selectChat is defined earlier in this closure
1669 - if (typeof selectChat === 'function') {
1670 - setTimeout(function() { selectChat(sid); }, 30);
1671 - }
1672 - });
1673 -
1674 - // Row delete -> confirm for one
1675 - $leads().on('click', '.mxch-leads-delete-row', function() {
1676 - const email = $(this).data('email');
1677 - if (!email) return;
1678 - openLeadsConfirm([String(email)]);
1679 - });
1680 -
1681 - // Bulk delete -> confirm for N
1682 - $('#mxch-leads-delete-selected').on('click', function() {
1683 - if (leadsState.selected.size === 0) return;
1684 - openLeadsConfirm(Array.from(leadsState.selected));
1685 - });
1686 -
1687 - function openLeadsConfirm(emails) {
1688 - leadsState.pendingDelete = emails;
1689 - const count = emails.length;
1690 - const msg = count === 1
1691 - ? 'Delete lead "' + emails[0] + '" and all of their conversations?'
1692 - : 'Delete ' + count + ' leads and all of their conversations?';
1693 - $('#mxch-leads-confirm-body').text(msg);
1694 - $('#mxch-leads-confirm').fadeIn(120);
1695 - }
1696 -
1697 - function closeLeadsConfirm() {
1698 - $('#mxch-leads-confirm').fadeOut(120);
1699 - leadsState.pendingDelete = [];
1700 - }
1701 -
1702 - $leads().on('click', '[data-mxch-leads-close]', closeLeadsConfirm);
1703 -
1704 - $('#mxch-leads-confirm-go').on('click', function() {
1705 - const emails = leadsState.pendingDelete.slice();
1706 - if (!emails.length) { closeLeadsConfirm(); return; }
1707 -
1708 - const $btn = $(this).prop('disabled', true).text('Deleting...');
1709 -
1710 - $.ajax({
1711 - url: ajaxurl,
1712 - type: 'POST',
1713 - data: {
1714 - action: 'mxchat_delete_leads',
1715 - security: $('#mxchat_leads_delete_nonce').val(),
1716 - emails: emails
1717 - },
1718 - success: function(response) {
1719 - $btn.prop('disabled', false).text('Delete permanently');
1720 - closeLeadsConfirm();
1721 - if (response && response.success) {
1722 - emails.forEach(function(e) { leadsState.selected.delete(e.toLowerCase()); });
1723 - loadLeads(leadsState.page);
1724 - } else {
1725 - alert((response && response.data && response.data.message) || 'Failed to delete leads.');
1726 - }
1727 - },
1728 - error: function() {
1729 - $btn.prop('disabled', false).text('Delete permanently');
1730 - alert('Network error while deleting.');
1731 - }
1732 - });
1733 - });
1734 -
1735 - // Export dropdown
1736 - $('#mxch-leads-export-btn').on('click', function(e) {
1737 - e.stopPropagation();
1738 - $('#mxch-leads-export-menu').toggleClass('is-open');
1739 - });
1740 -
1741 - $(document).on('click', function() {
1742 - $('#mxch-leads-export-menu').removeClass('is-open');
1743 - });
1744 -
1745 - $('#mxch-leads-export-menu').on('click', function(e) { e.stopPropagation(); });
1746 -
1747 - $('#mxch-leads-export-menu button').on('click', function() {
1748 - if ($(this).prop('disabled')) return;
1749 - const scope = $(this).data('scope') || 'all';
1750 - const fields = $(this).data('fields') || 'email_and_name';
1751 - submitLeadsExport(scope, fields);
1752 - $('#mxch-leads-export-menu').removeClass('is-open');
1753 - });
1754 -
1755 - function submitLeadsExport(scope, fields) {
1756 - const $form = $('<form>', { method: 'POST', action: ajaxurl, style: 'display:none;' });
1757 - $form.append($('<input>', { type: 'hidden', name: 'action', value: 'mxchat_export_leads' }));
1758 - $form.append($('<input>', { type: 'hidden', name: 'security', value: $('#mxchat_leads_export_nonce').val() }));
1759 - $form.append($('<input>', { type: 'hidden', name: 'scope', value: scope }));
1760 - $form.append($('<input>', { type: 'hidden', name: 'fields', value: fields }));
1761 - if (scope === 'selected') {
1762 - Array.from(leadsState.selected).forEach(function(e) {
1763 - $form.append($('<input>', { type: 'hidden', name: 'emails[]', value: e }));
1764 - });
1765 - }
1766 - $form.appendTo('body').submit().remove();
1767 - }
1768 -
1769 - // Preload leads metadata on page load (for the nav badge count only) without rendering.
1770 - // We keep this light — the full fetch only runs when the tab is clicked.
1771 - $.ajax({
1772 - url: ajaxurl,
1773 - type: 'POST',
1774 - data: { action: 'mxchat_fetch_leads', page: 1, per_page: 1 },
1775 - success: function(response) {
1776 - if (response && response.success && response.stats) {
1777 - const total = response.stats.total_leads || 0;
1778 - const $badge = $('#mxch-leads-nav-badge');
1779 - if (total > 0) $badge.text(total).show();
1780 - }
1781 - }
1782 - });
1783 -});
438 +});