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

1,280 lines 49.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * MxChat Transcripts Page JavaScript - v3.0
3 * Split-panel layout with chat list and conversation view
4 */
5 jQuery(document).ready(function($) {
6 // ==========================================================================
7 // Sidebar Navigation
8 // ==========================================================================
9
10 // Desktop sidebar navigation
11 $('.mxch-nav-link').on('click', function(e) {
12 e.preventDefault();
13 const target = $(this).data('target');
14
15 // Update active states
16 $('.mxch-nav-link').removeClass('active');
17 $(this).addClass('active');
18
19 // Show target section
20 $('.mxch-section').removeClass('active');
21 $('#' + target).addClass('active');
22
23 // Reset scroll position of content area
24 $('.mxch-content').scrollTop(0);
25
26 // Also update mobile nav if open
27 $('.mxch-mobile-nav-link').removeClass('active');
28 $('.mxch-mobile-nav-link[data-target="' + target + '"]').addClass('active');
29
30 // Load transcripts when switching to all-chats
31 if (target === 'all-chats' && !transcriptsLoaded) {
32 loadChatList(1, '');
33 }
34 });
35
36 // Mobile menu toggle
37 $('.mxch-mobile-menu-btn').on('click', function() {
38 $('.mxch-mobile-menu').addClass('open');
39 $('.mxch-mobile-overlay').addClass('open');
40 });
41
42 // Close mobile menu
43 $('.mxch-mobile-menu-close, .mxch-mobile-overlay').on('click', function() {
44 $('.mxch-mobile-menu').removeClass('open');
45 $('.mxch-mobile-overlay').removeClass('open');
46 });
47
48 // Mobile navigation
49 $('.mxch-mobile-nav-link').on('click', function(e) {
50 e.preventDefault();
51 const target = $(this).data('target');
52
53 $('.mxch-mobile-nav-link').removeClass('active');
54 $(this).addClass('active');
55
56 $('.mxch-section').removeClass('active');
57 $('#' + target).addClass('active');
58
59 // Reset scroll position of content area
60 $('.mxch-content').scrollTop(0);
61
62 $('.mxch-nav-link').removeClass('active');
63 $('.mxch-nav-link[data-target="' + target + '"]').addClass('active');
64
65 $('.mxch-mobile-menu').removeClass('open');
66 $('.mxch-mobile-overlay').removeClass('open');
67 });
68
69 // Quick action buttons
70 $('.mxch-quick-action-btn[data-action="view-chats"]').on('click', function() {
71 $('.mxch-nav-link[data-target="all-chats"]').trigger('click');
72 });
73
74 $('.mxch-quick-action-btn[data-action="settings"]').on('click', function() {
75 $('.mxch-nav-link[data-target="notifications"]').trigger('click');
76 });
77
78 // ==========================================================================
79 // Mobile Panel Management
80 // ==========================================================================
81
82 function isMobile() {
83 return window.innerWidth <= 782;
84 }
85
86 function showMobileConversationPanel() {
87 if (isMobile()) {
88 $('.mxch-chat-list-panel').addClass('panel-hidden');
89 $('#mxch-conversation-panel').addClass('panel-active');
90 $('#mxch-transcript-back-btn').show();
91 }
92 }
93
94 function hideMobileConversationPanel() {
95 if (isMobile()) {
96 $('#mxch-conversation-panel').removeClass('panel-active');
97 $('.mxch-chat-list-panel').removeClass('panel-hidden');
98 $('#mxch-transcript-back-btn').hide();
99 }
100 }
101
102 // Mobile back button handler
103 $('#mxch-transcript-back-btn').on('click', function(e) {
104 e.preventDefault();
105 hideMobileConversationPanel();
106 $('.mxch-chat-item').removeClass('active');
107 currentSessionId = null;
108 });
109
110 // Handle window resize
111 $(window).on('resize', function() {
112 if (!isMobile()) {
113 // Reset panel states when switching to desktop
114 $('.mxch-chat-list-panel').removeClass('panel-hidden');
115 $('#mxch-conversation-panel').removeClass('panel-active');
116 $('#mxch-transcript-back-btn').hide();
117 }
118 updateMobileViewportHeight();
119 });
120
121 // Fix for mobile browser address bar - sets CSS custom property for accurate viewport height
122 function updateMobileViewportHeight() {
123 if (isMobile()) {
124 // Use visualViewport if available (most reliable for mobile)
125 const vh = window.visualViewport ? window.visualViewport.height : window.innerHeight;
126 document.documentElement.style.setProperty('--mxch-mobile-vh', vh + 'px');
127 }
128 }
129
130 // Update on load and viewport changes
131 updateMobileViewportHeight();
132 if (window.visualViewport) {
133 window.visualViewport.addEventListener('resize', updateMobileViewportHeight);
134 }
135
136 // ==========================================================================
137 // Chat List - Split Panel
138 // ==========================================================================
139
140 let currentPage = 1;
141 const perPage = 50;
142 let totalPages = 1;
143 let currentSessionId = null;
144 let transcriptsLoaded = false;
145 let selectedSessions = new Set();
146 let currentSortOrder = 'desc'; // newest first
147
148 // Load chat list on page load
149 loadChatList(1, '');
150
151 // Search functionality with debounce
152 let searchTimeout;
153 $('#mxch-search-transcripts').on('input', function() {
154 clearTimeout(searchTimeout);
155 const searchTerm = $(this).val().toLowerCase();
156
157 searchTimeout = setTimeout(function() {
158 currentPage = 1;
159 loadChatList(currentPage, searchTerm);
160 }, 300);
161 });
162
163 // Refresh button
164 $('#mxch-refresh-list').on('click', function() {
165 const $btn = $(this);
166 $btn.addClass('spinning');
167 loadChatList(currentPage, $('#mxch-search-transcripts').val());
168 setTimeout(() => $btn.removeClass('spinning'), 500);
169 });
170
171 // ==========================================================================
172 // Bulk Selection & Actions
173 // ==========================================================================
174
175 // Select all checkbox
176 $('#mxch-select-all').on('change', function() {
177 const isChecked = $(this).is(':checked');
178 $('.mxch-chat-checkbox').prop('checked', isChecked);
179
180 if (isChecked) {
181 $('.mxch-chat-item').each(function() {
182 selectedSessions.add($(this).data('session-id'));
183 $(this).addClass('selected');
184 });
185 $('#mxch-chat-list').addClass('selection-mode');
186 } else {
187 selectedSessions.clear();
188 $('.mxch-chat-item').removeClass('selected');
189 $('#mxch-chat-list').removeClass('selection-mode');
190 }
191
192 updateSelectionUI();
193 });
194
195 // Update selection UI
196 function updateSelectionUI() {
197 const count = selectedSessions.size;
198 const $countEl = $('#mxch-selected-count');
199 const $deleteBtn = $('#mxch-delete-selected');
200
201 if (count > 0) {
202 $countEl.text(count + ' selected').addClass('has-selection');
203 $deleteBtn.prop('disabled', false);
204 $('#mxch-chat-list').addClass('selection-mode');
205 } else {
206 $countEl.removeClass('has-selection');
207 $deleteBtn.prop('disabled', true);
208 $('#mxch-chat-list').removeClass('selection-mode');
209 }
210
211 // Update select all checkbox state
212 const totalItems = $('.mxch-chat-checkbox').length;
213 const checkedItems = $('.mxch-chat-checkbox:checked').length;
214 $('#mxch-select-all').prop('checked', totalItems > 0 && checkedItems === totalItems);
215 $('#mxch-select-all').prop('indeterminate', checkedItems > 0 && checkedItems < totalItems);
216 }
217
218 // Sort button
219 $('#mxch-sort-btn').on('click', function() {
220 currentSortOrder = currentSortOrder === 'desc' ? 'asc' : 'desc';
221 $(this).find('svg').css('transform', currentSortOrder === 'asc' ? 'rotate(180deg)' : 'rotate(0deg)');
222 loadChatList(currentPage, $('#mxch-search-transcripts').val());
223 });
224
225 // Delete selected button
226 $('#mxch-delete-selected').on('click', function() {
227 const count = selectedSessions.size;
228 if (count === 0) return;
229
230 if (!confirm('Are you sure you want to delete ' + count + ' conversation(s)? This action cannot be undone.')) {
231 return;
232 }
233
234 deleteMultipleSessions(Array.from(selectedSessions));
235 });
236
237 // Delete multiple sessions
238 function deleteMultipleSessions(sessionIds) {
239 $.ajax({
240 url: ajaxurl,
241 type: 'POST',
242 data: {
243 action: 'mxchat_delete_chat_history',
244 delete_session_ids: sessionIds,
245 security: $('#mxchat_delete_chat_nonce').val()
246 },
247 success: function(response) {
248 try {
249 const jsonResponse = typeof response === 'object' ? response : JSON.parse(response);
250
251 if (jsonResponse.success) {
252 // Clear selection
253 selectedSessions.clear();
254 $('#mxch-select-all').prop('checked', false);
255 updateSelectionUI();
256
257 // If current conversation was deleted, reset panel
258 if (sessionIds.includes(currentSessionId)) {
259 currentSessionId = null;
260 $('#mxch-conversation-content').hide();
261 $('#mxch-conversation-empty').show();
262 $('#mxch-details-drawer').hide();
263 }
264
265 // Reload list
266 loadChatList(currentPage, $('#mxch-search-transcripts').val());
267 } else if (jsonResponse.error) {
268 alert('Error: ' + jsonResponse.error);
269 }
270 } catch (e) {
271 alert('An error occurred while processing the response.');
272 }
273 },
274 error: function() {
275 alert('An error occurred while deleting conversations.');
276 }
277 });
278 }
279
280 // Load chat list function
281 function loadChatList(page, searchTerm) {
282 const $container = $('#mxch-chat-list');
283 $container.html('<div class="mxch-list-loading"><span class="spinner is-active"></span></div>');
284
285 $.ajax({
286 url: ajaxurl,
287 type: 'POST',
288 data: {
289 action: 'mxchat_fetch_chat_history',
290 page: page,
291 per_page: perPage,
292 search: searchTerm,
293 sort_order: currentSortOrder
294 },
295 success: function(response) {
296 transcriptsLoaded = true;
297
298 if (response.success && response.sessions && response.sessions.length > 0) {
299 renderChatList(response.sessions);
300 currentPage = response.page;
301 totalPages = response.total_pages;
302 updateChatCount(response.showing_start, response.showing_end, response.total_sessions);
303 renderPagination(response.page, response.total_pages, searchTerm);
304 } else {
305 $container.html('<div class="mxch-list-empty"><p>No chats found</p></div>');
306 updateChatCount(0, 0, 0);
307 $('#mxch-pagination').html('');
308 }
309 },
310 error: function() {
311 $container.html('<div class="mxch-list-empty"><p>Error loading chats</p></div>');
312 }
313 });
314 }
315
316 // Render chat list items
317 function renderChatList(sessions) {
318 const $container = $('#mxch-chat-list');
319 let html = '';
320
321 sessions.forEach(function(session) {
322 const isActive = session.session_id === currentSessionId ? ' active' : '';
323 const isSelected = selectedSessions.has(session.session_id) ? ' selected' : '';
324 const isChecked = selectedSessions.has(session.session_id) ? ' checked' : '';
325 html += `
326 <div class="mxch-chat-item${isActive}${isSelected}" data-session-id="${escapeHtml(session.session_id)}">
327 <input type="checkbox" class="mxch-chat-checkbox"${isChecked}>
328 <div class="mxch-chat-avatar">
329 <span>${escapeHtml(session.initials)}</span>
330 </div>
331 <div class="mxch-chat-info">
332 <div class="mxch-chat-name">${escapeHtml(session.display_name)}</div>
333 <div class="mxch-chat-preview">${escapeHtml(session.preview)}</div>
334 </div>
335 <div class="mxch-chat-meta">
336 <span class="mxch-chat-time">${escapeHtml(session.time_display)}</span>
337 <span class="mxch-chat-count">${session.message_count}</span>
338 </div>
339 </div>
340 `;
341 });
342
343 $container.html(html);
344
345 // Attach checkbox handlers
346 $('.mxch-chat-checkbox').on('click', function(e) {
347 e.stopPropagation(); // Prevent triggering chat item click
348 const $item = $(this).closest('.mxch-chat-item');
349 const sessionId = $item.data('session-id');
350
351 if ($(this).is(':checked')) {
352 selectedSessions.add(sessionId);
353 $item.addClass('selected');
354 } else {
355 selectedSessions.delete(sessionId);
356 $item.removeClass('selected');
357 }
358
359 updateSelectionUI();
360 });
361
362 // Attach click handlers for selecting chat
363 $('.mxch-chat-item').on('click', function(e) {
364 // Don't trigger if clicking on checkbox
365 if ($(e.target).is('.mxch-chat-checkbox')) return;
366
367 const sessionId = $(this).data('session-id');
368 selectChat(sessionId);
369
370 // Update active state
371 $('.mxch-chat-item').removeClass('active');
372 $(this).addClass('active');
373
374 // Show conversation panel on mobile
375 showMobileConversationPanel();
376 });
377
378 // Update selection UI after render
379 updateSelectionUI();
380 }
381
382 // Update chat count display
383 function updateChatCount(start, end, total) {
384 if (total === 0) {
385 $('#mxch-chat-count').text('0 chats');
386 } else {
387 $('#mxch-chat-count').text(`${start}-${end} / ${total} chats`);
388 }
389 }
390
391 // Render pagination
392 function renderPagination(currentPage, totalPages, searchTerm) {
393 const $container = $('#mxch-pagination');
394
395 if (totalPages <= 1) {
396 $container.html('');
397 return;
398 }
399
400 let html = '<div class="mxch-pagination-btns">';
401
402 if (currentPage > 1) {
403 html += `<button class="mxch-page-btn" data-page="${currentPage - 1}">&laquo;</button>`;
404 }
405
406 html += `<span class="mxch-page-info">${currentPage} / ${totalPages}</span>`;
407
408 if (currentPage < totalPages) {
409 html += `<button class="mxch-page-btn" data-page="${currentPage + 1}">&raquo;</button>`;
410 }
411
412 html += '</div>';
413 $container.html(html);
414
415 // Pagination click handlers
416 $('.mxch-page-btn').on('click', function() {
417 const pageNum = $(this).data('page');
418 loadChatList(pageNum, searchTerm);
419 });
420 }
421
422 // ==========================================================================
423 // Conversation Panel
424 // ==========================================================================
425
426 // Select and load a chat conversation
427 function selectChat(sessionId) {
428 currentSessionId = sessionId;
429
430 // Reset translation state when selecting new chat
431 if (typeof resetTranslationState === 'function') {
432 resetTranslationState();
433 }
434
435 // Show loading in conversation panel
436 $('#mxch-conversation-empty').hide();
437 $('#mxch-conversation-content').show();
438 $('#mxch-messages-area').html('<div class="mxch-messages-loading"><span class="spinner is-active"></span> Loading conversation...</div>');
439
440 $.ajax({
441 url: ajaxurl,
442 type: 'POST',
443 data: {
444 action: 'mxchat_fetch_conversation',
445 session_id: sessionId
446 },
447 success: function(response) {
448 if (response.success) {
449 renderConversation(response);
450 // Load saved translation after rendering
451 if (typeof loadSavedTranslation === 'function') {
452 setTimeout(function() {
453 loadSavedTranslation(sessionId);
454 }, 100);
455 }
456 } else {
457 $('#mxch-messages-area').html('<div class="mxch-messages-error">Failed to load conversation</div>');
458 }
459 },
460 error: function() {
461 $('#mxch-messages-area').html('<div class="mxch-messages-error">Error loading conversation</div>');
462 }
463 });
464 }
465
466 // Render conversation content
467 function renderConversation(data) {
468 // Update header
469 $('#mxch-user-avatar span').text(data.user.initials);
470 $('#mxch-user-name').text(data.user.name);
471 $('#mxch-user-meta').text(data.user.sub);
472
473 // Update details drawer
474 $('#mxch-detail-messages').text(data.message_count);
475 $('#mxch-detail-started').text(data.started);
476
477 if (data.page.url) {
478 $('#mxch-detail-page').html(`<a href="${escapeHtml(data.page.url)}" target="_blank">${escapeHtml(data.page.title || data.page.url)}</a>`);
479 } else {
480 $('#mxch-detail-page').text('-');
481 }
482
483 if (data.user.email) {
484 $('#mxch-detail-email').text(data.user.email);
485 $('#mxch-detail-email-row').show();
486 } else {
487 $('#mxch-detail-email-row').hide();
488 }
489
490 // Clicked links
491 if (data.clicked_urls && data.clicked_urls.length > 0) {
492 let linksHtml = '';
493 data.clicked_urls.forEach(function(url) {
494 linksHtml += `<a href="${escapeHtml(url)}" target="_blank" class="mxch-clicked-link">${escapeHtml(url)}</a>`;
495 });
496 $('#mxch-clicked-links').html(linksHtml);
497 $('#mxch-clicked-section').show();
498 } else {
499 $('#mxch-clicked-section').hide();
500 }
501
502 // Render messages
503 let messagesHtml = '';
504 data.messages.forEach(function(msg) {
505 if (msg.is_user) {
506 messagesHtml += `
507 <div class="mxch-message mxch-message-user" data-message-id="${msg.id}">
508 <div class="mxch-message-row">
509 <div class="mxch-message-bubble">
510 ${msg.content}
511 </div>
512 </div>
513 <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>
514 </div>
515 `;
516 } else {
517 const ragLink = msg.has_rag ? `<a href="#" class="mxch-rag-link" data-message-id="${msg.id}">Sources</a>` : '';
518 messagesHtml += `
519 <div class="mxch-message mxch-message-bot" data-message-id="${msg.id}">
520 <div class="mxch-message-header">
521 <span class="mxch-bot-label">AI Assistant</span>
522 ${ragLink}
523 </div>
524 <div class="mxch-message-row">
525 <div class="mxch-message-bubble">
526 ${msg.content}
527 </div>
528 </div>
529 <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>
530 </div>
531 `;
532 }
533 });
534
535 $('#mxch-messages-area').html(messagesHtml);
536
537 // Scroll to bottom
538 const $area = $('#mxch-messages-area');
539 $area.scrollTop($area[0].scrollHeight);
540
541 // Attach RAG link handlers
542 $('.mxch-rag-link').on('click', function(e) {
543 e.preventDefault();
544 const messageId = $(this).data('message-id');
545 if (messageId) {
546 openRagContextModal(messageId);
547 }
548 });
549 }
550
551 // Toggle details drawer
552 $('#mxch-toggle-details').on('click', function() {
553 const $drawer = $('#mxch-details-drawer');
554 const $btn = $(this);
555
556 if ($drawer.is(':visible')) {
557 $drawer.slideUp(200);
558 $btn.removeClass('active');
559 } else {
560 $drawer.slideDown(200);
561 $btn.addClass('active');
562 }
563 });
564
565 // Delete current chat
566 $('#mxch-delete-current').on('click', function() {
567 if (!currentSessionId) return;
568
569 if (!confirm('Are you sure you want to delete this conversation? This action cannot be undone.')) {
570 return;
571 }
572
573 deleteSession(currentSessionId);
574 });
575
576 // Delete session function
577 function deleteSession(sessionId) {
578 $.ajax({
579 url: ajaxurl,
580 type: 'POST',
581 data: {
582 action: 'mxchat_delete_chat_history',
583 delete_session_ids: [sessionId],
584 security: $('#mxchat_delete_chat_nonce').val()
585 },
586 success: function(response) {
587 try {
588 const jsonResponse = typeof response === 'object' ? response : JSON.parse(response);
589
590 if (jsonResponse.success) {
591 // Reset conversation panel
592 currentSessionId = null;
593 $('#mxch-conversation-content').hide();
594 $('#mxch-conversation-empty').show();
595 $('#mxch-details-drawer').hide();
596
597 // Reload list
598 loadChatList(currentPage, $('#mxch-search-transcripts').val());
599 } else if (jsonResponse.error) {
600 alert('Error: ' + jsonResponse.error);
601 }
602 } catch (e) {
603 alert('An error occurred while processing the response.');
604 }
605 },
606 error: function() {
607 alert('An error occurred while deleting the conversation.');
608 }
609 });
610 }
611
612 // ==========================================================================
613 // Export Functionality
614 // ==========================================================================
615
616 $('#mxch-export-btn, #mxch-export-current').on('click', function() {
617 const $button = $(this);
618 $button.prop('disabled', true).addClass('loading');
619
620 const $form = $('<form>', {
621 method: 'post',
622 action: ajaxurl
623 });
624
625 $form.append($('<input>', {
626 type: 'hidden',
627 name: 'action',
628 value: 'mxchat_export_transcripts'
629 }));
630
631 $form.append($('<input>', {
632 type: 'hidden',
633 name: 'security',
634 value: mxchatAdmin.export_nonce
635 }));
636
637 $form.appendTo('body').submit();
638
639 setTimeout(function() {
640 $button.prop('disabled', false).removeClass('loading');
641 }, 2000);
642 });
643
644 // ==========================================================================
645 // Translation Functionality
646 // ==========================================================================
647
648 // Store original messages for reverting
649 let originalMessages = null;
650 let isTranslated = false;
651 let currentTranslationLang = null;
652
653 // Load saved language preference from localStorage
654 const savedLang = localStorage.getItem('mxch_translate_lang');
655 if (savedLang) {
656 $('#mxch-translate-lang').val(savedLang);
657 }
658
659 // Save language preference when changed
660 $('#mxch-translate-lang').on('change', function() {
661 localStorage.setItem('mxch_translate_lang', $(this).val());
662 });
663
664 // Apply translations to messages
665 function applyTranslations(translations) {
666 // Store original messages if not already stored
667 if (!originalMessages) {
668 originalMessages = [];
669 $('#mxch-messages-area .mxch-message-bubble').each(function() {
670 originalMessages.push($(this).html());
671 });
672 }
673
674 // Apply translations
675 translations.forEach(function(item) {
676 const $bubble = $('#mxch-messages-area .mxch-message-bubble').eq(item.index);
677 if ($bubble.length) {
678 $bubble.html(item.translated);
679 $bubble.addClass('translated');
680 }
681 });
682
683 isTranslated = true;
684 $('#mxch-show-original-btn').show();
685 }
686
687 // Load saved translation for current session
688 function loadSavedTranslation(sessionId) {
689 $.ajax({
690 url: ajaxurl,
691 type: 'POST',
692 data: {
693 action: 'mxchat_get_transcript_translation',
694 session_id: sessionId
695 },
696 success: function(response) {
697 if (response.success && response.has_translation) {
698 currentTranslationLang = response.language;
699 applyTranslations(response.translations);
700 // Update language selector to show saved language
701 $('#mxch-translate-lang').val(response.language);
702 }
703 }
704 });
705 }
706
707 // Translate button click handler
708 $('#mxch-translate-btn').on('click', function() {
709 if (!currentSessionId) return;
710
711 const $btn = $(this);
712 const targetLang = $('#mxch-translate-lang').val();
713
714 // Disable button and show loading state
715 $btn.prop('disabled', true);
716 $btn.find('.mxch-translate-text').text('Translating...');
717 $btn.find('svg').addClass('mxch-translate-spinner');
718
719 // Store original messages before translation
720 if (!originalMessages) {
721 originalMessages = [];
722 $('#mxch-messages-area .mxch-message-bubble').each(function() {
723 originalMessages.push($(this).html());
724 });
725 }
726
727 // If already translated, restore originals first before re-translating
728 if (isTranslated) {
729 $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
730 if (originalMessages[index]) {
731 $(this).html(originalMessages[index]);
732 $(this).removeClass('translated');
733 }
734 });
735 }
736
737 // Collect all message content (from originals)
738 const messages = [];
739 originalMessages.forEach(function(html, index) {
740 // Create temp element to get text content
741 const $temp = $('<div>').html(html);
742 messages.push({
743 index: index,
744 content: $temp.text().trim()
745 });
746 });
747
748 // Send translation request
749 $.ajax({
750 url: ajaxurl,
751 type: 'POST',
752 data: {
753 action: 'mxchat_translate_messages',
754 session_id: currentSessionId,
755 target_lang: targetLang,
756 messages: JSON.stringify(messages),
757 security: mxchatAdmin.translate_nonce || ''
758 },
759 success: function(response) {
760 if (response.success && response.translations) {
761 currentTranslationLang = response.language;
762 applyTranslations(response.translations);
763 $btn.find('.mxch-translate-text').text('Translate');
764 } else {
765 alert(response.error || 'Translation failed. Please try again.');
766 $btn.find('.mxch-translate-text').text('Translate');
767 }
768 },
769 error: function() {
770 alert('Translation request failed. Please try again.');
771 $btn.find('.mxch-translate-text').text('Translate');
772 },
773 complete: function() {
774 $btn.prop('disabled', false);
775 $btn.find('svg').removeClass('mxch-translate-spinner');
776 }
777 });
778 });
779
780 // Show original button click handler
781 $('#mxch-show-original-btn').on('click', function() {
782 if (!originalMessages) return;
783
784 // Restore original messages
785 $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
786 if (originalMessages[index]) {
787 $(this).html(originalMessages[index]);
788 $(this).removeClass('translated');
789 }
790 });
791
792 isTranslated = false;
793 $(this).hide();
794 });
795
796 // Reset translation state (called when selecting new chat)
797 function resetTranslationState() {
798 originalMessages = null;
799 isTranslated = false;
800 currentTranslationLang = null;
801 $('#mxch-show-original-btn').hide();
802 }
803
804 // Make functions available to selectChat
805 window.resetTranslationState = resetTranslationState;
806 window.loadSavedTranslation = loadSavedTranslation;
807
808 // ==========================================================================
809 // RAG Context Modal (Sources & Actions Tabs)
810 // ==========================================================================
811
812 function openRagContextModal(messageId) {
813 const $modal = $('#mxch-rag-modal');
814 const $loading = $modal.find('.mxch-rag-loading');
815 const $sourcesContent = $modal.find('.mxch-rag-content');
816 const $actionsContent = $modal.find('.mxch-actions-content');
817
818 // Reset to Sources tab
819 $modal.find('.mxch-context-tab').removeClass('active');
820 $modal.find('.mxch-context-tab[data-tab="sources"]').addClass('active');
821 $('#mxch-tab-sources').show();
822 $('#mxch-tab-actions').hide();
823
824 // Reset badge counts
825 $('#mxch-sources-count, #mxch-actions-count').hide().text('0');
826
827 $modal.fadeIn(200);
828 $loading.show();
829 $sourcesContent.html('');
830 $actionsContent.html('');
831
832 $.ajax({
833 url: ajaxurl,
834 type: 'POST',
835 data: {
836 action: 'mxchat_get_rag_context',
837 message_id: messageId
838 },
839 success: function(response) {
840 $loading.hide();
841
842 if (response.success && response.data) {
843 // Render sources tab
844 renderRagContext(response.data, $sourcesContent);
845
846 // Render actions tab
847 renderActionsContext(response.data, $actionsContent);
848
849 // Update badge counts
850 const sourcesCount = response.data.top_matches ? response.data.top_matches.length : 0;
851 const actionsCount = response.data.action_analysis ? response.data.action_analysis.length : 0;
852
853 if (sourcesCount > 0) {
854 $('#mxch-sources-count').text(sourcesCount).show();
855 }
856 if (actionsCount > 0) {
857 $('#mxch-actions-count').text(actionsCount).show();
858 }
859 } else {
860 $sourcesContent.html('<div class="mxch-rag-error">Unable to load document context.</div>');
861 $actionsContent.html('<div class="mxch-rag-error">No action data available.</div>');
862 }
863 },
864 error: function() {
865 $loading.hide();
866 $sourcesContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
867 $actionsContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
868 }
869 });
870 }
871
872 // Tab switching
873 $(document).on('click', '.mxch-context-tab', function() {
874 const $tab = $(this);
875 const tabName = $tab.data('tab');
876
877 // Update active tab
878 $('.mxch-context-tab').removeClass('active');
879 $tab.addClass('active');
880
881 // Show/hide content
882 $('.mxch-tab-content').hide();
883 $('#mxch-tab-' + tabName).show();
884 });
885
886 function renderRagContext(data, $container) {
887 let html = '';
888
889 // Check if we have any source data
890 if (!data.top_matches || data.top_matches.length === 0) {
891 html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>';
892 $container.html(html);
893 return;
894 }
895
896 html += '<div class="mxch-rag-summary">';
897 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>';
898 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>';
899 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>';
900 html += '</div>';
901
902 const groupedByUrl = {};
903
904 data.top_matches.forEach(function(match) {
905 const url = match.source_display || 'Unknown';
906 if (!groupedByUrl[url]) {
907 groupedByUrl[url] = {
908 url: url,
909 isUrl: url.startsWith('http'),
910 bestScore: 0,
911 usedForContext: false,
912 matchedChunks: []
913 };
914 }
915
916 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
917 groupedByUrl[url].bestScore = match.similarity_percentage;
918 }
919
920 if (match.used_for_context) {
921 groupedByUrl[url].usedForContext = true;
922 }
923
924 groupedByUrl[url].matchedChunks.push({
925 chunkIndex: match.chunk_index,
926 score: match.similarity_percentage,
927 usedForContext: match.used_for_context
928 });
929 });
930
931 const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore);
932 const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length;
933 const chunksInfo = data.total_chunks_used > 0 ? data.total_chunks_used + ' chunks sent to AI' : '';
934
935 html += '<div class="mxch-rag-matches">';
936 html += '<h3>Retrieved Documents</h3>';
937 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>';
938
939 urlGroups.forEach(function(group) {
940 const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
941 const statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
942 const statusLabel = group.usedForContext ? 'Used' : 'Not Used';
943
944 html += '<div class="mxch-rag-match-card ' + cardClass + '">';
945 html += '<div class="mxch-rag-match-header">';
946 html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
947
948 if (group.matchedChunks.length > 1) {
949 html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
950 }
951
952 html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
953 html += '</div>';
954
955 html += '<div class="mxch-rag-match-source">';
956 if (group.isUrl) {
957 html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>';
958 } else {
959 html += escapeHtml(group.url);
960 }
961 html += '</div>';
962 html += '</div>';
963 });
964
965 html += '</div>';
966 $container.html(html);
967 }
968
969 function renderActionsContext(data, $container) {
970 let html = '';
971
972 // Check if we have action analysis data
973 if (!data.action_analysis || data.action_analysis.length === 0) {
974 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>';
975 $container.html(html);
976 return;
977 }
978
979 const actions = data.action_analysis;
980 const triggeredAction = actions.find(a => a.triggered);
981 const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
982
983 // Summary section
984 html += '<div class="mxch-rag-summary">';
985 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Actions Evaluated:</span> <span class="mxch-rag-value">' + actions.length + '</span></div>';
986 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Above Threshold:</span> <span class="mxch-rag-value">' + actionsAboveThreshold + '</span></div>';
987 if (triggeredAction) {
988 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>';
989 }
990 html += '</div>';
991
992 // Actions list
993 html += '<div class="mxch-rag-matches">';
994 html += '<h3>Action Scores</h3>';
995 html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">Showing all evaluated actions sorted by similarity score</p>';
996
997 actions.forEach(function(action) {
998 let cardClass = 'mxch-rag-match-below';
999 let statusIcon = '&#10007;';
1000 let statusLabel = 'Below Threshold';
1001
1002 if (action.triggered) {
1003 cardClass = 'mxch-action-triggered';
1004 statusIcon = '&#9889;';
1005 statusLabel = 'Triggered';
1006 } else if (action.above_threshold) {
1007 cardClass = 'mxch-rag-match-used';
1008 statusIcon = '&#10003;';
1009 statusLabel = 'Above Threshold';
1010 }
1011
1012 html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1013 html += '<div class="mxch-rag-match-header">';
1014 html += '<span class="mxch-rag-match-score">' + action.similarity_percentage + '%</span>';
1015 html += '<span class="mxch-action-threshold-badge">Threshold: ' + action.threshold_percentage + '%</span>';
1016 html += '<span class="mxch-rag-match-status ' + (action.triggered ? 'status-triggered' : (action.above_threshold ? 'status-used' : 'status-below')) + '">' + statusIcon + ' ' + statusLabel + '</span>';
1017 html += '</div>';
1018
1019 html += '<div class="mxch-action-details">';
1020 html += '<div class="mxch-action-label">' + escapeHtml(action.intent_label) + '</div>';
1021 html += '<div class="mxch-action-callback"><span class="mxch-action-callback-label">Callback:</span> ' + escapeHtml(action.callback_function) + '</div>';
1022 html += '</div>';
1023
1024 // Score bar visualization
1025 const scoreBarWidth = Math.min(action.similarity_percentage, 100);
1026 const thresholdPos = Math.min(action.threshold_percentage, 100);
1027 html += '<div class="mxch-action-score-bar">';
1028 html += '<div class="mxch-action-score-fill" style="width: ' + scoreBarWidth + '%;"></div>';
1029 html += '<div class="mxch-action-threshold-marker" style="left: ' + thresholdPos + '%;"></div>';
1030 html += '</div>';
1031
1032 html += '</div>';
1033 });
1034
1035 html += '</div>';
1036 $container.html(html);
1037 }
1038
1039 function escapeHtml(text) {
1040 if (!text) return '';
1041 const div = document.createElement('div');
1042 div.textContent = text;
1043 return div.innerHTML;
1044 }
1045
1046 // Close RAG modal
1047 $('.mxch-modal-close').on('click', function() {
1048 $(this).closest('.mxch-modal-overlay').fadeOut(200);
1049 });
1050
1051 $('.mxch-modal-overlay').on('click', function(e) {
1052 if ($(e.target).is('.mxch-modal-overlay')) {
1053 $(this).fadeOut(200);
1054 }
1055 });
1056
1057 $(document).on('keydown', function(e) {
1058 if (e.key === 'Escape') {
1059 $('.mxch-modal-overlay').fadeOut(200);
1060 }
1061 });
1062
1063 // ==========================================================================
1064 // Activity Chart
1065 // ==========================================================================
1066
1067 // Simple chart implementation (no external dependencies)
1068 class SimpleChart {
1069 constructor(canvas, config) {
1070 this.canvas = canvas;
1071 this.ctx = canvas.getContext('2d');
1072 this.config = config;
1073 this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
1074 this.render();
1075 }
1076
1077 destroy() {
1078 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1079 }
1080
1081 render() {
1082 const dpr = window.devicePixelRatio || 1;
1083 const rect = this.canvas.getBoundingClientRect();
1084
1085 this.canvas.width = rect.width * dpr;
1086 this.canvas.height = rect.height * dpr;
1087 this.ctx.scale(dpr, dpr);
1088
1089 this.canvas.style.width = rect.width + 'px';
1090 this.canvas.style.height = rect.height + 'px';
1091
1092 const width = rect.width - this.padding.left - this.padding.right;
1093 const height = rect.height - this.padding.top - this.padding.bottom;
1094
1095 // Find max value
1096 let maxValue = 0;
1097 this.config.datasets.forEach(dataset => {
1098 const max = Math.max(...dataset.data);
1099 if (max > maxValue) maxValue = max;
1100 });
1101
1102 // Add some padding to max value
1103 maxValue = Math.ceil(maxValue * 1.1);
1104 if (maxValue === 0) maxValue = 10;
1105
1106 // Draw grid lines
1107 this.ctx.strokeStyle = '#e5e7eb';
1108 this.ctx.lineWidth = 1;
1109 const gridLines = 5;
1110
1111 for (let i = 0; i <= gridLines; i++) {
1112 const y = this.padding.top + (height / gridLines) * i;
1113 this.ctx.beginPath();
1114 this.ctx.moveTo(this.padding.left, y);
1115 this.ctx.lineTo(this.padding.left + width, y);
1116 this.ctx.stroke();
1117
1118 // Draw y-axis labels
1119 const value = maxValue - (maxValue / gridLines) * i;
1120 this.ctx.fillStyle = '#6b7280';
1121 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1122 this.ctx.textAlign = 'right';
1123 this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
1124 }
1125
1126 // Draw datasets
1127 this.config.datasets.forEach(dataset => {
1128 const points = [];
1129 const xStep = width / (this.config.labels.length - 1 || 1);
1130
1131 dataset.data.forEach((value, index) => {
1132 const x = this.padding.left + (xStep * index);
1133 const y = this.padding.top + height - (value / maxValue * height);
1134 points.push({ x, y, value });
1135 });
1136
1137 // Draw filled area
1138 if (dataset.fill && dataset.backgroundColor) {
1139 this.ctx.fillStyle = dataset.backgroundColor;
1140 this.ctx.beginPath();
1141 this.ctx.moveTo(points[0].x, this.padding.top + height);
1142 points.forEach(point => {
1143 this.ctx.lineTo(point.x, point.y);
1144 });
1145 this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
1146 this.ctx.closePath();
1147 this.ctx.fill();
1148 }
1149
1150 // Draw line
1151 this.ctx.strokeStyle = dataset.borderColor;
1152 this.ctx.lineWidth = 3;
1153 this.ctx.lineCap = 'round';
1154 this.ctx.lineJoin = 'round';
1155
1156 this.ctx.beginPath();
1157 points.forEach((point, index) => {
1158 if (index === 0) {
1159 this.ctx.moveTo(point.x, point.y);
1160 } else {
1161 this.ctx.lineTo(point.x, point.y);
1162 }
1163 });
1164 this.ctx.stroke();
1165
1166 // Draw points
1167 points.forEach(point => {
1168 this.ctx.fillStyle = '#ffffff';
1169 this.ctx.beginPath();
1170 this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
1171 this.ctx.fill();
1172 this.ctx.strokeStyle = dataset.borderColor;
1173 this.ctx.lineWidth = 2;
1174 this.ctx.stroke();
1175 });
1176 });
1177
1178 // Draw x-axis labels
1179 const xStep = width / (this.config.labels.length - 1 || 1);
1180 this.ctx.fillStyle = '#6b7280';
1181 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1182 this.ctx.textAlign = 'center';
1183
1184 this.config.labels.forEach((label, index) => {
1185 const x = this.padding.left + (xStep * index);
1186 this.ctx.fillText(label, x, this.padding.top + height + 20);
1187 });
1188 }
1189 }
1190
1191 // Initialize activity chart
1192 function initActivityChart() {
1193 console.log('[MxChat Chart] initActivityChart called');
1194
1195 const canvas = document.getElementById('mxchat-activity-chart');
1196 console.log('[MxChat Chart] Canvas element:', canvas);
1197
1198 if (!canvas) {
1199 console.log('[MxChat Chart] Canvas not found, aborting');
1200 return;
1201 }
1202
1203 console.log('[MxChat Chart] mxchatChartData exists:', typeof mxchatChartData !== 'undefined');
1204 if (typeof mxchatChartData === 'undefined') {
1205 console.log('[MxChat Chart] mxchatChartData is undefined, aborting');
1206 return;
1207 }
1208
1209 console.log('[MxChat Chart] Raw mxchatChartData:', mxchatChartData);
1210
1211 // Check if chart already exists and destroy it
1212 if (canvas.chartInstance) {
1213 canvas.chartInstance.destroy();
1214 }
1215
1216 const ctx = canvas.getContext('2d');
1217 console.log('[MxChat Chart] Canvas context:', ctx);
1218 console.log('[MxChat Chart] Canvas dimensions:', canvas.getBoundingClientRect());
1219
1220 // Create gradient for chats line
1221 const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
1222 chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
1223 chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
1224
1225 // Create gradient for messages line
1226 const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
1227 messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
1228 messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
1229
1230 // Convert wp_localize_script objects to arrays (WordPress converts indexed arrays to objects)
1231 const labels = Object.values(mxchatChartData.labels);
1232 const chatsData = Object.values(mxchatChartData.chats).map(Number);
1233 const messagesData = Object.values(mxchatChartData.messages).map(Number);
1234
1235 console.log('[MxChat Chart] Processed labels:', labels);
1236 console.log('[MxChat Chart] Processed chatsData:', chatsData);
1237 console.log('[MxChat Chart] Processed messagesData:', messagesData);
1238
1239 // Create chart
1240 try {
1241 canvas.chartInstance = new SimpleChart(canvas, {
1242 labels: labels,
1243 datasets: [
1244 {
1245 label: 'Chats',
1246 data: chatsData,
1247 borderColor: '#667eea',
1248 backgroundColor: chatsGradient,
1249 fill: true
1250 },
1251 {
1252 label: 'Messages',
1253 data: messagesData,
1254 borderColor: '#764ba2',
1255 backgroundColor: messagesGradient,
1256 fill: true
1257 }
1258 ]
1259 });
1260 console.log('[MxChat Chart] Chart created successfully');
1261 } catch (error) {
1262 console.error('[MxChat Chart] Error creating chart:', error);
1263 }
1264 }
1265
1266 // Initialize chart on page load (dashboard is shown by default)
1267 setTimeout(function() {
1268 initActivityChart();
1269 }, 100);
1270
1271 // Reinitialize chart on window resize
1272 let resizeTimeout;
1273 $(window).on('resize', function() {
1274 clearTimeout(resizeTimeout);
1275 resizeTimeout = setTimeout(function() {
1276 initActivityChart();
1277 }, 250);
1278 });
1279 });
1280