PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.2
MxChat – AI Chatbot & Content Generation for WordPress v3.2.2
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / js / mxchat_transcripts.js

mxchat_transcripts.js in MxChat – AI Chatbot & Content Generation for WordPress 3.2.2, at js/mxchat_transcripts.js

1,784 lines 71.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * MxChat Transcripts Page JavaScript - v3.0
3 * Split-panel layout with chat list and conversation view
4 */
5 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
140 let currentPage = 1;
141 const perPage = 50;
142 let totalPages = 1;
143 let currentSessionId = null;
144 let transcriptsLoaded = false;
145 let selectedSessions = new Set();
146 let currentSortOrder = 'desc'; // newest first
147
148 // Load chat list on page load
149 loadChatList(1, '');
150
151 // Search functionality with debounce
152 let searchTimeout;
153 $('#mxch-search-transcripts').on('input', function() {
154 clearTimeout(searchTimeout);
155 const searchTerm = $(this).val().toLowerCase();
156
157 searchTimeout = setTimeout(function() {
158 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');
188 } else {
189 selectedSessions.clear();
190 $('.mxch-chat-item').removeClass('selected');
191 $('#mxch-chat-list').removeClass('selection-mode');
192 }
193
194 updateSelectionUI();
195 });
196
197 // Update selection UI
198 function updateSelectionUI() {
199 const count = selectedSessions.size;
200 const $countEl = $('#mxch-selected-count');
201 const $deleteBtn = $('#mxch-delete-selected');
202
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) {
267 $.ajax({
268 url: ajaxurl,
269 type: 'POST',
270 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 action: 'mxchat_fetch_chat_history',
324 page: page,
325 per_page: perPage,
326 search: searchTerm,
327 sort_order: currentSortOrder
328 },
329 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 }
343 },
344 error: function() {
345 $container.html('<div class="mxch-list-empty"><p>Error loading chats</p></div>');
346 }
347 });
348 }
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');
391 }
392
393 updateSelectionUI();
394 });
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>');
496 }
497 });
498 }
499
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);
506
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 `;
550 } 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 `;
566 }
567 });
568
569 $('#mxch-messages-area').html(messagesHtml);
570
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');
579 if (messageId) {
580 openRagContextModal(messageId);
581 }
582 });
583 }
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');
593 } else {
594 $drawer.slideDown(200);
595 $btn.addClass('active');
596 }
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) {
607 $.ajax({
608 url: ajaxurl,
609 type: 'POST',
610 data: {
611 action: 'mxchat_delete_chat_history',
612 delete_session_ids: [sessionId],
613 also_delete_lead: alsoDeleteLead ? '1' : '0',
614 security: $('#mxchat_delete_chat_nonce').val()
615 },
616 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.');
634 }
635 },
636 error: function() {
637 alert('An error occurred while deleting the conversation.');
638 }
639 });
640 }
641
642 // ==========================================================================
643 // Export Functionality
644 // ==========================================================================
645
646 $('#mxch-export-btn, #mxch-export-current').on('click', function() {
647 const $button = $(this);
648 $button.prop('disabled', true).addClass('loading');
649
650 const $form = $('<form>', {
651 method: 'post',
652 action: ajaxurl
653 });
654
655 $form.append($('<input>', {
656 type: 'hidden',
657 name: 'action',
658 value: 'mxchat_export_transcripts'
659 }));
660
661 $form.append($('<input>', {
662 type: 'hidden',
663 name: 'security',
664 value: mxchatAdmin.export_nonce
665 }));
666
667 $form.appendTo('body').submit();
668
669 setTimeout(function() {
670 $button.prop('disabled', false).removeClass('loading');
671 }, 2000);
672 });
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());
692 });
693
694 // Apply translations to messages
695 function applyTranslations(translations) {
696 // Store original messages if not already stored
697 if (!originalMessages) {
698 originalMessages = [];
699 $('#mxch-messages-area .mxch-message-bubble').each(function() {
700 originalMessages.push($(this).html());
701 });
702 }
703
704 // Apply translations
705 translations.forEach(function(item) {
706 const $bubble = $('#mxch-messages-area .mxch-message-bubble').eq(item.index);
707 if ($bubble.length) {
708 $bubble.html(item.translated);
709 $bubble.addClass('translated');
710 }
711 });
712
713 isTranslated = true;
714 $('#mxch-show-original-btn').show();
715 }
716
717 // Load saved translation for current session
718 function loadSavedTranslation(sessionId) {
719 $.ajax({
720 url: ajaxurl,
721 type: 'POST',
722 data: {
723 action: 'mxchat_get_transcript_translation',
724 session_id: sessionId
725 },
726 success: function(response) {
727 if (response.success && response.has_translation) {
728 currentTranslationLang = response.language;
729 applyTranslations(response.translations);
730 // Update language selector to show saved language
731 $('#mxch-translate-lang').val(response.language);
732 }
733 }
734 });
735 }
736
737 // Translate button click handler
738 $('#mxch-translate-btn').on('click', function() {
739 if (!currentSessionId) return;
740
741 const $btn = $(this);
742 const targetLang = $('#mxch-translate-lang').val();
743
744 // Disable button and show loading state
745 $btn.prop('disabled', true);
746 $btn.find('.mxch-translate-text').text('Translating...');
747 $btn.find('svg').addClass('mxch-translate-spinner');
748
749 // Store original messages before translation
750 if (!originalMessages) {
751 originalMessages = [];
752 $('#mxch-messages-area .mxch-message-bubble').each(function() {
753 originalMessages.push($(this).html());
754 });
755 }
756
757 // If already translated, restore originals first before re-translating
758 if (isTranslated) {
759 $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
760 if (originalMessages[index]) {
761 $(this).html(originalMessages[index]);
762 $(this).removeClass('translated');
763 }
764 });
765 }
766
767 // Collect all message content (from originals)
768 const messages = [];
769 originalMessages.forEach(function(html, index) {
770 // Create temp element to get text content
771 const $temp = $('<div>').html(html);
772 messages.push({
773 index: index,
774 content: $temp.text().trim()
775 });
776 });
777
778 // Send translation request
779 $.ajax({
780 url: ajaxurl,
781 type: 'POST',
782 data: {
783 action: 'mxchat_translate_messages',
784 session_id: currentSessionId,
785 target_lang: targetLang,
786 messages: JSON.stringify(messages),
787 security: mxchatAdmin.translate_nonce || ''
788 },
789 success: function(response) {
790 if (response.success && response.translations) {
791 currentTranslationLang = response.language;
792 applyTranslations(response.translations);
793 $btn.find('.mxch-translate-text').text('Translate');
794 } else {
795 alert(response.error || 'Translation failed. Please try again.');
796 $btn.find('.mxch-translate-text').text('Translate');
797 }
798 },
799 error: function() {
800 alert('Translation request failed. Please try again.');
801 $btn.find('.mxch-translate-text').text('Translate');
802 },
803 complete: function() {
804 $btn.prop('disabled', false);
805 $btn.find('svg').removeClass('mxch-translate-spinner');
806 }
807 });
808 });
809
810 // Show original button click handler
811 $('#mxch-show-original-btn').on('click', function() {
812 if (!originalMessages) return;
813
814 // Restore original messages
815 $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
816 if (originalMessages[index]) {
817 $(this).html(originalMessages[index]);
818 $(this).removeClass('translated');
819 }
820 });
821
822 isTranslated = false;
823 $(this).hide();
824 });
825
826 // Reset translation state (called when selecting new chat)
827 function resetTranslationState() {
828 originalMessages = null;
829 isTranslated = false;
830 currentTranslationLang = null;
831 $('#mxch-show-original-btn').hide();
832 }
833
834 // Make functions available to selectChat
835 window.resetTranslationState = resetTranslationState;
836 window.loadSavedTranslation = loadSavedTranslation;
837
838 // ==========================================================================
839 // RAG Context Modal (Sources & Actions Tabs)
840 // ==========================================================================
841
842 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');
847
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);
858 $loading.show();
859 $sourcesContent.html('');
860 $actionsContent.html('');
861
862 $.ajax({
863 url: ajaxurl,
864 type: 'POST',
865 data: {
866 action: 'mxchat_get_rag_context',
867 message_id: messageId
868 },
869 success: function(response) {
870 $loading.hide();
871
872 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 }
889 } 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>');
892 }
893 },
894 error: function() {
895 $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>');
898 }
899 });
900 }
901
902 // Tab switching
903 $(document).on('click', '.mxch-context-tab', function() {
904 const $tab = $(this);
905 const tabName = $tab.data('tab');
906
907 // Update active tab
908 $('.mxch-context-tab').removeClass('active');
909 $tab.addClass('active');
910
911 // Show/hide content
912 $('.mxch-tab-content').hide();
913 $('#mxch-tab-' + tabName).show();
914 });
915
916 function renderRagContext(data, $container) {
917 let html = '';
918
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>';
930 html += '</div>';
931
932 const groupedByUrl = {};
933
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 }
945
946 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
947 groupedByUrl[url].bestScore = match.similarity_percentage;
948 }
949
950 if (match.used_for_context) {
951 groupedByUrl[url].usedForContext = true;
952 }
953
954 groupedByUrl[url].matchedChunks.push({
955 chunkIndex: match.chunk_index,
956 score: match.similarity_percentage,
957 usedForContext: match.used_for_context
958 });
959 });
960
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' : '';
964
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>';
968
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 html += '</div>';
984
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 }
991 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 }
1008
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 $container.html(html);
1067 }
1068
1069 function escapeHtml(text) {
1070 if (!text) return '';
1071 const div = document.createElement('div');
1072 div.textContent = text;
1073 return div.innerHTML;
1074 }
1075
1076 // Close RAG modal
1077 $('.mxch-modal-close').on('click', function() {
1078 $(this).closest('.mxch-modal-overlay').fadeOut(200);
1079 });
1080
1081 $('.mxch-modal-overlay').on('click', function(e) {
1082 if ($(e.target).is('.mxch-modal-overlay')) {
1083 $(this).fadeOut(200);
1084 }
1085 });
1086
1087 $(document).on('keydown', function(e) {
1088 if (e.key === 'Escape') {
1089 $('.mxch-modal-overlay').fadeOut(200);
1090 }
1091 });
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 });
1784