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

1,279 lines 49.7 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 = urlGroups.filter(g => g.usedForContext).length;
933
934 html += '<div class="mxch-rag-matches">';
935 html += '<h3>Retrieved Documents</h3>';
936 html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">' + usedUrlCount + ' entr' + (usedUrlCount === 1 ? 'y' : 'ies') + ' used for response</p>';
937
938 urlGroups.forEach(function(group) {
939 const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
940 const statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
941 const statusLabel = group.usedForContext ? 'Used' : 'Not Used';
942
943 html += '<div class="mxch-rag-match-card ' + cardClass + '">';
944 html += '<div class="mxch-rag-match-header">';
945 html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
946
947 if (group.matchedChunks.length > 1) {
948 html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
949 }
950
951 html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
952 html += '</div>';
953
954 html += '<div class="mxch-rag-match-source">';
955 if (group.isUrl) {
956 html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>';
957 } else {
958 html += escapeHtml(group.url);
959 }
960 html += '</div>';
961 html += '</div>';
962 });
963
964 html += '</div>';
965 $container.html(html);
966 }
967
968 function renderActionsContext(data, $container) {
969 let html = '';
970
971 // Check if we have action analysis data
972 if (!data.action_analysis || data.action_analysis.length === 0) {
973 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>';
974 $container.html(html);
975 return;
976 }
977
978 const actions = data.action_analysis;
979 const triggeredAction = actions.find(a => a.triggered);
980 const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
981
982 // Summary section
983 html += '<div class="mxch-rag-summary">';
984 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Actions Evaluated:</span> <span class="mxch-rag-value">' + actions.length + '</span></div>';
985 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Above Threshold:</span> <span class="mxch-rag-value">' + actionsAboveThreshold + '</span></div>';
986 if (triggeredAction) {
987 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>';
988 }
989 html += '</div>';
990
991 // Actions list
992 html += '<div class="mxch-rag-matches">';
993 html += '<h3>Action Scores</h3>';
994 html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">Showing all evaluated actions sorted by similarity score</p>';
995
996 actions.forEach(function(action) {
997 let cardClass = 'mxch-rag-match-below';
998 let statusIcon = '&#10007;';
999 let statusLabel = 'Below Threshold';
1000
1001 if (action.triggered) {
1002 cardClass = 'mxch-action-triggered';
1003 statusIcon = '&#9889;';
1004 statusLabel = 'Triggered';
1005 } else if (action.above_threshold) {
1006 cardClass = 'mxch-rag-match-used';
1007 statusIcon = '&#10003;';
1008 statusLabel = 'Above Threshold';
1009 }
1010
1011 html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1012 html += '<div class="mxch-rag-match-header">';
1013 html += '<span class="mxch-rag-match-score">' + action.similarity_percentage + '%</span>';
1014 html += '<span class="mxch-action-threshold-badge">Threshold: ' + action.threshold_percentage + '%</span>';
1015 html += '<span class="mxch-rag-match-status ' + (action.triggered ? 'status-triggered' : (action.above_threshold ? 'status-used' : 'status-below')) + '">' + statusIcon + ' ' + statusLabel + '</span>';
1016 html += '</div>';
1017
1018 html += '<div class="mxch-action-details">';
1019 html += '<div class="mxch-action-label">' + escapeHtml(action.intent_label) + '</div>';
1020 html += '<div class="mxch-action-callback"><span class="mxch-action-callback-label">Callback:</span> ' + escapeHtml(action.callback_function) + '</div>';
1021 html += '</div>';
1022
1023 // Score bar visualization
1024 const scoreBarWidth = Math.min(action.similarity_percentage, 100);
1025 const thresholdPos = Math.min(action.threshold_percentage, 100);
1026 html += '<div class="mxch-action-score-bar">';
1027 html += '<div class="mxch-action-score-fill" style="width: ' + scoreBarWidth + '%;"></div>';
1028 html += '<div class="mxch-action-threshold-marker" style="left: ' + thresholdPos + '%;"></div>';
1029 html += '</div>';
1030
1031 html += '</div>';
1032 });
1033
1034 html += '</div>';
1035 $container.html(html);
1036 }
1037
1038 function escapeHtml(text) {
1039 if (!text) return '';
1040 const div = document.createElement('div');
1041 div.textContent = text;
1042 return div.innerHTML;
1043 }
1044
1045 // Close RAG modal
1046 $('.mxch-modal-close').on('click', function() {
1047 $(this).closest('.mxch-modal-overlay').fadeOut(200);
1048 });
1049
1050 $('.mxch-modal-overlay').on('click', function(e) {
1051 if ($(e.target).is('.mxch-modal-overlay')) {
1052 $(this).fadeOut(200);
1053 }
1054 });
1055
1056 $(document).on('keydown', function(e) {
1057 if (e.key === 'Escape') {
1058 $('.mxch-modal-overlay').fadeOut(200);
1059 }
1060 });
1061
1062 // ==========================================================================
1063 // Activity Chart
1064 // ==========================================================================
1065
1066 // Simple chart implementation (no external dependencies)
1067 class SimpleChart {
1068 constructor(canvas, config) {
1069 this.canvas = canvas;
1070 this.ctx = canvas.getContext('2d');
1071 this.config = config;
1072 this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
1073 this.render();
1074 }
1075
1076 destroy() {
1077 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1078 }
1079
1080 render() {
1081 const dpr = window.devicePixelRatio || 1;
1082 const rect = this.canvas.getBoundingClientRect();
1083
1084 this.canvas.width = rect.width * dpr;
1085 this.canvas.height = rect.height * dpr;
1086 this.ctx.scale(dpr, dpr);
1087
1088 this.canvas.style.width = rect.width + 'px';
1089 this.canvas.style.height = rect.height + 'px';
1090
1091 const width = rect.width - this.padding.left - this.padding.right;
1092 const height = rect.height - this.padding.top - this.padding.bottom;
1093
1094 // Find max value
1095 let maxValue = 0;
1096 this.config.datasets.forEach(dataset => {
1097 const max = Math.max(...dataset.data);
1098 if (max > maxValue) maxValue = max;
1099 });
1100
1101 // Add some padding to max value
1102 maxValue = Math.ceil(maxValue * 1.1);
1103 if (maxValue === 0) maxValue = 10;
1104
1105 // Draw grid lines
1106 this.ctx.strokeStyle = '#e5e7eb';
1107 this.ctx.lineWidth = 1;
1108 const gridLines = 5;
1109
1110 for (let i = 0; i <= gridLines; i++) {
1111 const y = this.padding.top + (height / gridLines) * i;
1112 this.ctx.beginPath();
1113 this.ctx.moveTo(this.padding.left, y);
1114 this.ctx.lineTo(this.padding.left + width, y);
1115 this.ctx.stroke();
1116
1117 // Draw y-axis labels
1118 const value = maxValue - (maxValue / gridLines) * i;
1119 this.ctx.fillStyle = '#6b7280';
1120 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1121 this.ctx.textAlign = 'right';
1122 this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
1123 }
1124
1125 // Draw datasets
1126 this.config.datasets.forEach(dataset => {
1127 const points = [];
1128 const xStep = width / (this.config.labels.length - 1 || 1);
1129
1130 dataset.data.forEach((value, index) => {
1131 const x = this.padding.left + (xStep * index);
1132 const y = this.padding.top + height - (value / maxValue * height);
1133 points.push({ x, y, value });
1134 });
1135
1136 // Draw filled area
1137 if (dataset.fill && dataset.backgroundColor) {
1138 this.ctx.fillStyle = dataset.backgroundColor;
1139 this.ctx.beginPath();
1140 this.ctx.moveTo(points[0].x, this.padding.top + height);
1141 points.forEach(point => {
1142 this.ctx.lineTo(point.x, point.y);
1143 });
1144 this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
1145 this.ctx.closePath();
1146 this.ctx.fill();
1147 }
1148
1149 // Draw line
1150 this.ctx.strokeStyle = dataset.borderColor;
1151 this.ctx.lineWidth = 3;
1152 this.ctx.lineCap = 'round';
1153 this.ctx.lineJoin = 'round';
1154
1155 this.ctx.beginPath();
1156 points.forEach((point, index) => {
1157 if (index === 0) {
1158 this.ctx.moveTo(point.x, point.y);
1159 } else {
1160 this.ctx.lineTo(point.x, point.y);
1161 }
1162 });
1163 this.ctx.stroke();
1164
1165 // Draw points
1166 points.forEach(point => {
1167 this.ctx.fillStyle = '#ffffff';
1168 this.ctx.beginPath();
1169 this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
1170 this.ctx.fill();
1171 this.ctx.strokeStyle = dataset.borderColor;
1172 this.ctx.lineWidth = 2;
1173 this.ctx.stroke();
1174 });
1175 });
1176
1177 // Draw x-axis labels
1178 const xStep = width / (this.config.labels.length - 1 || 1);
1179 this.ctx.fillStyle = '#6b7280';
1180 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1181 this.ctx.textAlign = 'center';
1182
1183 this.config.labels.forEach((label, index) => {
1184 const x = this.padding.left + (xStep * index);
1185 this.ctx.fillText(label, x, this.padding.top + height + 20);
1186 });
1187 }
1188 }
1189
1190 // Initialize activity chart
1191 function initActivityChart() {
1192 console.log('[MxChat Chart] initActivityChart called');
1193
1194 const canvas = document.getElementById('mxchat-activity-chart');
1195 console.log('[MxChat Chart] Canvas element:', canvas);
1196
1197 if (!canvas) {
1198 console.log('[MxChat Chart] Canvas not found, aborting');
1199 return;
1200 }
1201
1202 console.log('[MxChat Chart] mxchatChartData exists:', typeof mxchatChartData !== 'undefined');
1203 if (typeof mxchatChartData === 'undefined') {
1204 console.log('[MxChat Chart] mxchatChartData is undefined, aborting');
1205 return;
1206 }
1207
1208 console.log('[MxChat Chart] Raw mxchatChartData:', mxchatChartData);
1209
1210 // Check if chart already exists and destroy it
1211 if (canvas.chartInstance) {
1212 canvas.chartInstance.destroy();
1213 }
1214
1215 const ctx = canvas.getContext('2d');
1216 console.log('[MxChat Chart] Canvas context:', ctx);
1217 console.log('[MxChat Chart] Canvas dimensions:', canvas.getBoundingClientRect());
1218
1219 // Create gradient for chats line
1220 const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
1221 chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
1222 chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
1223
1224 // Create gradient for messages line
1225 const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
1226 messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
1227 messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
1228
1229 // Convert wp_localize_script objects to arrays (WordPress converts indexed arrays to objects)
1230 const labels = Object.values(mxchatChartData.labels);
1231 const chatsData = Object.values(mxchatChartData.chats).map(Number);
1232 const messagesData = Object.values(mxchatChartData.messages).map(Number);
1233
1234 console.log('[MxChat Chart] Processed labels:', labels);
1235 console.log('[MxChat Chart] Processed chatsData:', chatsData);
1236 console.log('[MxChat Chart] Processed messagesData:', messagesData);
1237
1238 // Create chart
1239 try {
1240 canvas.chartInstance = new SimpleChart(canvas, {
1241 labels: labels,
1242 datasets: [
1243 {
1244 label: 'Chats',
1245 data: chatsData,
1246 borderColor: '#667eea',
1247 backgroundColor: chatsGradient,
1248 fill: true
1249 },
1250 {
1251 label: 'Messages',
1252 data: messagesData,
1253 borderColor: '#764ba2',
1254 backgroundColor: messagesGradient,
1255 fill: true
1256 }
1257 ]
1258 });
1259 console.log('[MxChat Chart] Chart created successfully');
1260 } catch (error) {
1261 console.error('[MxChat Chart] Error creating chart:', error);
1262 }
1263 }
1264
1265 // Initialize chart on page load (dashboard is shown by default)
1266 setTimeout(function() {
1267 initActivityChart();
1268 }, 100);
1269
1270 // Reinitialize chart on window resize
1271 let resizeTimeout;
1272 $(window).on('resize', function() {
1273 clearTimeout(resizeTimeout);
1274 resizeTimeout = setTimeout(function() {
1275 initActivityChart();
1276 }, 250);
1277 });
1278 });
1279