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

1,844 lines 76.5 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 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 });
1843 });
1844