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

1,978 lines 84.1 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 // 470f68: the Actions tab now carries two mechanisms — tools
948 // that ran and trigger phrases that scored. Badge counts both.
949 const toolCallsCount = response.data.tool_calls ? response.data.tool_calls.length : 0;
950 const actionsCount = (response.data.action_analysis ? response.data.action_analysis.length : 0) + toolCallsCount;
951
952 if (sourcesCount > 0) {
953 $('#mxch-sources-count').text(sourcesCount).show();
954 }
955 if (actionsCount > 0) {
956 $('#mxch-actions-count').text(actionsCount).show();
957 }
958 } else {
959 $sourcesContent.html('<div class="mxch-rag-error">Unable to load document context.</div>');
960 $actionsContent.html('<div class="mxch-rag-error">No action data available.</div>');
961 }
962 },
963 error: function() {
964 $loading.hide();
965 $sourcesContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
966 $actionsContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
967 }
968 });
969 }
970
971 // Tab switching
972 $(document).on('click', '.mxch-context-tab', function() {
973 const $tab = $(this);
974 const tabName = $tab.data('tab');
975
976 // Update active tab
977 $('.mxch-context-tab').removeClass('active');
978 $tab.addClass('active');
979
980 // Show/hide content
981 $('.mxch-tab-content').hide();
982 $('#mxch-tab-' + tabName).show();
983 });
984
985 function renderRagContext(data, $container) {
986 let html = '';
987
988 // 67fc92: "the KB was searched and nothing matched" and "nothing was
989 // recorded for this row" are different facts. Retrieval keys present
990 // means the search ran and was recorded (top_matches may be empty);
991 // no retrieval keys means there is simply no record (older rows, or
992 // turns that never consulted the knowledge base).
993 const retrievalRecorded = typeof data.total_documents_checked !== 'undefined'
994 || (data.top_matches && data.top_matches.length > 0);
995
996 if (!retrievalRecorded) {
997 html += '<div class="mxch-no-results"><p>No retrieval data was recorded for this response.</p></div>';
998 $container.html(html);
999 return;
1000 }
1001
1002 // Hybrid keyword boost (38ffa1): rows carry matched_via + fused_rank
1003 // when hybrid retrieval was on for this response. Cosine % stays the
1004 // anchor; the chip explains WHY a low-% row ranked high.
1005 const topMatches = data.top_matches || [];
1006 const hybridOn = topMatches.some(function(m) { return m.matched_via; });
1007
1008 html += '<div class="mxch-rag-summary">';
1009 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>';
1010 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>';
1011 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>';
1012 if (hybridOn) {
1013 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Retrieval:</span> <span class="mxch-rag-value">Hybrid</span></div>';
1014 }
1015 html += '</div>';
1016
1017 if (topMatches.length === 0) {
1018 html += '<div class="mxch-no-results"><p>The knowledge base was searched &mdash; no documents matched this response.</p></div>';
1019 $container.html(html);
1020 return;
1021 }
1022
1023 const groupedByUrl = {};
1024
1025 data.top_matches.forEach(function(match) {
1026 const url = match.source_display || 'Unknown';
1027 if (!groupedByUrl[url]) {
1028 groupedByUrl[url] = {
1029 url: url,
1030 isUrl: url.startsWith('http'),
1031 bestScore: 0,
1032 usedForContext: false,
1033 matchedChunks: [],
1034 bestFusedRank: Infinity,
1035 viaSet: {}
1036 };
1037 }
1038
1039 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
1040 groupedByUrl[url].bestScore = match.similarity_percentage;
1041 }
1042
1043 if (match.used_for_context) {
1044 groupedByUrl[url].usedForContext = true;
1045 }
1046
1047 if (match.fused_rank && match.fused_rank < groupedByUrl[url].bestFusedRank) {
1048 groupedByUrl[url].bestFusedRank = match.fused_rank;
1049 }
1050 if (match.matched_via) {
1051 groupedByUrl[url].viaSet[match.matched_via] = true;
1052 }
1053
1054 groupedByUrl[url].matchedChunks.push({
1055 chunkIndex: match.chunk_index,
1056 score: match.similarity_percentage,
1057 usedForContext: match.used_for_context
1058 });
1059 });
1060
1061 // Hybrid on: order by fused rank (rows without one sort last);
1062 // otherwise by cosine, exactly as before.
1063 const urlGroups = Object.values(groupedByUrl).sort(function(a, b) {
1064 if (hybridOn && a.bestFusedRank !== b.bestFusedRank) {
1065 return a.bestFusedRank - b.bestFusedRank;
1066 }
1067 return b.bestScore - a.bestScore;
1068 });
1069 const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length;
1070 const chunksInfo = data.total_chunks_used > 0 ? data.total_chunks_used + ' chunks sent to AI' : '';
1071
1072 html += '<div class="mxch-rag-matches">';
1073 html += '<h3>Retrieved Documents</h3>';
1074 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>';
1075
1076 urlGroups.forEach(function(group) {
1077 const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
1078 const statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
1079 const statusLabel = group.usedForContext ? 'Used' : 'Not Used';
1080
1081 html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1082 html += '<div class="mxch-rag-match-header">';
1083 html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
1084
1085 if (hybridOn) {
1086 const vias = Object.keys(group.viaSet);
1087 if (vias.length) {
1088 const viaLabel = (vias.length > 1 || vias[0] === 'both') ? 'Both'
1089 : (vias[0] === 'keyword' ? 'Keyword' : 'Vector');
1090 html += '<span class="mxch-rag-via-chip mxch-rag-via-' + viaLabel.toLowerCase() + '">' + viaLabel + '</span>';
1091 }
1092 }
1093
1094 if (group.matchedChunks.length > 1) {
1095 html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
1096 }
1097
1098 html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
1099 html += '</div>';
1100
1101 html += '<div class="mxch-rag-match-source">';
1102 if (group.isUrl) {
1103 html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>';
1104 } else {
1105 html += escapeHtml(group.url);
1106 }
1107 html += '</div>';
1108 html += '</div>';
1109 });
1110
1111 html += '</div>';
1112 $container.html(html);
1113 }
1114
1115 // AI Tools trace (470f68): one card per tool EXECUTION for this message.
1116 // Rendered ABOVE the trigger-phrase scores — a tool that actually ran
1117 // outranks a phrase that merely scored.
1118 function renderToolCalls(toolCalls) {
1119 const failed = toolCalls.filter(function(t) { return !t.ok; }).length;
1120
1121 let html = '<div class="mxch-rag-summary">';
1122 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Tools Used:</span> <span class="mxch-rag-value">' + toolCalls.length + '</span></div>';
1123 if (failed > 0) {
1124 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Failed:</span> <span class="mxch-rag-value" style="color: #ef4444; font-weight: 600;">' + failed + '</span></div>';
1125 }
1126 html += '</div>';
1127
1128 html += '<div class="mxch-rag-matches">';
1129 html += '<h3>AI Tools</h3>';
1130 html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">Tools the assistant chose and ran for this message, in order.</p>';
1131
1132 toolCalls.forEach(function(tool) {
1133 const ok = !!tool.ok;
1134 const cardClass = ok ? 'mxch-rag-match-used' : 'mxch-tool-failed';
1135 const statusClass = ok ? 'status-used' : 'status-failed';
1136 const statusIcon = ok ? '&#10003;' : '&#10007;';
1137 const statusLabel = ok ? 'Ran' : 'Failed';
1138
1139 html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1140 html += '<div class="mxch-rag-match-header">';
1141 html += '<span class="mxch-rag-match-score">&#9889;</span>';
1142 if (typeof tool.ms === 'number') {
1143 html += '<span class="mxch-tool-duration">' + tool.ms + ' ms</span>';
1144 }
1145 html += '<span class="mxch-rag-match-status ' + statusClass + '">' + statusIcon + ' ' + statusLabel + '</span>';
1146 html += '</div>';
1147
1148 html += '<div class="mxch-action-details">';
1149 html += '<div class="mxch-action-label">' + escapeHtml(tool.label || tool.name) + '</div>';
1150 html += '<div class="mxch-action-callback"><span class="mxch-action-callback-label">Tool:</span> ' + escapeHtml(tool.name) + '</div>';
1151 html += '</div>';
1152
1153 if (!ok && tool.error) {
1154 html += '<div class="mxch-tool-error">' + escapeHtml(tool.error) + '</div>';
1155 }
1156
1157 if (tool.args_redacted === 'sensitive') {
1158 html += '<div class="mxch-tool-redacted">Arguments not recorded &mdash; this tool is marked sensitive.</div>';
1159 } else if (tool.args_excerpt) {
1160 html += '<details class="mxch-tool-args"><summary>Arguments</summary>';
1161 html += '<div class="mxch-tool-args-body">' + escapeHtml(tool.args_excerpt) + '</div></details>';
1162 }
1163
1164 html += '</div>';
1165 });
1166
1167 html += '</div>';
1168 return html;
1169 }
1170
1171 function renderActionsContext(data, $container) {
1172 let html = '';
1173
1174 const toolCalls = (data.tool_calls && data.tool_calls.length) ? data.tool_calls : [];
1175 const actions = (data.action_analysis && data.action_analysis.length) ? data.action_analysis : [];
1176
1177 // 95d79d's empty state now shows only when NEITHER mechanism produced
1178 // anything — the string itself is unchanged.
1179 if (toolCalls.length === 0 && actions.length === 0) {
1180 html += '<div class="mxch-no-results"><p>No trigger-phrase analysis for this message.</p><p style="color: var(--mxch-text-secondary); font-size: 13px; margin-top: 8px;">This panel shows how your <strong>Trigger Phrases</strong> scored &mdash; it stays empty if you have none enabled for this bot, or if the answer came from <strong>AI Tools</strong>, which don\'t produce similarity scores. It\'s recorded when the answer is generated, so it won\'t appear on older conversations.</p></div>';
1181 $container.html(html);
1182 return;
1183 }
1184
1185 if (toolCalls.length > 0) {
1186 html += renderToolCalls(toolCalls);
1187 }
1188
1189 if (actions.length === 0) {
1190 $container.html(html);
1191 return;
1192 }
1193 const triggeredAction = actions.find(a => a.triggered);
1194 const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
1195
1196 // Summary section
1197 html += '<div class="mxch-rag-summary">';
1198 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Actions Evaluated:</span> <span class="mxch-rag-value">' + actions.length + '</span></div>';
1199 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Above Threshold:</span> <span class="mxch-rag-value">' + actionsAboveThreshold + '</span></div>';
1200 if (triggeredAction) {
1201 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>';
1202 }
1203 html += '</div>';
1204
1205 // Actions list
1206 html += '<div class="mxch-rag-matches">';
1207 html += '<h3>Action Scores</h3>';
1208 html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">Showing all evaluated actions sorted by similarity score</p>';
1209
1210 actions.forEach(function(action) {
1211 let cardClass = 'mxch-rag-match-below';
1212 let statusIcon = '&#10007;';
1213 let statusLabel = 'Below Threshold';
1214
1215 if (action.triggered) {
1216 cardClass = 'mxch-action-triggered';
1217 statusIcon = '&#9889;';
1218 statusLabel = 'Triggered';
1219 } else if (action.above_threshold) {
1220 cardClass = 'mxch-rag-match-used';
1221 statusIcon = '&#10003;';
1222 statusLabel = 'Above Threshold';
1223 }
1224
1225 html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1226 html += '<div class="mxch-rag-match-header">';
1227 html += '<span class="mxch-rag-match-score">' + action.similarity_percentage + '%</span>';
1228 html += '<span class="mxch-action-threshold-badge">Threshold: ' + action.threshold_percentage + '%</span>';
1229 html += '<span class="mxch-rag-match-status ' + (action.triggered ? 'status-triggered' : (action.above_threshold ? 'status-used' : 'status-below')) + '">' + statusIcon + ' ' + statusLabel + '</span>';
1230 html += '</div>';
1231
1232 html += '<div class="mxch-action-details">';
1233 html += '<div class="mxch-action-label">' + escapeHtml(action.intent_label) + '</div>';
1234 html += '<div class="mxch-action-callback"><span class="mxch-action-callback-label">Callback:</span> ' + escapeHtml(action.callback_function) + '</div>';
1235 html += '</div>';
1236
1237 // Score bar visualization
1238 const scoreBarWidth = Math.min(action.similarity_percentage, 100);
1239 const thresholdPos = Math.min(action.threshold_percentage, 100);
1240 html += '<div class="mxch-action-score-bar">';
1241 html += '<div class="mxch-action-score-fill" style="width: ' + scoreBarWidth + '%;"></div>';
1242 html += '<div class="mxch-action-threshold-marker" style="left: ' + thresholdPos + '%;"></div>';
1243 html += '</div>';
1244
1245 html += '</div>';
1246 });
1247
1248 html += '</div>';
1249 $container.html(html);
1250 }
1251
1252 function escapeHtml(text) {
1253 if (!text) return '';
1254 const div = document.createElement('div');
1255 div.textContent = text;
1256 return div.innerHTML;
1257 }
1258
1259 // Close RAG modal
1260 $('.mxch-modal-close').on('click', function() {
1261 $(this).closest('.mxch-modal-overlay').fadeOut(200);
1262 });
1263
1264 $('.mxch-modal-overlay').on('click', function(e) {
1265 if ($(e.target).is('.mxch-modal-overlay')) {
1266 $(this).fadeOut(200);
1267 }
1268 });
1269
1270 $(document).on('keydown', function(e) {
1271 if (e.key === 'Escape') {
1272 $('.mxch-modal-overlay').fadeOut(200);
1273 }
1274 });
1275
1276 // ==========================================================================
1277 // Activity Chart
1278 // ==========================================================================
1279
1280 // Simple chart implementation (no external dependencies)
1281 class SimpleChart {
1282 constructor(canvas, config) {
1283 this.canvas = canvas;
1284 this.ctx = canvas.getContext('2d');
1285 this.config = config;
1286 this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
1287 this.render();
1288 }
1289
1290 destroy() {
1291 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1292 }
1293
1294 render() {
1295 const dpr = window.devicePixelRatio || 1;
1296 const rect = this.canvas.getBoundingClientRect();
1297
1298 this.canvas.width = rect.width * dpr;
1299 this.canvas.height = rect.height * dpr;
1300 this.ctx.scale(dpr, dpr);
1301
1302 this.canvas.style.width = rect.width + 'px';
1303 this.canvas.style.height = rect.height + 'px';
1304
1305 const width = rect.width - this.padding.left - this.padding.right;
1306 const height = rect.height - this.padding.top - this.padding.bottom;
1307
1308 // Find max value
1309 let maxValue = 0;
1310 this.config.datasets.forEach(dataset => {
1311 const max = Math.max(...dataset.data);
1312 if (max > maxValue) maxValue = max;
1313 });
1314
1315 // Add some padding to max value
1316 maxValue = Math.ceil(maxValue * 1.1);
1317 if (maxValue === 0) maxValue = 10;
1318
1319 // Draw grid lines
1320 this.ctx.strokeStyle = '#e5e7eb';
1321 this.ctx.lineWidth = 1;
1322 const gridLines = 5;
1323
1324 for (let i = 0; i <= gridLines; i++) {
1325 const y = this.padding.top + (height / gridLines) * i;
1326 this.ctx.beginPath();
1327 this.ctx.moveTo(this.padding.left, y);
1328 this.ctx.lineTo(this.padding.left + width, y);
1329 this.ctx.stroke();
1330
1331 // Draw y-axis labels
1332 const value = maxValue - (maxValue / gridLines) * i;
1333 this.ctx.fillStyle = '#6b7280';
1334 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1335 this.ctx.textAlign = 'right';
1336 this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
1337 }
1338
1339 // Draw datasets
1340 this.config.datasets.forEach(dataset => {
1341 const points = [];
1342 const xStep = width / (this.config.labels.length - 1 || 1);
1343
1344 dataset.data.forEach((value, index) => {
1345 const x = this.padding.left + (xStep * index);
1346 const y = this.padding.top + height - (value / maxValue * height);
1347 points.push({ x, y, value });
1348 });
1349
1350 // Draw filled area
1351 if (dataset.fill && dataset.backgroundColor) {
1352 this.ctx.fillStyle = dataset.backgroundColor;
1353 this.ctx.beginPath();
1354 this.ctx.moveTo(points[0].x, this.padding.top + height);
1355 points.forEach(point => {
1356 this.ctx.lineTo(point.x, point.y);
1357 });
1358 this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
1359 this.ctx.closePath();
1360 this.ctx.fill();
1361 }
1362
1363 // Draw line
1364 this.ctx.strokeStyle = dataset.borderColor;
1365 this.ctx.lineWidth = 3;
1366 this.ctx.lineCap = 'round';
1367 this.ctx.lineJoin = 'round';
1368
1369 this.ctx.beginPath();
1370 points.forEach((point, index) => {
1371 if (index === 0) {
1372 this.ctx.moveTo(point.x, point.y);
1373 } else {
1374 this.ctx.lineTo(point.x, point.y);
1375 }
1376 });
1377 this.ctx.stroke();
1378
1379 // Draw points
1380 points.forEach(point => {
1381 this.ctx.fillStyle = '#ffffff';
1382 this.ctx.beginPath();
1383 this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
1384 this.ctx.fill();
1385 this.ctx.strokeStyle = dataset.borderColor;
1386 this.ctx.lineWidth = 2;
1387 this.ctx.stroke();
1388 });
1389 });
1390
1391 // Draw x-axis labels
1392 const xStep = width / (this.config.labels.length - 1 || 1);
1393 this.ctx.fillStyle = '#6b7280';
1394 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1395 this.ctx.textAlign = 'center';
1396
1397 this.config.labels.forEach((label, index) => {
1398 const x = this.padding.left + (xStep * index);
1399 this.ctx.fillText(label, x, this.padding.top + height + 20);
1400 });
1401 }
1402 }
1403
1404 // Initialize activity chart
1405 function initActivityChart() {
1406 console.log('[MxChat Chart] initActivityChart called');
1407
1408 const canvas = document.getElementById('mxchat-activity-chart');
1409 console.log('[MxChat Chart] Canvas element:', canvas);
1410
1411 if (!canvas) {
1412 console.log('[MxChat Chart] Canvas not found, aborting');
1413 return;
1414 }
1415
1416 console.log('[MxChat Chart] mxchatChartData exists:', typeof mxchatChartData !== 'undefined');
1417 if (typeof mxchatChartData === 'undefined') {
1418 console.log('[MxChat Chart] mxchatChartData is undefined, aborting');
1419 return;
1420 }
1421
1422 console.log('[MxChat Chart] Raw mxchatChartData:', mxchatChartData);
1423
1424 // Check if chart already exists and destroy it
1425 if (canvas.chartInstance) {
1426 canvas.chartInstance.destroy();
1427 }
1428
1429 const ctx = canvas.getContext('2d');
1430 console.log('[MxChat Chart] Canvas context:', ctx);
1431 console.log('[MxChat Chart] Canvas dimensions:', canvas.getBoundingClientRect());
1432
1433 // Create gradient for chats line
1434 const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
1435 chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
1436 chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
1437
1438 // Create gradient for messages line
1439 const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
1440 messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
1441 messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
1442
1443 // Convert wp_localize_script objects to arrays (WordPress converts indexed arrays to objects)
1444 const labels = Object.values(mxchatChartData.labels);
1445 const chatsData = Object.values(mxchatChartData.chats).map(Number);
1446 const messagesData = Object.values(mxchatChartData.messages).map(Number);
1447
1448 console.log('[MxChat Chart] Processed labels:', labels);
1449 console.log('[MxChat Chart] Processed chatsData:', chatsData);
1450 console.log('[MxChat Chart] Processed messagesData:', messagesData);
1451
1452 // Create chart
1453 try {
1454 canvas.chartInstance = new SimpleChart(canvas, {
1455 labels: labels,
1456 datasets: [
1457 {
1458 label: 'Chats',
1459 data: chatsData,
1460 borderColor: '#667eea',
1461 backgroundColor: chatsGradient,
1462 fill: true
1463 },
1464 {
1465 label: 'Messages',
1466 data: messagesData,
1467 borderColor: '#764ba2',
1468 backgroundColor: messagesGradient,
1469 fill: true
1470 }
1471 ]
1472 });
1473 console.log('[MxChat Chart] Chart created successfully');
1474 } catch (error) {
1475 console.error('[MxChat Chart] Error creating chart:', error);
1476 }
1477 }
1478
1479 // Initialize chart on page load (dashboard is shown by default)
1480 setTimeout(function() {
1481 initActivityChart();
1482 }, 100);
1483
1484 // Reinitialize chart on window resize
1485 let resizeTimeout;
1486 $(window).on('resize', function() {
1487 clearTimeout(resizeTimeout);
1488 resizeTimeout = setTimeout(function() {
1489 initActivityChart();
1490 }, 250);
1491 });
1492
1493 // ==========================================================================
1494 // Leads Tab
1495 // ==========================================================================
1496
1497 const leadsState = {
1498 loaded: false,
1499 page: 1,
1500 perPage: 25,
1501 totalPages: 1,
1502 totalCount: 0,
1503 selected: new Set(),
1504 filters: {
1505 search: '',
1506 dateRange: 'all',
1507 status: 'all',
1508 pageUrl: '',
1509 pageTitle: ''
1510 },
1511 pendingDelete: [],
1512 leadsRows: [] // last-rendered rows for quick lookup
1513 };
1514
1515 function $leads() { return $('#leads'); }
1516
1517 // Called after a transcript delete from the All Chats side. Marks the Leads tab
1518 // data stale so the next tab visit re-fetches, and refreshes immediately if the
1519 // Leads tab happens to already be visible.
1520 function invalidateLeadsData() {
1521 leadsState.loaded = false;
1522 if ($('#leads').hasClass('active')) {
1523 loadLeads(1);
1524 }
1525 }
1526
1527 function escapeHtmlLeads(s) {
1528 if (s === null || typeof s === 'undefined') return '';
1529 return String(s)
1530 .replace(/&/g, '&amp;')
1531 .replace(/</g, '&lt;')
1532 .replace(/>/g, '&gt;')
1533 .replace(/"/g, '&quot;')
1534 .replace(/'/g, '&#039;');
1535 }
1536
1537 function leadsFiltersActive() {
1538 const f = leadsState.filters;
1539 return f.search !== '' || f.dateRange !== 'all' || f.status !== 'all' || f.pageUrl !== '';
1540 }
1541
1542 function updateClearFiltersButton() {
1543 if (leadsFiltersActive()) {
1544 $('#mxch-leads-clear-filters').show();
1545 } else {
1546 $('#mxch-leads-clear-filters').hide();
1547 }
1548 }
1549
1550 function setPageFilterChip(url, title) {
1551 leadsState.filters.pageUrl = url || '';
1552 leadsState.filters.pageTitle = title || url || '';
1553 const $chip = $('#mxch-leads-active-page-filter');
1554 if (url) {
1555 $chip.find('.mxch-leads-page-chip-label').text('Page: ' + (title || url));
1556 $chip.show();
1557 } else {
1558 $chip.hide();
1559 }
1560 updateClearFiltersButton();
1561 }
1562
1563 function loadLeads(page) {
1564 if (typeof page === 'number') leadsState.page = page;
1565
1566 const $tbody = $('#mxch-leads-tbody');
1567 $tbody.html('<tr><td colspan="6" class="mxch-leads-loading"><span class="spinner is-active"></span></td></tr>');
1568
1569 $.ajax({
1570 url: ajaxurl,
1571 type: 'POST',
1572 data: {
1573 action: 'mxchat_fetch_leads',
1574 page: leadsState.page,
1575 per_page: leadsState.perPage,
1576 search: leadsState.filters.search,
1577 date_range: leadsState.filters.dateRange,
1578 status: leadsState.filters.status,
1579 page_url: leadsState.filters.pageUrl
1580 },
1581 success: function(response) {
1582 leadsState.loaded = true;
1583 if (!response || !response.success) {
1584 $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1585 return;
1586 }
1587 leadsState.totalPages = response.total_pages || 1;
1588 leadsState.totalCount = response.total_count || 0;
1589 leadsState.leadsRows = response.leads || [];
1590
1591 renderLeadsStats(response.stats || {});
1592 renderLeadsTopPages(response.top_pages || []);
1593 renderLeadsTable(response.leads || []);
1594 renderLeadsCount(response.showing_start, response.showing_end, response.total_count);
1595 renderLeadsPagination(response.page, response.total_pages);
1596
1597 // Nav badge
1598 if (response.stats && typeof response.stats.total_leads === 'number') {
1599 const $badge = $('#mxch-leads-nav-badge');
1600 if (response.stats.total_leads > 0) {
1601 $badge.text(response.stats.total_leads).show();
1602 } else {
1603 $badge.hide();
1604 }
1605 }
1606 },
1607 error: function() {
1608 $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1609 }
1610 });
1611 }
1612
1613 function renderLeadsStats(stats) {
1614 $('#mxch-leads-stat-total').text(stats.total_leads || 0);
1615 $('#mxch-leads-stat-new').text(stats.new_this_week || 0);
1616 $('#mxch-leads-stat-avg').text(stats.avg_convos || 0);
1617 const pct = stats.orphan_pct || 0;
1618 $('#mxch-leads-stat-orphan').text(pct + '%');
1619 const orphanCount = stats.orphan_count || 0;
1620 $('#mxch-leads-stat-orphan-sub').text(orphanCount + (orphanCount === 1 ? ' lead captured but never chatted' : ' leads captured but never chatted'));
1621 }
1622
1623 function renderLeadsTopPages(pages) {
1624 const $wrap = $('#mxch-leads-toppages-list');
1625 if (!pages || pages.length === 0) {
1626 $wrap.html('<div class="mxch-leads-empty-mini">No page data yet.</div>');
1627 return;
1628 }
1629 let html = '';
1630 pages.forEach(function(p) {
1631 const isActive = leadsState.filters.pageUrl === p.url ? ' is-active' : '';
1632 html += `
1633 <button type="button" class="mxch-leads-toppage-row${isActive}" data-url="${escapeHtmlLeads(p.url)}" data-title="${escapeHtmlLeads(p.title)}">
1634 <span class="mxch-leads-toppage-title">${escapeHtmlLeads(p.title || p.url)}</span>
1635 <span class="mxch-leads-toppage-count">${p.lead_count}</span>
1636 </button>
1637 `;
1638 });
1639 $wrap.html(html);
1640 }
1641
1642 function renderLeadsTable(rows) {
1643 const $tbody = $('#mxch-leads-tbody');
1644 if (!rows || rows.length === 0) {
1645 $tbody.html(`
1646 <tr><td colspan="7" class="mxch-leads-empty">
1647 <div class="mxch-leads-empty-wrap">
1648 <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>
1649 <p>No leads match the current filters.</p>
1650 </div>
1651 </td></tr>
1652 `);
1653 return;
1654 }
1655
1656 let html = '';
1657 rows.forEach(function(r) {
1658 const emailKey = (r.email || '').toLowerCase();
1659 const isChecked = leadsState.selected.has(emailKey) ? ' checked' : '';
1660 // Status: 'active' (has conversations), 'chat_deleted' (admin removed the chat), 'orphan' (no chat ever).
1661 const status = r.status || (r.is_orphan ? 'orphan' : 'active');
1662 const isOrphan = (status === 'orphan');
1663 const isChatDeleted = (status === 'chat_deleted');
1664 const nameLine = r.name
1665 ? `<span class="mxch-leads-lead-name">${escapeHtmlLeads(r.name)}</span>`
1666 : '';
1667 const leadCell = `
1668 <div class="mxch-leads-lead-cell">
1669 <span class="mxch-leads-lead-email" title="${escapeHtmlLeads(r.email)}">${escapeHtmlLeads(r.email)}</span>
1670 ${nameLine}
1671 </div>`;
1672 let countCell;
1673 if (isOrphan) {
1674 countCell = `<span class="mxch-leads-pill mxch-leads-pill-orphan">Orphan</span>`;
1675 } else if (isChatDeleted) {
1676 countCell = `<span class="mxch-leads-pill mxch-leads-pill-deleted" title="Chat was deleted by an admin">Chat deleted</span>`;
1677 } else {
1678 countCell = `<span class="mxch-leads-pill">${r.conversation_count}</span>`;
1679 }
1680 const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : ''));
1681 const pageCell = r.top_page_url
1682 ? `<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>`
1683 : '<span class="mxch-leads-muted">—</span>';
1684 // Consent (b062c4): 'yes'/'no' are recorded decisions; '' means the
1685 // capture predates the checkbox — "Not recorded", never "No".
1686 let consentCell;
1687 if (r.consent === 'yes' || r.consent === 'no') {
1688 const consentTitle = (r.consent_at ? 'Recorded ' + r.consent_at : '') +
1689 (r.consent_label ? (r.consent_at ? '' : '') + '"' + r.consent_label + '"' : '');
1690 consentCell = `<span class="mxch-leads-pill${r.consent === 'yes' ? ' mxch-leads-pill-consent-yes' : ' mxch-leads-pill-consent-no'}" title="${escapeHtmlLeads(consentTitle)}">${r.consent === 'yes' ? 'Yes' : 'No'}</span>`;
1691 } else {
1692 consentCell = '<span class="mxch-leads-muted" title="Captured before the consent checkbox was enabled">Not recorded</span>';
1693 }
1694 // View Convo only for active leads (orphans and chat_deleted have no viewable session).
1695 const viewBtn = (status === 'active' && r.latest_session_id)
1696 ? `<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">
1697 <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>
1698 <span>View convo</span>
1699 </button>`
1700 : '';
1701 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">
1702 <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>
1703 </button>`;
1704
1705 const rowStateClass = isOrphan ? ' is-orphan' : (isChatDeleted ? ' is-chat-deleted' : '');
1706 html += `
1707 <tr class="mxch-leads-row${rowStateClass}" data-email="${escapeHtmlLeads(r.email)}">
1708 <td class="mxch-leads-col-check"><input type="checkbox" class="mxch-leads-rowcheck"${isChecked}></td>
1709 <td class="mxch-leads-col-lead">${leadCell}</td>
1710 <td class="mxch-leads-col-count">${countCell}</td>
1711 <td class="mxch-leads-col-last">${lastCell}</td>
1712 <td class="mxch-leads-col-page">${pageCell}</td>
1713 <td class="mxch-leads-col-consent">${consentCell}</td>
1714 <td class="mxch-leads-col-actions">${viewBtn}${deleteBtn}</td>
1715 </tr>
1716 `;
1717 });
1718
1719 $tbody.html(html);
1720 updateLeadsSelectionUI();
1721 }
1722
1723 function renderLeadsCount(start, end, total) {
1724 if (!total) {
1725 $('#mxch-leads-count').text('0 leads');
1726 } else {
1727 $('#mxch-leads-count').text(start + '-' + end + ' / ' + total + ' leads');
1728 }
1729 }
1730
1731 function renderLeadsPagination(currentPage, totalPages) {
1732 const $c = $('#mxch-leads-pagination');
1733 if (!totalPages || totalPages <= 1) { $c.html(''); return; }
1734 let html = '<div class="mxch-pagination-btns">';
1735 if (currentPage > 1) {
1736 html += `<button class="mxch-page-btn" data-page="${currentPage - 1}">&laquo;</button>`;
1737 }
1738 html += `<span class="mxch-page-info">${currentPage} / ${totalPages}</span>`;
1739 if (currentPage < totalPages) {
1740 html += `<button class="mxch-page-btn" data-page="${currentPage + 1}">&raquo;</button>`;
1741 }
1742 html += '</div>';
1743 $c.html(html);
1744 }
1745
1746 function updateLeadsSelectionUI() {
1747 const count = leadsState.selected.size;
1748 const $countEl = $('#mxch-leads-selected-count');
1749 const $del = $('#mxch-leads-delete-selected');
1750 if (count > 0) {
1751 $countEl.text(count + ' selected').addClass('has-selection');
1752 $del.prop('disabled', false);
1753 } else {
1754 $countEl.text('0').removeClass('has-selection');
1755 $del.prop('disabled', true);
1756 }
1757 // Selected-scope export menu items
1758 $('#mxch-leads-export-menu button[data-scope="selected"]').prop('disabled', count === 0);
1759
1760 // Select-all checkbox state
1761 const $checks = $('.mxch-leads-rowcheck');
1762 const checked = $checks.filter(':checked').length;
1763 const total = $checks.length;
1764 $('#mxch-leads-select-all').prop('checked', total > 0 && checked === total);
1765 $('#mxch-leads-select-all').prop('indeterminate', checked > 0 && checked < total);
1766 }
1767
1768 // Trigger leads load when switching to the tab (works alongside the main nav handler above).
1769 $('.mxch-nav-link[data-target="leads"], .mxch-mobile-nav-link[data-target="leads"]').on('click', function() {
1770 if (!leadsState.loaded) {
1771 loadLeads(1);
1772 }
1773 });
1774
1775 // Filter: search (debounced)
1776 let leadsSearchTimer;
1777 $('#mxch-leads-search').on('input', function() {
1778 clearTimeout(leadsSearchTimer);
1779 const val = $(this).val();
1780 leadsSearchTimer = setTimeout(function() {
1781 leadsState.filters.search = (val || '').trim();
1782 updateClearFiltersButton();
1783 loadLeads(1);
1784 }, 300);
1785 });
1786
1787 // Filter: date range
1788 $('#mxch-leads-date-range').on('change', function() {
1789 leadsState.filters.dateRange = $(this).val();
1790 updateClearFiltersButton();
1791 loadLeads(1);
1792 });
1793
1794 // Filter: status
1795 $('#mxch-leads-status').on('change', function() {
1796 leadsState.filters.status = $(this).val();
1797 updateClearFiltersButton();
1798 loadLeads(1);
1799 });
1800
1801 // Clear filters
1802 $('#mxch-leads-clear-filters').on('click', function() {
1803 leadsState.filters = { search: '', dateRange: 'all', status: 'all', pageUrl: '', pageTitle: '' };
1804 $('#mxch-leads-search').val('');
1805 $('#mxch-leads-date-range').val('all');
1806 $('#mxch-leads-status').val('all');
1807 setPageFilterChip('', '');
1808 loadLeads(1);
1809 });
1810
1811 // Remove page chip
1812 $leads().on('click', '.mxch-leads-page-chip-remove', function() {
1813 setPageFilterChip('', '');
1814 loadLeads(1);
1815 });
1816
1817 // Top Pages click -> set filter
1818 $leads().on('click', '.mxch-leads-toppage-row', function() {
1819 const url = $(this).data('url') || '';
1820 const title = $(this).data('title') || '';
1821 setPageFilterChip(url, title);
1822 loadLeads(1);
1823 });
1824
1825 // Pagination click
1826 $leads().on('click', '#mxch-leads-pagination .mxch-page-btn', function() {
1827 const p = parseInt($(this).data('page'), 10);
1828 if (p > 0) loadLeads(p);
1829 });
1830
1831 // Select-all
1832 $('#mxch-leads-select-all').on('change', function() {
1833 const on = $(this).is(':checked');
1834 $('.mxch-leads-rowcheck').prop('checked', on);
1835 $('.mxch-leads-row').each(function() {
1836 const email = ($(this).data('email') || '').toString().toLowerCase();
1837 if (on) {
1838 leadsState.selected.add(email);
1839 } else {
1840 leadsState.selected.delete(email);
1841 }
1842 });
1843 updateLeadsSelectionUI();
1844 });
1845
1846 // Row checkbox
1847 $leads().on('change', '.mxch-leads-rowcheck', function() {
1848 const email = ($(this).closest('.mxch-leads-row').data('email') || '').toString().toLowerCase();
1849 if ($(this).is(':checked')) {
1850 leadsState.selected.add(email);
1851 } else {
1852 leadsState.selected.delete(email);
1853 }
1854 updateLeadsSelectionUI();
1855 });
1856
1857 // View convo -> jump to All Chats tab and open the session
1858 $leads().on('click', '.mxch-leads-view', function() {
1859 const sid = $(this).attr('data-session-id');
1860 if (!sid) return;
1861 $('.mxch-nav-link[data-target="all-chats"]').trigger('click');
1862 // selectChat is defined earlier in this closure
1863 if (typeof selectChat === 'function') {
1864 setTimeout(function() { selectChat(sid); }, 30);
1865 }
1866 });
1867
1868 // Row delete -> confirm for one
1869 $leads().on('click', '.mxch-leads-delete-row', function() {
1870 const email = $(this).data('email');
1871 if (!email) return;
1872 openLeadsConfirm([String(email)]);
1873 });
1874
1875 // Bulk delete -> confirm for N
1876 $('#mxch-leads-delete-selected').on('click', function() {
1877 if (leadsState.selected.size === 0) return;
1878 openLeadsConfirm(Array.from(leadsState.selected));
1879 });
1880
1881 function openLeadsConfirm(emails) {
1882 leadsState.pendingDelete = emails;
1883 const count = emails.length;
1884 const msg = count === 1
1885 ? 'Delete lead "' + emails[0] + '" and all of their conversations?'
1886 : 'Delete ' + count + ' leads and all of their conversations?';
1887 $('#mxch-leads-confirm-body').text(msg);
1888 $('#mxch-leads-confirm').fadeIn(120);
1889 }
1890
1891 function closeLeadsConfirm() {
1892 $('#mxch-leads-confirm').fadeOut(120);
1893 leadsState.pendingDelete = [];
1894 }
1895
1896 $leads().on('click', '[data-mxch-leads-close]', closeLeadsConfirm);
1897
1898 $('#mxch-leads-confirm-go').on('click', function() {
1899 const emails = leadsState.pendingDelete.slice();
1900 if (!emails.length) { closeLeadsConfirm(); return; }
1901
1902 const $btn = $(this).prop('disabled', true).text('Deleting...');
1903
1904 $.ajax({
1905 url: ajaxurl,
1906 type: 'POST',
1907 data: {
1908 action: 'mxchat_delete_leads',
1909 security: $('#mxchat_leads_delete_nonce').val(),
1910 emails: emails
1911 },
1912 success: function(response) {
1913 $btn.prop('disabled', false).text('Delete permanently');
1914 closeLeadsConfirm();
1915 if (response && response.success) {
1916 emails.forEach(function(e) { leadsState.selected.delete(e.toLowerCase()); });
1917 loadLeads(leadsState.page);
1918 } else {
1919 alert((response && response.data && response.data.message) || 'Failed to delete leads.');
1920 }
1921 },
1922 error: function() {
1923 $btn.prop('disabled', false).text('Delete permanently');
1924 alert('Network error while deleting.');
1925 }
1926 });
1927 });
1928
1929 // Export dropdown
1930 $('#mxch-leads-export-btn').on('click', function(e) {
1931 e.stopPropagation();
1932 $('#mxch-leads-export-menu').toggleClass('is-open');
1933 });
1934
1935 $(document).on('click', function() {
1936 $('#mxch-leads-export-menu').removeClass('is-open');
1937 });
1938
1939 $('#mxch-leads-export-menu').on('click', function(e) { e.stopPropagation(); });
1940
1941 $('#mxch-leads-export-menu button').on('click', function() {
1942 if ($(this).prop('disabled')) return;
1943 const scope = $(this).data('scope') || 'all';
1944 const fields = $(this).data('fields') || 'email_and_name';
1945 submitLeadsExport(scope, fields);
1946 $('#mxch-leads-export-menu').removeClass('is-open');
1947 });
1948
1949 function submitLeadsExport(scope, fields) {
1950 const $form = $('<form>', { method: 'POST', action: ajaxurl, style: 'display:none;' });
1951 $form.append($('<input>', { type: 'hidden', name: 'action', value: 'mxchat_export_leads' }));
1952 $form.append($('<input>', { type: 'hidden', name: 'security', value: $('#mxchat_leads_export_nonce').val() }));
1953 $form.append($('<input>', { type: 'hidden', name: 'scope', value: scope }));
1954 $form.append($('<input>', { type: 'hidden', name: 'fields', value: fields }));
1955 if (scope === 'selected') {
1956 Array.from(leadsState.selected).forEach(function(e) {
1957 $form.append($('<input>', { type: 'hidden', name: 'emails[]', value: e }));
1958 });
1959 }
1960 $form.appendTo('body').submit().remove();
1961 }
1962
1963 // Preload leads metadata on page load (for the nav badge count only) without rendering.
1964 // We keep this light — the full fetch only runs when the tab is clicked.
1965 $.ajax({
1966 url: ajaxurl,
1967 type: 'POST',
1968 data: { action: 'mxchat_fetch_leads', page: 1, per_page: 1 },
1969 success: function(response) {
1970 if (response && response.success && response.stats) {
1971 const total = response.stats.total_leads || 0;
1972 const $badge = $('#mxch-leads-nav-badge');
1973 if (total > 0) $badge.text(total).show();
1974 }
1975 }
1976 });
1977 });
1978