PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.15
MxChat – AI Chatbot & Content Generation for WordPress v3.2.15
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.15, at js/mxchat_transcripts.js

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