PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 1.0.8
MxChat – AI Chatbot & Content Generation for WordPress v1.0.8
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/mxchat_transcripts.js +29 -1822 3.2.71.0.8 View file →
@@ -1,1843 +1,50 @@
1 -/**
2 - * MxChat Transcripts Page JavaScript - v3.0
3 - * Split-panel layout with chat list and conversation view
4 - */
5 1 jQuery(document).ready(function($) {
6 - // ==========================================================================
7 - // Sidebar Navigation
8 - // ==========================================================================
2 + $('#mxchat-select-all-transcripts').click(function() {
3 + var checkedStatus = this.checked;
4 + $('#mxchat-transcripts').find('input[type=checkbox]').each(function() {
5 + $(this).prop('checked', checkedStatus);
6 + });
7 + });
9 8
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, '');
9 + $.ajax({
10 + url: ajaxurl,
11 + type: 'POST',
12 + data: {
13 + action: 'mxchat_fetch_chat_history'
14 + },
15 + success: function(response) {
16 + $('#mxchat-transcripts').html(response);
33 17 }
34 18 });
35 19
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) {
20 + $('#mxchat-delete-form').submit(function(e) {
50 21 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) {
22 + var checkedSessionIds = $('input[name="delete_session_ids[]"]:checked').map(function() {
23 + return $(this).val();
24 + }).get();
316 25 $.ajax({
317 26 url: ajaxurl,
318 27 type: 'POST',
319 28 data: {
320 29 action: 'mxchat_delete_chat_history',
321 - delete_session_ids: sessionIds,
322 - also_delete_lead: alsoDeleteLead ? '1' : '0',
30 + delete_session_ids: checkedSessionIds,
323 31 security: $('#mxchat_delete_chat_nonce').val()
324 32 },
325 33 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);
34 + var jsonResponse = JSON.parse(response);
35 + if (jsonResponse.success) {
36 + alert("Success: " + jsonResponse.success);
37 + } else if (jsonResponse.error) {
38 + alert("Error: " + jsonResponse.error);
387 39 } else {
388 - $container.html('<div class="mxch-list-empty"><p>No chats found</p></div>');
389 - updateChatCount(0, 0, 0);
390 - $('#mxch-pagination').html('');
40 + //console.log("Unexpected response format.");
391 41 }
42 + location.reload();
392 43 },
393 - error: function() {
394 - $container.html('<div class="mxch-list-empty"><p>Error loading chats</p></div>');
44 + error: function(xhr, status, error) {
45 + //console.error("AJAX Error: " + status + " - " + error);
46 + //console.log(xhr.responseText);
395 47 }
396 48 });
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 - const ragLink = msg.has_rag ? `<a href="#" class="mxch-rag-link" data-message-id="${msg.id}">Sources</a>` : '';
612 - messagesHtml += `
613 - <div class="mxch-message mxch-message-bot" data-message-id="${msg.id}">
614 - <div class="mxch-message-header">
615 - <span class="mxch-bot-label">AI Assistant</span>
616 - ${ragLink}
617 - </div>
618 - <div class="mxch-message-row">
619 - <div class="mxch-message-bubble">
620 - ${msg.content}
621 - </div>
622 - </div>
623 - <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>
624 - </div>
625 - `;
626 - }
627 - });
628 -
629 - $('#mxch-messages-area').html(messagesHtml);
630 -
631 - // Scroll to bottom
632 - const $area = $('#mxch-messages-area');
633 - $area.scrollTop($area[0].scrollHeight);
634 -
635 - // Attach RAG link handlers
636 - $('.mxch-rag-link').on('click', function(e) {
637 - e.preventDefault();
638 - const messageId = $(this).data('message-id');
639 - if (messageId) {
640 - openRagContextModal(messageId);
641 - }
642 - });
643 - }
644 -
645 - // Toggle details drawer
646 - $('#mxch-toggle-details').on('click', function() {
647 - const $drawer = $('#mxch-details-drawer');
648 - const $btn = $(this);
649 -
650 - if ($drawer.is(':visible')) {
651 - $drawer.slideUp(200);
652 - $btn.removeClass('active');
653 - } else {
654 - $drawer.slideDown(200);
655 - $btn.addClass('active');
656 - }
657 - });
658 -
659 - // Delete current chat — opens the shared confirm modal.
660 - $('#mxch-delete-current').on('click', function() {
661 - if (!currentSessionId) return;
662 - openTranscriptConfirm([currentSessionId], 1);
663 - });
664 -
665 - // Delete session function
666 - function deleteSession(sessionId, alsoDeleteLead) {
667 - $.ajax({
668 - url: ajaxurl,
669 - type: 'POST',
670 - data: {
671 - action: 'mxchat_delete_chat_history',
672 - delete_session_ids: [sessionId],
673 - also_delete_lead: alsoDeleteLead ? '1' : '0',
674 - security: $('#mxchat_delete_chat_nonce').val()
675 - },
676 - success: function(response) {
677 - try {
678 - const jsonResponse = typeof response === 'object' ? response : JSON.parse(response);
679 -
680 - if (jsonResponse.success) {
681 - // Reset conversation panel
682 - currentSessionId = null;
683 - $('#mxch-conversation-content').hide();
684 - $('#mxch-conversation-empty').show();
685 - $('#mxch-details-drawer').hide();
686 -
687 - // Reload list
688 - loadChatList(currentPage, $('#mxch-search-transcripts').val());
689 - } else if (jsonResponse.error) {
690 - alert('Error: ' + jsonResponse.error);
691 - }
692 - } catch (e) {
693 - alert('An error occurred while processing the response.');
694 - }
695 - },
696 - error: function() {
697 - alert('An error occurred while deleting the conversation.');
698 - }
699 - });
700 - }
701 -
702 - // ==========================================================================
703 - // Export Functionality
704 - // ==========================================================================
705 -
706 - $('#mxch-export-btn, #mxch-export-current').on('click', function() {
707 - const $button = $(this);
708 - $button.prop('disabled', true).addClass('loading');
709 -
710 - const $form = $('<form>', {
711 - method: 'post',
712 - action: ajaxurl
713 - });
714 -
715 - $form.append($('<input>', {
716 - type: 'hidden',
717 - name: 'action',
718 - value: 'mxchat_export_transcripts'
719 - }));
720 -
721 - $form.append($('<input>', {
722 - type: 'hidden',
723 - name: 'security',
724 - value: mxchatAdmin.export_nonce
725 - }));
726 -
727 - $form.appendTo('body').submit();
728 -
729 - setTimeout(function() {
730 - $button.prop('disabled', false).removeClass('loading');
731 - }, 2000);
732 - });
733 -
734 - // ==========================================================================
735 - // Translation Functionality
736 - // ==========================================================================
737 -
738 - // Store original messages for reverting
739 - let originalMessages = null;
740 - let isTranslated = false;
741 - let currentTranslationLang = null;
742 -
743 - // Load saved language preference from localStorage
744 - const savedLang = localStorage.getItem('mxch_translate_lang');
745 - if (savedLang) {
746 - $('#mxch-translate-lang').val(savedLang);
747 - }
748 -
749 - // Save language preference when changed
750 - $('#mxch-translate-lang').on('change', function() {
751 - localStorage.setItem('mxch_translate_lang', $(this).val());
752 - });
753 -
754 - // Apply translations to messages
755 - function applyTranslations(translations) {
756 - // Store original messages if not already stored
757 - if (!originalMessages) {
758 - originalMessages = [];
759 - $('#mxch-messages-area .mxch-message-bubble').each(function() {
760 - originalMessages.push($(this).html());
761 - });
762 - }
763 -
764 - // Apply translations
765 - translations.forEach(function(item) {
766 - const $bubble = $('#mxch-messages-area .mxch-message-bubble').eq(item.index);
767 - if ($bubble.length) {
768 - $bubble.html(item.translated);
769 - $bubble.addClass('translated');
770 - }
771 - });
772 -
773 - isTranslated = true;
774 - $('#mxch-show-original-btn').show();
775 - }
776 -
777 - // Load saved translation for current session
778 - function loadSavedTranslation(sessionId) {
779 - $.ajax({
780 - url: ajaxurl,
781 - type: 'POST',
782 - data: {
783 - action: 'mxchat_get_transcript_translation',
784 - session_id: sessionId
785 - },
786 - success: function(response) {
787 - if (response.success && response.has_translation) {
788 - currentTranslationLang = response.language;
789 - applyTranslations(response.translations);
790 - // Update language selector to show saved language
791 - $('#mxch-translate-lang').val(response.language);
792 - }
793 - }
794 - });
795 - }
796 -
797 - // Translate button click handler
798 - $('#mxch-translate-btn').on('click', function() {
799 - if (!currentSessionId) return;
800 -
801 - const $btn = $(this);
802 - const targetLang = $('#mxch-translate-lang').val();
803 -
804 - // Disable button and show loading state
805 - $btn.prop('disabled', true);
806 - $btn.find('.mxch-translate-text').text('Translating...');
807 - $btn.find('svg').addClass('mxch-translate-spinner');
808 -
809 - // Store original messages before translation
810 - if (!originalMessages) {
811 - originalMessages = [];
812 - $('#mxch-messages-area .mxch-message-bubble').each(function() {
813 - originalMessages.push($(this).html());
814 - });
815 - }
816 -
817 - // If already translated, restore originals first before re-translating
818 - if (isTranslated) {
819 - $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
820 - if (originalMessages[index]) {
821 - $(this).html(originalMessages[index]);
822 - $(this).removeClass('translated');
823 - }
824 - });
825 - }
826 -
827 - // Collect all message content (from originals)
828 - const messages = [];
829 - originalMessages.forEach(function(html, index) {
830 - // Create temp element to get text content
831 - const $temp = $('<div>').html(html);
832 - messages.push({
833 - index: index,
834 - content: $temp.text().trim()
835 - });
836 - });
837 -
838 - // Send translation request
839 - $.ajax({
840 - url: ajaxurl,
841 - type: 'POST',
842 - data: {
843 - action: 'mxchat_translate_messages',
844 - session_id: currentSessionId,
845 - target_lang: targetLang,
846 - messages: JSON.stringify(messages),
847 - security: mxchatAdmin.translate_nonce || ''
848 - },
849 - success: function(response) {
850 - if (response.success && response.translations) {
851 - currentTranslationLang = response.language;
852 - applyTranslations(response.translations);
853 - $btn.find('.mxch-translate-text').text('Translate');
854 - } else {
855 - alert(response.error || 'Translation failed. Please try again.');
856 - $btn.find('.mxch-translate-text').text('Translate');
857 - }
858 - },
859 - error: function() {
860 - alert('Translation request failed. Please try again.');
861 - $btn.find('.mxch-translate-text').text('Translate');
862 - },
863 - complete: function() {
864 - $btn.prop('disabled', false);
865 - $btn.find('svg').removeClass('mxch-translate-spinner');
866 - }
867 - });
868 - });
869 -
870 - // Show original button click handler
871 - $('#mxch-show-original-btn').on('click', function() {
872 - if (!originalMessages) return;
873 -
874 - // Restore original messages
875 - $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
876 - if (originalMessages[index]) {
877 - $(this).html(originalMessages[index]);
878 - $(this).removeClass('translated');
879 - }
880 - });
881 -
882 - isTranslated = false;
883 - $(this).hide();
884 - });
885 -
886 - // Reset translation state (called when selecting new chat)
887 - function resetTranslationState() {
888 - originalMessages = null;
889 - isTranslated = false;
890 - currentTranslationLang = null;
891 - $('#mxch-show-original-btn').hide();
892 - }
893 -
894 - // Make functions available to selectChat
895 - window.resetTranslationState = resetTranslationState;
896 - window.loadSavedTranslation = loadSavedTranslation;
897 -
898 - // ==========================================================================
899 - // RAG Context Modal (Sources & Actions Tabs)
900 - // ==========================================================================
901 -
902 - function openRagContextModal(messageId) {
903 - const $modal = $('#mxch-rag-modal');
904 - const $loading = $modal.find('.mxch-rag-loading');
905 - const $sourcesContent = $modal.find('.mxch-rag-content');
906 - const $actionsContent = $modal.find('.mxch-actions-content');
907 -
908 - // Reset to Sources tab
909 - $modal.find('.mxch-context-tab').removeClass('active');
910 - $modal.find('.mxch-context-tab[data-tab="sources"]').addClass('active');
911 - $('#mxch-tab-sources').show();
912 - $('#mxch-tab-actions').hide();
913 -
914 - // Reset badge counts
915 - $('#mxch-sources-count, #mxch-actions-count').hide().text('0');
916 -
917 - $modal.fadeIn(200);
918 - $loading.show();
919 - $sourcesContent.html('');
920 - $actionsContent.html('');
921 -
922 - $.ajax({
923 - url: ajaxurl,
924 - type: 'POST',
925 - data: {
926 - action: 'mxchat_get_rag_context',
927 - message_id: messageId
928 - },
929 - success: function(response) {
930 - $loading.hide();
931 -
932 - if (response.success && response.data) {
933 - // Render sources tab
934 - renderRagContext(response.data, $sourcesContent);
935 -
936 - // Render actions tab
937 - renderActionsContext(response.data, $actionsContent);
938 -
939 - // Update badge counts
940 - const sourcesCount = response.data.top_matches ? response.data.top_matches.length : 0;
941 - const actionsCount = response.data.action_analysis ? response.data.action_analysis.length : 0;
942 -
943 - if (sourcesCount > 0) {
944 - $('#mxch-sources-count').text(sourcesCount).show();
945 - }
946 - if (actionsCount > 0) {
947 - $('#mxch-actions-count').text(actionsCount).show();
948 - }
949 - } else {
950 - $sourcesContent.html('<div class="mxch-rag-error">Unable to load document context.</div>');
951 - $actionsContent.html('<div class="mxch-rag-error">No action data available.</div>');
952 - }
953 - },
954 - error: function() {
955 - $loading.hide();
956 - $sourcesContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
957 - $actionsContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
958 - }
959 - });
960 - }
961 -
962 - // Tab switching
963 - $(document).on('click', '.mxch-context-tab', function() {
964 - const $tab = $(this);
965 - const tabName = $tab.data('tab');
966 -
967 - // Update active tab
968 - $('.mxch-context-tab').removeClass('active');
969 - $tab.addClass('active');
970 -
971 - // Show/hide content
972 - $('.mxch-tab-content').hide();
973 - $('#mxch-tab-' + tabName).show();
974 - });
975 -
976 - function renderRagContext(data, $container) {
977 - let html = '';
978 -
979 - // Check if we have any source data
980 - if (!data.top_matches || data.top_matches.length === 0) {
981 - html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>';
982 - $container.html(html);
983 - return;
984 - }
985 -
986 - html += '<div class="mxch-rag-summary">';
987 - 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>';
988 - 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>';
989 - 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>';
990 - html += '</div>';
991 -
992 - const groupedByUrl = {};
993 -
994 - data.top_matches.forEach(function(match) {
995 - const url = match.source_display || 'Unknown';
996 - if (!groupedByUrl[url]) {
997 - groupedByUrl[url] = {
998 - url: url,
999 - isUrl: url.startsWith('http'),
1000 - bestScore: 0,
1001 - usedForContext: false,
1002 - matchedChunks: []
1003 - };
1004 - }
1005 -
1006 - if (match.similarity_percentage > groupedByUrl[url].bestScore) {
1007 - groupedByUrl[url].bestScore = match.similarity_percentage;
1008 - }
1009 -
1010 - if (match.used_for_context) {
1011 - groupedByUrl[url].usedForContext = true;
1012 - }
1013 -
1014 - groupedByUrl[url].matchedChunks.push({
1015 - chunkIndex: match.chunk_index,
1016 - score: match.similarity_percentage,
1017 - usedForContext: match.used_for_context
1018 - });
1019 - });
1020 -
1021 - const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore);
1022 - const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length;
1023 - const chunksInfo = data.total_chunks_used > 0 ? data.total_chunks_used + ' chunks sent to AI' : '';
1024 -
1025 - html += '<div class="mxch-rag-matches">';
1026 - html += '<h3>Retrieved Documents</h3>';
1027 - 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>';
1028 -
1029 - urlGroups.forEach(function(group) {
1030 - const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
1031 - const statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
1032 - const statusLabel = group.usedForContext ? 'Used' : 'Not Used';
1033 -
1034 - html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1035 - html += '<div class="mxch-rag-match-header">';
1036 - html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
1037 -
1038 - if (group.matchedChunks.length > 1) {
1039 - html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
1040 - }
1041 -
1042 - html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
1043 - html += '</div>';
1044 -
1045 - html += '<div class="mxch-rag-match-source">';
1046 - if (group.isUrl) {
1047 - html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>';
1048 - } else {
1049 - html += escapeHtml(group.url);
1050 - }
1051 - html += '</div>';
1052 - html += '</div>';
1053 - });
1054 -
1055 - html += '</div>';
1056 - $container.html(html);
1057 - }
1058 -
1059 - function renderActionsContext(data, $container) {
1060 - let html = '';
1061 -
1062 - // Check if we have action analysis data
1063 - if (!data.action_analysis || data.action_analysis.length === 0) {
1064 - 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>';
1065 - $container.html(html);
1066 - return;
1067 - }
1068 -
1069 - const actions = data.action_analysis;
1070 - const triggeredAction = actions.find(a => a.triggered);
1071 - const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
1072 -
1073 - // Summary section
1074 - html += '<div class="mxch-rag-summary">';
1075 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Actions Evaluated:</span> <span class="mxch-rag-value">' + actions.length + '</span></div>';
1076 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Above Threshold:</span> <span class="mxch-rag-value">' + actionsAboveThreshold + '</span></div>';
1077 - if (triggeredAction) {
1078 - 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>';
1079 - }
1080 - html += '</div>';
1081 -
1082 - // Actions list
1083 - html += '<div class="mxch-rag-matches">';
1084 - html += '<h3>Action Scores</h3>';
1085 - html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">Showing all evaluated actions sorted by similarity score</p>';
1086 -
1087 - actions.forEach(function(action) {
1088 - let cardClass = 'mxch-rag-match-below';
1089 - let statusIcon = '&#10007;';
1090 - let statusLabel = 'Below Threshold';
1091 -
1092 - if (action.triggered) {
1093 - cardClass = 'mxch-action-triggered';
1094 - statusIcon = '&#9889;';
1095 - statusLabel = 'Triggered';
1096 - } else if (action.above_threshold) {
1097 - cardClass = 'mxch-rag-match-used';
1098 - statusIcon = '&#10003;';
1099 - statusLabel = 'Above Threshold';
1100 - }
1101 -
1102 - html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1103 - html += '<div class="mxch-rag-match-header">';
1104 - html += '<span class="mxch-rag-match-score">' + action.similarity_percentage + '%</span>';
1105 - html += '<span class="mxch-action-threshold-badge">Threshold: ' + action.threshold_percentage + '%</span>';
1106 - html += '<span class="mxch-rag-match-status ' + (action.triggered ? 'status-triggered' : (action.above_threshold ? 'status-used' : 'status-below')) + '">' + statusIcon + ' ' + statusLabel + '</span>';
1107 - html += '</div>';
1108 -
1109 - html += '<div class="mxch-action-details">';
1110 - html += '<div class="mxch-action-label">' + escapeHtml(action.intent_label) + '</div>';
1111 - html += '<div class="mxch-action-callback"><span class="mxch-action-callback-label">Callback:</span> ' + escapeHtml(action.callback_function) + '</div>';
1112 - html += '</div>';
1113 -
1114 - // Score bar visualization
1115 - const scoreBarWidth = Math.min(action.similarity_percentage, 100);
1116 - const thresholdPos = Math.min(action.threshold_percentage, 100);
1117 - html += '<div class="mxch-action-score-bar">';
1118 - html += '<div class="mxch-action-score-fill" style="width: ' + scoreBarWidth + '%;"></div>';
1119 - html += '<div class="mxch-action-threshold-marker" style="left: ' + thresholdPos + '%;"></div>';
1120 - html += '</div>';
1121 -
1122 - html += '</div>';
1123 - });
1124 -
1125 - html += '</div>';
1126 - $container.html(html);
1127 - }
1128 -
1129 - function escapeHtml(text) {
1130 - if (!text) return '';
1131 - const div = document.createElement('div');
1132 - div.textContent = text;
1133 - return div.innerHTML;
1134 - }
1135 -
1136 - // Close RAG modal
1137 - $('.mxch-modal-close').on('click', function() {
1138 - $(this).closest('.mxch-modal-overlay').fadeOut(200);
1139 - });
1140 -
1141 - $('.mxch-modal-overlay').on('click', function(e) {
1142 - if ($(e.target).is('.mxch-modal-overlay')) {
1143 - $(this).fadeOut(200);
1144 - }
1145 - });
1146 -
1147 - $(document).on('keydown', function(e) {
1148 - if (e.key === 'Escape') {
1149 - $('.mxch-modal-overlay').fadeOut(200);
1150 - }
1151 - });
1152 -
1153 - // ==========================================================================
1154 - // Activity Chart
1155 - // ==========================================================================
1156 -
1157 - // Simple chart implementation (no external dependencies)
1158 - class SimpleChart {
1159 - constructor(canvas, config) {
1160 - this.canvas = canvas;
1161 - this.ctx = canvas.getContext('2d');
1162 - this.config = config;
1163 - this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
1164 - this.render();
1165 - }
1166 -
1167 - destroy() {
1168 - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1169 - }
1170 -
1171 - render() {
1172 - const dpr = window.devicePixelRatio || 1;
1173 - const rect = this.canvas.getBoundingClientRect();
1174 -
1175 - this.canvas.width = rect.width * dpr;
1176 - this.canvas.height = rect.height * dpr;
1177 - this.ctx.scale(dpr, dpr);
1178 -
1179 - this.canvas.style.width = rect.width + 'px';
1180 - this.canvas.style.height = rect.height + 'px';
1181 -
1182 - const width = rect.width - this.padding.left - this.padding.right;
1183 - const height = rect.height - this.padding.top - this.padding.bottom;
1184 -
1185 - // Find max value
1186 - let maxValue = 0;
1187 - this.config.datasets.forEach(dataset => {
1188 - const max = Math.max(...dataset.data);
1189 - if (max > maxValue) maxValue = max;
1190 - });
1191 -
1192 - // Add some padding to max value
1193 - maxValue = Math.ceil(maxValue * 1.1);
1194 - if (maxValue === 0) maxValue = 10;
1195 -
1196 - // Draw grid lines
1197 - this.ctx.strokeStyle = '#e5e7eb';
1198 - this.ctx.lineWidth = 1;
1199 - const gridLines = 5;
1200 -
1201 - for (let i = 0; i <= gridLines; i++) {
1202 - const y = this.padding.top + (height / gridLines) * i;
1203 - this.ctx.beginPath();
1204 - this.ctx.moveTo(this.padding.left, y);
1205 - this.ctx.lineTo(this.padding.left + width, y);
1206 - this.ctx.stroke();
1207 -
1208 - // Draw y-axis labels
1209 - const value = maxValue - (maxValue / gridLines) * i;
1210 - this.ctx.fillStyle = '#6b7280';
1211 - this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1212 - this.ctx.textAlign = 'right';
1213 - this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
1214 - }
1215 -
1216 - // Draw datasets
1217 - this.config.datasets.forEach(dataset => {
1218 - const points = [];
1219 - const xStep = width / (this.config.labels.length - 1 || 1);
1220 -
1221 - dataset.data.forEach((value, index) => {
1222 - const x = this.padding.left + (xStep * index);
1223 - const y = this.padding.top + height - (value / maxValue * height);
1224 - points.push({ x, y, value });
1225 - });
1226 -
1227 - // Draw filled area
1228 - if (dataset.fill && dataset.backgroundColor) {
1229 - this.ctx.fillStyle = dataset.backgroundColor;
1230 - this.ctx.beginPath();
1231 - this.ctx.moveTo(points[0].x, this.padding.top + height);
1232 - points.forEach(point => {
1233 - this.ctx.lineTo(point.x, point.y);
1234 - });
1235 - this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
1236 - this.ctx.closePath();
1237 - this.ctx.fill();
1238 - }
1239 -
1240 - // Draw line
1241 - this.ctx.strokeStyle = dataset.borderColor;
1242 - this.ctx.lineWidth = 3;
1243 - this.ctx.lineCap = 'round';
1244 - this.ctx.lineJoin = 'round';
1245 -
1246 - this.ctx.beginPath();
1247 - points.forEach((point, index) => {
1248 - if (index === 0) {
1249 - this.ctx.moveTo(point.x, point.y);
1250 - } else {
1251 - this.ctx.lineTo(point.x, point.y);
1252 - }
1253 - });
1254 - this.ctx.stroke();
1255 -
1256 - // Draw points
1257 - points.forEach(point => {
1258 - this.ctx.fillStyle = '#ffffff';
1259 - this.ctx.beginPath();
1260 - this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
1261 - this.ctx.fill();
1262 - this.ctx.strokeStyle = dataset.borderColor;
1263 - this.ctx.lineWidth = 2;
1264 - this.ctx.stroke();
1265 - });
1266 - });
1267 -
1268 - // Draw x-axis labels
1269 - const xStep = width / (this.config.labels.length - 1 || 1);
1270 - this.ctx.fillStyle = '#6b7280';
1271 - this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1272 - this.ctx.textAlign = 'center';
1273 -
1274 - this.config.labels.forEach((label, index) => {
1275 - const x = this.padding.left + (xStep * index);
1276 - this.ctx.fillText(label, x, this.padding.top + height + 20);
1277 - });
1278 - }
1279 - }
1280 -
1281 - // Initialize activity chart
1282 - function initActivityChart() {
1283 - console.log('[MxChat Chart] initActivityChart called');
1284 -
1285 - const canvas = document.getElementById('mxchat-activity-chart');
1286 - console.log('[MxChat Chart] Canvas element:', canvas);
1287 -
1288 - if (!canvas) {
1289 - console.log('[MxChat Chart] Canvas not found, aborting');
1290 - return;
1291 - }
1292 -
1293 - console.log('[MxChat Chart] mxchatChartData exists:', typeof mxchatChartData !== 'undefined');
1294 - if (typeof mxchatChartData === 'undefined') {
1295 - console.log('[MxChat Chart] mxchatChartData is undefined, aborting');
1296 - return;
1297 - }
1298 -
1299 - console.log('[MxChat Chart] Raw mxchatChartData:', mxchatChartData);
1300 -
1301 - // Check if chart already exists and destroy it
1302 - if (canvas.chartInstance) {
1303 - canvas.chartInstance.destroy();
1304 - }
1305 -
1306 - const ctx = canvas.getContext('2d');
1307 - console.log('[MxChat Chart] Canvas context:', ctx);
1308 - console.log('[MxChat Chart] Canvas dimensions:', canvas.getBoundingClientRect());
1309 -
1310 - // Create gradient for chats line
1311 - const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
1312 - chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
1313 - chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
1314 -
1315 - // Create gradient for messages line
1316 - const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
1317 - messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
1318 - messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
1319 -
1320 - // Convert wp_localize_script objects to arrays (WordPress converts indexed arrays to objects)
1321 - const labels = Object.values(mxchatChartData.labels);
1322 - const chatsData = Object.values(mxchatChartData.chats).map(Number);
1323 - const messagesData = Object.values(mxchatChartData.messages).map(Number);
1324 -
1325 - console.log('[MxChat Chart] Processed labels:', labels);
1326 - console.log('[MxChat Chart] Processed chatsData:', chatsData);
1327 - console.log('[MxChat Chart] Processed messagesData:', messagesData);
1328 -
1329 - // Create chart
1330 - try {
1331 - canvas.chartInstance = new SimpleChart(canvas, {
1332 - labels: labels,
1333 - datasets: [
1334 - {
1335 - label: 'Chats',
1336 - data: chatsData,
1337 - borderColor: '#667eea',
1338 - backgroundColor: chatsGradient,
1339 - fill: true
1340 - },
1341 - {
1342 - label: 'Messages',
1343 - data: messagesData,
1344 - borderColor: '#764ba2',
1345 - backgroundColor: messagesGradient,
1346 - fill: true
1347 - }
1348 - ]
1349 - });
1350 - console.log('[MxChat Chart] Chart created successfully');
1351 - } catch (error) {
1352 - console.error('[MxChat Chart] Error creating chart:', error);
1353 - }
1354 - }
1355 -
1356 - // Initialize chart on page load (dashboard is shown by default)
1357 - setTimeout(function() {
1358 - initActivityChart();
1359 - }, 100);
1360 -
1361 - // Reinitialize chart on window resize
1362 - let resizeTimeout;
1363 - $(window).on('resize', function() {
1364 - clearTimeout(resizeTimeout);
1365 - resizeTimeout = setTimeout(function() {
1366 - initActivityChart();
1367 - }, 250);
1368 - });
1369 -
1370 - // ==========================================================================
1371 - // Leads Tab
1372 - // ==========================================================================
1373 -
1374 - const leadsState = {
1375 - loaded: false,
1376 - page: 1,
1377 - perPage: 25,
1378 - totalPages: 1,
1379 - totalCount: 0,
1380 - selected: new Set(),
1381 - filters: {
1382 - search: '',
1383 - dateRange: 'all',
1384 - status: 'all',
1385 - pageUrl: '',
1386 - pageTitle: ''
1387 - },
1388 - pendingDelete: [],
1389 - leadsRows: [] // last-rendered rows for quick lookup
1390 - };
1391 -
1392 - function $leads() { return $('#leads'); }
1393 -
1394 - // Called after a transcript delete from the All Chats side. Marks the Leads tab
1395 - // data stale so the next tab visit re-fetches, and refreshes immediately if the
1396 - // Leads tab happens to already be visible.
1397 - function invalidateLeadsData() {
1398 - leadsState.loaded = false;
1399 - if ($('#leads').hasClass('active')) {
1400 - loadLeads(1);
1401 - }
1402 - }
1403 -
1404 - function escapeHtmlLeads(s) {
1405 - if (s === null || typeof s === 'undefined') return '';
1406 - return String(s)
1407 - .replace(/&/g, '&amp;')
1408 - .replace(/</g, '&lt;')
1409 - .replace(/>/g, '&gt;')
1410 - .replace(/"/g, '&quot;')
1411 - .replace(/'/g, '&#039;');
1412 - }
1413 -
1414 - function leadsFiltersActive() {
1415 - const f = leadsState.filters;
1416 - return f.search !== '' || f.dateRange !== 'all' || f.status !== 'all' || f.pageUrl !== '';
1417 - }
1418 -
1419 - function updateClearFiltersButton() {
1420 - if (leadsFiltersActive()) {
1421 - $('#mxch-leads-clear-filters').show();
1422 - } else {
1423 - $('#mxch-leads-clear-filters').hide();
1424 - }
1425 - }
1426 -
1427 - function setPageFilterChip(url, title) {
1428 - leadsState.filters.pageUrl = url || '';
1429 - leadsState.filters.pageTitle = title || url || '';
1430 - const $chip = $('#mxch-leads-active-page-filter');
1431 - if (url) {
1432 - $chip.find('.mxch-leads-page-chip-label').text('Page: ' + (title || url));
1433 - $chip.show();
1434 - } else {
1435 - $chip.hide();
1436 - }
1437 - updateClearFiltersButton();
1438 - }
1439 -
1440 - function loadLeads(page) {
1441 - if (typeof page === 'number') leadsState.page = page;
1442 -
1443 - const $tbody = $('#mxch-leads-tbody');
1444 - $tbody.html('<tr><td colspan="6" class="mxch-leads-loading"><span class="spinner is-active"></span></td></tr>');
1445 -
1446 - $.ajax({
1447 - url: ajaxurl,
1448 - type: 'POST',
1449 - data: {
1450 - action: 'mxchat_fetch_leads',
1451 - page: leadsState.page,
1452 - per_page: leadsState.perPage,
1453 - search: leadsState.filters.search,
1454 - date_range: leadsState.filters.dateRange,
1455 - status: leadsState.filters.status,
1456 - page_url: leadsState.filters.pageUrl
1457 - },
1458 - success: function(response) {
1459 - leadsState.loaded = true;
1460 - if (!response || !response.success) {
1461 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1462 - return;
1463 - }
1464 - leadsState.totalPages = response.total_pages || 1;
1465 - leadsState.totalCount = response.total_count || 0;
1466 - leadsState.leadsRows = response.leads || [];
1467 -
1468 - renderLeadsStats(response.stats || {});
1469 - renderLeadsTopPages(response.top_pages || []);
1470 - renderLeadsTable(response.leads || []);
1471 - renderLeadsCount(response.showing_start, response.showing_end, response.total_count);
1472 - renderLeadsPagination(response.page, response.total_pages);
1473 -
1474 - // Nav badge
1475 - if (response.stats && typeof response.stats.total_leads === 'number') {
1476 - const $badge = $('#mxch-leads-nav-badge');
1477 - if (response.stats.total_leads > 0) {
1478 - $badge.text(response.stats.total_leads).show();
1479 - } else {
1480 - $badge.hide();
1481 - }
1482 - }
1483 - },
1484 - error: function() {
1485 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1486 - }
1487 - });
1488 - }
1489 -
1490 - function renderLeadsStats(stats) {
1491 - $('#mxch-leads-stat-total').text(stats.total_leads || 0);
1492 - $('#mxch-leads-stat-new').text(stats.new_this_week || 0);
1493 - $('#mxch-leads-stat-avg').text(stats.avg_convos || 0);
1494 - const pct = stats.orphan_pct || 0;
1495 - $('#mxch-leads-stat-orphan').text(pct + '%');
1496 - const orphanCount = stats.orphan_count || 0;
1497 - $('#mxch-leads-stat-orphan-sub').text(orphanCount + (orphanCount === 1 ? ' lead captured but never chatted' : ' leads captured but never chatted'));
1498 - }
1499 -
1500 - function renderLeadsTopPages(pages) {
1501 - const $wrap = $('#mxch-leads-toppages-list');
1502 - if (!pages || pages.length === 0) {
1503 - $wrap.html('<div class="mxch-leads-empty-mini">No page data yet.</div>');
1504 - return;
1505 - }
1506 - let html = '';
1507 - pages.forEach(function(p) {
1508 - const isActive = leadsState.filters.pageUrl === p.url ? ' is-active' : '';
1509 - html += `
1510 - <button type="button" class="mxch-leads-toppage-row${isActive}" data-url="${escapeHtmlLeads(p.url)}" data-title="${escapeHtmlLeads(p.title)}">
1511 - <span class="mxch-leads-toppage-title">${escapeHtmlLeads(p.title || p.url)}</span>
1512 - <span class="mxch-leads-toppage-count">${p.lead_count}</span>
1513 - </button>
1514 - `;
1515 - });
1516 - $wrap.html(html);
1517 - }
1518 -
1519 - function renderLeadsTable(rows) {
1520 - const $tbody = $('#mxch-leads-tbody');
1521 - if (!rows || rows.length === 0) {
1522 - $tbody.html(`
1523 - <tr><td colspan="6" class="mxch-leads-empty">
1524 - <div class="mxch-leads-empty-wrap">
1525 - <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>
1526 - <p>No leads match the current filters.</p>
1527 - </div>
1528 - </td></tr>
1529 - `);
1530 - return;
1531 - }
1532 -
1533 - let html = '';
1534 - rows.forEach(function(r) {
1535 - const emailKey = (r.email || '').toLowerCase();
1536 - const isChecked = leadsState.selected.has(emailKey) ? ' checked' : '';
1537 - // Status: 'active' (has conversations), 'chat_deleted' (admin removed the chat), 'orphan' (no chat ever).
1538 - const status = r.status || (r.is_orphan ? 'orphan' : 'active');
1539 - const isOrphan = (status === 'orphan');
1540 - const isChatDeleted = (status === 'chat_deleted');
1541 - const nameLine = r.name
1542 - ? `<span class="mxch-leads-lead-name">${escapeHtmlLeads(r.name)}</span>`
1543 - : '';
1544 - const leadCell = `
1545 - <div class="mxch-leads-lead-cell">
1546 - <span class="mxch-leads-lead-email" title="${escapeHtmlLeads(r.email)}">${escapeHtmlLeads(r.email)}</span>
1547 - ${nameLine}
1548 - </div>`;
1549 - let countCell;
1550 - if (isOrphan) {
1551 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-orphan">Orphan</span>`;
1552 - } else if (isChatDeleted) {
1553 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-deleted" title="Chat was deleted by an admin">Chat deleted</span>`;
1554 - } else {
1555 - countCell = `<span class="mxch-leads-pill">${r.conversation_count}</span>`;
1556 - }
1557 - const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : ''));
1558 - const pageCell = r.top_page_url
1559 - ? `<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>`
1560 - : '<span class="mxch-leads-muted">—</span>';
1561 - // View Convo only for active leads (orphans and chat_deleted have no viewable session).
1562 - const viewBtn = (status === 'active' && r.latest_session_id)
1563 - ? `<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">
1564 - <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>
1565 - <span>View convo</span>
1566 - </button>`
1567 - : '';
1568 - 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">
1569 - <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>
1570 - </button>`;
1571 -
1572 - const rowStateClass = isOrphan ? ' is-orphan' : (isChatDeleted ? ' is-chat-deleted' : '');
1573 - html += `
1574 - <tr class="mxch-leads-row${rowStateClass}" data-email="${escapeHtmlLeads(r.email)}">
1575 - <td class="mxch-leads-col-check"><input type="checkbox" class="mxch-leads-rowcheck"${isChecked}></td>
1576 - <td class="mxch-leads-col-lead">${leadCell}</td>
1577 - <td class="mxch-leads-col-count">${countCell}</td>
1578 - <td class="mxch-leads-col-last">${lastCell}</td>
1579 - <td class="mxch-leads-col-page">${pageCell}</td>
1580 - <td class="mxch-leads-col-actions">${viewBtn}${deleteBtn}</td>
1581 - </tr>
1582 - `;
1583 - });
1584 -
1585 - $tbody.html(html);
1586 - updateLeadsSelectionUI();
1587 - }
1588 -
1589 - function renderLeadsCount(start, end, total) {
1590 - if (!total) {
1591 - $('#mxch-leads-count').text('0 leads');
1592 - } else {
1593 - $('#mxch-leads-count').text(start + '-' + end + ' / ' + total + ' leads');
1594 - }
1595 - }
1596 -
1597 - function renderLeadsPagination(currentPage, totalPages) {
1598 - const $c = $('#mxch-leads-pagination');
1599 - if (!totalPages || totalPages <= 1) { $c.html(''); return; }
1600 - let html = '<div class="mxch-pagination-btns">';
1601 - if (currentPage > 1) {
1602 - html += `<button class="mxch-page-btn" data-page="${currentPage - 1}">&laquo;</button>`;
1603 - }
1604 - html += `<span class="mxch-page-info">${currentPage} / ${totalPages}</span>`;
1605 - if (currentPage < totalPages) {
1606 - html += `<button class="mxch-page-btn" data-page="${currentPage + 1}">&raquo;</button>`;
1607 - }
1608 - html += '</div>';
1609 - $c.html(html);
1610 - }
1611 -
1612 - function updateLeadsSelectionUI() {
1613 - const count = leadsState.selected.size;
1614 - const $countEl = $('#mxch-leads-selected-count');
1615 - const $del = $('#mxch-leads-delete-selected');
1616 - if (count > 0) {
1617 - $countEl.text(count + ' selected').addClass('has-selection');
1618 - $del.prop('disabled', false);
1619 - } else {
1620 - $countEl.text('0').removeClass('has-selection');
1621 - $del.prop('disabled', true);
1622 - }
1623 - // Selected-scope export menu items
1624 - $('#mxch-leads-export-menu button[data-scope="selected"]').prop('disabled', count === 0);
1625 -
1626 - // Select-all checkbox state
1627 - const $checks = $('.mxch-leads-rowcheck');
1628 - const checked = $checks.filter(':checked').length;
1629 - const total = $checks.length;
1630 - $('#mxch-leads-select-all').prop('checked', total > 0 && checked === total);
1631 - $('#mxch-leads-select-all').prop('indeterminate', checked > 0 && checked < total);
1632 - }
1633 -
1634 - // Trigger leads load when switching to the tab (works alongside the main nav handler above).
1635 - $('.mxch-nav-link[data-target="leads"], .mxch-mobile-nav-link[data-target="leads"]').on('click', function() {
1636 - if (!leadsState.loaded) {
1637 - loadLeads(1);
1638 - }
1639 - });
1640 -
1641 - // Filter: search (debounced)
1642 - let leadsSearchTimer;
1643 - $('#mxch-leads-search').on('input', function() {
1644 - clearTimeout(leadsSearchTimer);
1645 - const val = $(this).val();
1646 - leadsSearchTimer = setTimeout(function() {
1647 - leadsState.filters.search = (val || '').trim();
1648 - updateClearFiltersButton();
1649 - loadLeads(1);
1650 - }, 300);
1651 - });
1652 -
1653 - // Filter: date range
1654 - $('#mxch-leads-date-range').on('change', function() {
1655 - leadsState.filters.dateRange = $(this).val();
1656 - updateClearFiltersButton();
1657 - loadLeads(1);
1658 - });
1659 -
1660 - // Filter: status
1661 - $('#mxch-leads-status').on('change', function() {
1662 - leadsState.filters.status = $(this).val();
1663 - updateClearFiltersButton();
1664 - loadLeads(1);
1665 - });
1666 -
1667 - // Clear filters
1668 - $('#mxch-leads-clear-filters').on('click', function() {
1669 - leadsState.filters = { search: '', dateRange: 'all', status: 'all', pageUrl: '', pageTitle: '' };
1670 - $('#mxch-leads-search').val('');
1671 - $('#mxch-leads-date-range').val('all');
1672 - $('#mxch-leads-status').val('all');
1673 - setPageFilterChip('', '');
1674 - loadLeads(1);
1675 - });
1676 -
1677 - // Remove page chip
1678 - $leads().on('click', '.mxch-leads-page-chip-remove', function() {
1679 - setPageFilterChip('', '');
1680 - loadLeads(1);
1681 - });
1682 -
1683 - // Top Pages click -> set filter
1684 - $leads().on('click', '.mxch-leads-toppage-row', function() {
1685 - const url = $(this).data('url') || '';
1686 - const title = $(this).data('title') || '';
1687 - setPageFilterChip(url, title);
1688 - loadLeads(1);
1689 - });
1690 -
1691 - // Pagination click
1692 - $leads().on('click', '#mxch-leads-pagination .mxch-page-btn', function() {
1693 - const p = parseInt($(this).data('page'), 10);
1694 - if (p > 0) loadLeads(p);
1695 - });
1696 -
1697 - // Select-all
1698 - $('#mxch-leads-select-all').on('change', function() {
1699 - const on = $(this).is(':checked');
1700 - $('.mxch-leads-rowcheck').prop('checked', on);
1701 - $('.mxch-leads-row').each(function() {
1702 - const email = ($(this).data('email') || '').toString().toLowerCase();
1703 - if (on) {
1704 - leadsState.selected.add(email);
1705 - } else {
1706 - leadsState.selected.delete(email);
1707 - }
1708 - });
1709 - updateLeadsSelectionUI();
1710 - });
1711 -
1712 - // Row checkbox
1713 - $leads().on('change', '.mxch-leads-rowcheck', function() {
1714 - const email = ($(this).closest('.mxch-leads-row').data('email') || '').toString().toLowerCase();
1715 - if ($(this).is(':checked')) {
1716 - leadsState.selected.add(email);
1717 - } else {
1718 - leadsState.selected.delete(email);
1719 - }
1720 - updateLeadsSelectionUI();
1721 - });
1722 -
1723 - // View convo -> jump to All Chats tab and open the session
1724 - $leads().on('click', '.mxch-leads-view', function() {
1725 - const sid = $(this).attr('data-session-id');
1726 - if (!sid) return;
1727 - $('.mxch-nav-link[data-target="all-chats"]').trigger('click');
1728 - // selectChat is defined earlier in this closure
1729 - if (typeof selectChat === 'function') {
1730 - setTimeout(function() { selectChat(sid); }, 30);
1731 - }
1732 - });
1733 -
1734 - // Row delete -> confirm for one
1735 - $leads().on('click', '.mxch-leads-delete-row', function() {
1736 - const email = $(this).data('email');
1737 - if (!email) return;
1738 - openLeadsConfirm([String(email)]);
1739 - });
1740 -
1741 - // Bulk delete -> confirm for N
1742 - $('#mxch-leads-delete-selected').on('click', function() {
1743 - if (leadsState.selected.size === 0) return;
1744 - openLeadsConfirm(Array.from(leadsState.selected));
1745 - });
1746 -
1747 - function openLeadsConfirm(emails) {
1748 - leadsState.pendingDelete = emails;
1749 - const count = emails.length;
1750 - const msg = count === 1
1751 - ? 'Delete lead "' + emails[0] + '" and all of their conversations?'
1752 - : 'Delete ' + count + ' leads and all of their conversations?';
1753 - $('#mxch-leads-confirm-body').text(msg);
1754 - $('#mxch-leads-confirm').fadeIn(120);
1755 - }
1756 -
1757 - function closeLeadsConfirm() {
1758 - $('#mxch-leads-confirm').fadeOut(120);
1759 - leadsState.pendingDelete = [];
1760 - }
1761 -
1762 - $leads().on('click', '[data-mxch-leads-close]', closeLeadsConfirm);
1763 -
1764 - $('#mxch-leads-confirm-go').on('click', function() {
1765 - const emails = leadsState.pendingDelete.slice();
1766 - if (!emails.length) { closeLeadsConfirm(); return; }
1767 -
1768 - const $btn = $(this).prop('disabled', true).text('Deleting...');
1769 -
1770 - $.ajax({
1771 - url: ajaxurl,
1772 - type: 'POST',
1773 - data: {
1774 - action: 'mxchat_delete_leads',
1775 - security: $('#mxchat_leads_delete_nonce').val(),
1776 - emails: emails
1777 - },
1778 - success: function(response) {
1779 - $btn.prop('disabled', false).text('Delete permanently');
1780 - closeLeadsConfirm();
1781 - if (response && response.success) {
1782 - emails.forEach(function(e) { leadsState.selected.delete(e.toLowerCase()); });
1783 - loadLeads(leadsState.page);
1784 - } else {
1785 - alert((response && response.data && response.data.message) || 'Failed to delete leads.');
1786 - }
1787 - },
1788 - error: function() {
1789 - $btn.prop('disabled', false).text('Delete permanently');
1790 - alert('Network error while deleting.');
1791 - }
1792 - });
1793 - });
1794 -
1795 - // Export dropdown
1796 - $('#mxch-leads-export-btn').on('click', function(e) {
1797 - e.stopPropagation();
1798 - $('#mxch-leads-export-menu').toggleClass('is-open');
1799 - });
1800 -
1801 - $(document).on('click', function() {
1802 - $('#mxch-leads-export-menu').removeClass('is-open');
1803 - });
1804 -
1805 - $('#mxch-leads-export-menu').on('click', function(e) { e.stopPropagation(); });
1806 -
1807 - $('#mxch-leads-export-menu button').on('click', function() {
1808 - if ($(this).prop('disabled')) return;
1809 - const scope = $(this).data('scope') || 'all';
1810 - const fields = $(this).data('fields') || 'email_and_name';
1811 - submitLeadsExport(scope, fields);
1812 - $('#mxch-leads-export-menu').removeClass('is-open');
1813 - });
1814 -
1815 - function submitLeadsExport(scope, fields) {
1816 - const $form = $('<form>', { method: 'POST', action: ajaxurl, style: 'display:none;' });
1817 - $form.append($('<input>', { type: 'hidden', name: 'action', value: 'mxchat_export_leads' }));
1818 - $form.append($('<input>', { type: 'hidden', name: 'security', value: $('#mxchat_leads_export_nonce').val() }));
1819 - $form.append($('<input>', { type: 'hidden', name: 'scope', value: scope }));
1820 - $form.append($('<input>', { type: 'hidden', name: 'fields', value: fields }));
1821 - if (scope === 'selected') {
1822 - Array.from(leadsState.selected).forEach(function(e) {
1823 - $form.append($('<input>', { type: 'hidden', name: 'emails[]', value: e }));
1824 - });
1825 - }
1826 - $form.appendTo('body').submit().remove();
1827 - }
1828 -
1829 - // Preload leads metadata on page load (for the nav badge count only) without rendering.
1830 - // We keep this light — the full fetch only runs when the tab is clicked.
1831 - $.ajax({
1832 - url: ajaxurl,
1833 - type: 'POST',
1834 - data: { action: 'mxchat_fetch_leads', page: 1, per_page: 1 },
1835 - success: function(response) {
1836 - if (response && response.success && response.stats) {
1837 - const total = response.stats.total_leads || 0;
1838 - const $badge = $('#mxch-leads-nav-badge');
1839 - if (total > 0) $badge.text(total).show();
1840 - }
1841 - }
1842 49 });
1843 50 });