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

990 lines 37.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 // Show loading in conversation panel
431 $('#mxch-conversation-empty').hide();
432 $('#mxch-conversation-content').show();
433 $('#mxch-messages-area').html('<div class="mxch-messages-loading"><span class="spinner is-active"></span> Loading conversation...</div>');
434
435 $.ajax({
436 url: ajaxurl,
437 type: 'POST',
438 data: {
439 action: 'mxchat_fetch_conversation',
440 session_id: sessionId
441 },
442 success: function(response) {
443 if (response.success) {
444 renderConversation(response);
445 } else {
446 $('#mxch-messages-area').html('<div class="mxch-messages-error">Failed to load conversation</div>');
447 }
448 },
449 error: function() {
450 $('#mxch-messages-area').html('<div class="mxch-messages-error">Error loading conversation</div>');
451 }
452 });
453 }
454
455 // Render conversation content
456 function renderConversation(data) {
457 // Update header
458 $('#mxch-user-avatar span').text(data.user.initials);
459 $('#mxch-user-name').text(data.user.name);
460 $('#mxch-user-meta').text(data.user.sub);
461
462 // Update details drawer
463 $('#mxch-detail-messages').text(data.message_count);
464 $('#mxch-detail-started').text(data.started);
465
466 if (data.page.url) {
467 $('#mxch-detail-page').html(`<a href="${escapeHtml(data.page.url)}" target="_blank">${escapeHtml(data.page.title || data.page.url)}</a>`);
468 } else {
469 $('#mxch-detail-page').text('-');
470 }
471
472 if (data.user.email) {
473 $('#mxch-detail-email').text(data.user.email);
474 $('#mxch-detail-email-row').show();
475 } else {
476 $('#mxch-detail-email-row').hide();
477 }
478
479 // Clicked links
480 if (data.clicked_urls && data.clicked_urls.length > 0) {
481 let linksHtml = '';
482 data.clicked_urls.forEach(function(url) {
483 linksHtml += `<a href="${escapeHtml(url)}" target="_blank" class="mxch-clicked-link">${escapeHtml(url)}</a>`;
484 });
485 $('#mxch-clicked-links').html(linksHtml);
486 $('#mxch-clicked-section').show();
487 } else {
488 $('#mxch-clicked-section').hide();
489 }
490
491 // Render messages
492 let messagesHtml = '';
493 data.messages.forEach(function(msg) {
494 if (msg.is_user) {
495 messagesHtml += `
496 <div class="mxch-message mxch-message-user" data-message-id="${msg.id}">
497 <div class="mxch-message-row">
498 <div class="mxch-message-bubble">
499 ${msg.content}
500 </div>
501 </div>
502 <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>
503 </div>
504 `;
505 } else {
506 const ragLink = msg.has_rag ? `<a href="#" class="mxch-rag-link" data-message-id="${msg.id}">Sources</a>` : '';
507 messagesHtml += `
508 <div class="mxch-message mxch-message-bot" data-message-id="${msg.id}">
509 <div class="mxch-message-header">
510 <span class="mxch-bot-label">AI Assistant</span>
511 ${ragLink}
512 </div>
513 <div class="mxch-message-row">
514 <div class="mxch-message-bubble">
515 ${msg.content}
516 </div>
517 </div>
518 <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>
519 </div>
520 `;
521 }
522 });
523
524 $('#mxch-messages-area').html(messagesHtml);
525
526 // Scroll to bottom
527 const $area = $('#mxch-messages-area');
528 $area.scrollTop($area[0].scrollHeight);
529
530 // Attach RAG link handlers
531 $('.mxch-rag-link').on('click', function(e) {
532 e.preventDefault();
533 const messageId = $(this).data('message-id');
534 if (messageId) {
535 openRagContextModal(messageId);
536 }
537 });
538 }
539
540 // Toggle details drawer
541 $('#mxch-toggle-details').on('click', function() {
542 const $drawer = $('#mxch-details-drawer');
543 const $btn = $(this);
544
545 if ($drawer.is(':visible')) {
546 $drawer.slideUp(200);
547 $btn.removeClass('active');
548 } else {
549 $drawer.slideDown(200);
550 $btn.addClass('active');
551 }
552 });
553
554 // Delete current chat
555 $('#mxch-delete-current').on('click', function() {
556 if (!currentSessionId) return;
557
558 if (!confirm('Are you sure you want to delete this conversation? This action cannot be undone.')) {
559 return;
560 }
561
562 deleteSession(currentSessionId);
563 });
564
565 // Delete session function
566 function deleteSession(sessionId) {
567 $.ajax({
568 url: ajaxurl,
569 type: 'POST',
570 data: {
571 action: 'mxchat_delete_chat_history',
572 delete_session_ids: [sessionId],
573 security: $('#mxchat_delete_chat_nonce').val()
574 },
575 success: function(response) {
576 try {
577 const jsonResponse = typeof response === 'object' ? response : JSON.parse(response);
578
579 if (jsonResponse.success) {
580 // Reset conversation panel
581 currentSessionId = null;
582 $('#mxch-conversation-content').hide();
583 $('#mxch-conversation-empty').show();
584 $('#mxch-details-drawer').hide();
585
586 // Reload list
587 loadChatList(currentPage, $('#mxch-search-transcripts').val());
588 } else if (jsonResponse.error) {
589 alert('Error: ' + jsonResponse.error);
590 }
591 } catch (e) {
592 alert('An error occurred while processing the response.');
593 }
594 },
595 error: function() {
596 alert('An error occurred while deleting the conversation.');
597 }
598 });
599 }
600
601 // ==========================================================================
602 // Export Functionality
603 // ==========================================================================
604
605 $('#mxch-export-btn, #mxch-export-current').on('click', function() {
606 const $button = $(this);
607 $button.prop('disabled', true).addClass('loading');
608
609 const $form = $('<form>', {
610 method: 'post',
611 action: ajaxurl
612 });
613
614 $form.append($('<input>', {
615 type: 'hidden',
616 name: 'action',
617 value: 'mxchat_export_transcripts'
618 }));
619
620 $form.append($('<input>', {
621 type: 'hidden',
622 name: 'security',
623 value: mxchatAdmin.export_nonce
624 }));
625
626 $form.appendTo('body').submit();
627
628 setTimeout(function() {
629 $button.prop('disabled', false).removeClass('loading');
630 }, 2000);
631 });
632
633 // ==========================================================================
634 // RAG Context Modal
635 // ==========================================================================
636
637 function openRagContextModal(messageId) {
638 const $modal = $('#mxch-rag-modal');
639 const $loading = $modal.find('.mxch-rag-loading');
640 const $content = $modal.find('.mxch-rag-content');
641
642 $modal.fadeIn(200);
643 $loading.show();
644 $content.html('');
645
646 $.ajax({
647 url: ajaxurl,
648 type: 'POST',
649 data: {
650 action: 'mxchat_get_rag_context',
651 message_id: messageId
652 },
653 success: function(response) {
654 $loading.hide();
655
656 if (response.success && response.data) {
657 renderRagContext(response.data, $content);
658 } else {
659 $content.html('<div class="mxch-rag-error">Unable to load document context.</div>');
660 }
661 },
662 error: function() {
663 $loading.hide();
664 $content.html('<div class="mxch-rag-error">Error loading document context. Please try again.</div>');
665 }
666 });
667 }
668
669 function renderRagContext(data, $container) {
670 let html = '';
671
672 html += '<div class="mxch-rag-summary">';
673 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>';
674 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>';
675 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>';
676 html += '</div>';
677
678 if (data.top_matches && data.top_matches.length > 0) {
679 const groupedByUrl = {};
680
681 data.top_matches.forEach(function(match) {
682 const url = match.source_display || 'Unknown';
683 if (!groupedByUrl[url]) {
684 groupedByUrl[url] = {
685 url: url,
686 isUrl: url.startsWith('http'),
687 bestScore: 0,
688 usedForContext: false,
689 matchedChunks: []
690 };
691 }
692
693 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
694 groupedByUrl[url].bestScore = match.similarity_percentage;
695 }
696
697 if (match.used_for_context) {
698 groupedByUrl[url].usedForContext = true;
699 }
700
701 groupedByUrl[url].matchedChunks.push({
702 chunkIndex: match.chunk_index,
703 score: match.similarity_percentage,
704 usedForContext: match.used_for_context
705 });
706 });
707
708 const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore);
709 const usedUrlCount = urlGroups.filter(g => g.usedForContext).length;
710
711 html += '<div class="mxch-rag-matches">';
712 html += '<h3>Retrieved Documents</h3>';
713 html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">' + usedUrlCount + ' entr' + (usedUrlCount === 1 ? 'y' : 'ies') + ' used for response</p>';
714
715 urlGroups.forEach(function(group) {
716 const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
717 const statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
718 const statusLabel = group.usedForContext ? 'Used' : 'Not Used';
719
720 html += '<div class="mxch-rag-match-card ' + cardClass + '">';
721 html += '<div class="mxch-rag-match-header">';
722 html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
723
724 if (group.matchedChunks.length > 1) {
725 html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
726 }
727
728 html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
729 html += '</div>';
730
731 html += '<div class="mxch-rag-match-source">';
732 if (group.isUrl) {
733 html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>';
734 } else {
735 html += escapeHtml(group.url);
736 }
737 html += '</div>';
738 html += '</div>';
739 });
740
741 html += '</div>';
742 } else {
743 html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>';
744 }
745
746 $container.html(html);
747 }
748
749 function escapeHtml(text) {
750 if (!text) return '';
751 const div = document.createElement('div');
752 div.textContent = text;
753 return div.innerHTML;
754 }
755
756 // Close RAG modal
757 $('.mxch-modal-close').on('click', function() {
758 $(this).closest('.mxch-modal-overlay').fadeOut(200);
759 });
760
761 $('.mxch-modal-overlay').on('click', function(e) {
762 if ($(e.target).is('.mxch-modal-overlay')) {
763 $(this).fadeOut(200);
764 }
765 });
766
767 $(document).on('keydown', function(e) {
768 if (e.key === 'Escape') {
769 $('.mxch-modal-overlay').fadeOut(200);
770 }
771 });
772
773 // ==========================================================================
774 // Activity Chart
775 // ==========================================================================
776
777 // Simple chart implementation (no external dependencies)
778 class SimpleChart {
779 constructor(canvas, config) {
780 this.canvas = canvas;
781 this.ctx = canvas.getContext('2d');
782 this.config = config;
783 this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
784 this.render();
785 }
786
787 destroy() {
788 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
789 }
790
791 render() {
792 const dpr = window.devicePixelRatio || 1;
793 const rect = this.canvas.getBoundingClientRect();
794
795 this.canvas.width = rect.width * dpr;
796 this.canvas.height = rect.height * dpr;
797 this.ctx.scale(dpr, dpr);
798
799 this.canvas.style.width = rect.width + 'px';
800 this.canvas.style.height = rect.height + 'px';
801
802 const width = rect.width - this.padding.left - this.padding.right;
803 const height = rect.height - this.padding.top - this.padding.bottom;
804
805 // Find max value
806 let maxValue = 0;
807 this.config.datasets.forEach(dataset => {
808 const max = Math.max(...dataset.data);
809 if (max > maxValue) maxValue = max;
810 });
811
812 // Add some padding to max value
813 maxValue = Math.ceil(maxValue * 1.1);
814 if (maxValue === 0) maxValue = 10;
815
816 // Draw grid lines
817 this.ctx.strokeStyle = '#e5e7eb';
818 this.ctx.lineWidth = 1;
819 const gridLines = 5;
820
821 for (let i = 0; i <= gridLines; i++) {
822 const y = this.padding.top + (height / gridLines) * i;
823 this.ctx.beginPath();
824 this.ctx.moveTo(this.padding.left, y);
825 this.ctx.lineTo(this.padding.left + width, y);
826 this.ctx.stroke();
827
828 // Draw y-axis labels
829 const value = maxValue - (maxValue / gridLines) * i;
830 this.ctx.fillStyle = '#6b7280';
831 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
832 this.ctx.textAlign = 'right';
833 this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
834 }
835
836 // Draw datasets
837 this.config.datasets.forEach(dataset => {
838 const points = [];
839 const xStep = width / (this.config.labels.length - 1 || 1);
840
841 dataset.data.forEach((value, index) => {
842 const x = this.padding.left + (xStep * index);
843 const y = this.padding.top + height - (value / maxValue * height);
844 points.push({ x, y, value });
845 });
846
847 // Draw filled area
848 if (dataset.fill && dataset.backgroundColor) {
849 this.ctx.fillStyle = dataset.backgroundColor;
850 this.ctx.beginPath();
851 this.ctx.moveTo(points[0].x, this.padding.top + height);
852 points.forEach(point => {
853 this.ctx.lineTo(point.x, point.y);
854 });
855 this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
856 this.ctx.closePath();
857 this.ctx.fill();
858 }
859
860 // Draw line
861 this.ctx.strokeStyle = dataset.borderColor;
862 this.ctx.lineWidth = 3;
863 this.ctx.lineCap = 'round';
864 this.ctx.lineJoin = 'round';
865
866 this.ctx.beginPath();
867 points.forEach((point, index) => {
868 if (index === 0) {
869 this.ctx.moveTo(point.x, point.y);
870 } else {
871 this.ctx.lineTo(point.x, point.y);
872 }
873 });
874 this.ctx.stroke();
875
876 // Draw points
877 points.forEach(point => {
878 this.ctx.fillStyle = '#ffffff';
879 this.ctx.beginPath();
880 this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
881 this.ctx.fill();
882 this.ctx.strokeStyle = dataset.borderColor;
883 this.ctx.lineWidth = 2;
884 this.ctx.stroke();
885 });
886 });
887
888 // Draw x-axis labels
889 const xStep = width / (this.config.labels.length - 1 || 1);
890 this.ctx.fillStyle = '#6b7280';
891 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
892 this.ctx.textAlign = 'center';
893
894 this.config.labels.forEach((label, index) => {
895 const x = this.padding.left + (xStep * index);
896 this.ctx.fillText(label, x, this.padding.top + height + 20);
897 });
898 }
899 }
900
901 // Initialize activity chart
902 function initActivityChart() {
903 console.log('[MxChat Chart] initActivityChart called');
904
905 const canvas = document.getElementById('mxchat-activity-chart');
906 console.log('[MxChat Chart] Canvas element:', canvas);
907
908 if (!canvas) {
909 console.log('[MxChat Chart] Canvas not found, aborting');
910 return;
911 }
912
913 console.log('[MxChat Chart] mxchatChartData exists:', typeof mxchatChartData !== 'undefined');
914 if (typeof mxchatChartData === 'undefined') {
915 console.log('[MxChat Chart] mxchatChartData is undefined, aborting');
916 return;
917 }
918
919 console.log('[MxChat Chart] Raw mxchatChartData:', mxchatChartData);
920
921 // Check if chart already exists and destroy it
922 if (canvas.chartInstance) {
923 canvas.chartInstance.destroy();
924 }
925
926 const ctx = canvas.getContext('2d');
927 console.log('[MxChat Chart] Canvas context:', ctx);
928 console.log('[MxChat Chart] Canvas dimensions:', canvas.getBoundingClientRect());
929
930 // Create gradient for chats line
931 const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
932 chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
933 chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
934
935 // Create gradient for messages line
936 const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
937 messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
938 messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
939
940 // Convert wp_localize_script objects to arrays (WordPress converts indexed arrays to objects)
941 const labels = Object.values(mxchatChartData.labels);
942 const chatsData = Object.values(mxchatChartData.chats).map(Number);
943 const messagesData = Object.values(mxchatChartData.messages).map(Number);
944
945 console.log('[MxChat Chart] Processed labels:', labels);
946 console.log('[MxChat Chart] Processed chatsData:', chatsData);
947 console.log('[MxChat Chart] Processed messagesData:', messagesData);
948
949 // Create chart
950 try {
951 canvas.chartInstance = new SimpleChart(canvas, {
952 labels: labels,
953 datasets: [
954 {
955 label: 'Chats',
956 data: chatsData,
957 borderColor: '#667eea',
958 backgroundColor: chatsGradient,
959 fill: true
960 },
961 {
962 label: 'Messages',
963 data: messagesData,
964 borderColor: '#764ba2',
965 backgroundColor: messagesGradient,
966 fill: true
967 }
968 ]
969 });
970 console.log('[MxChat Chart] Chart created successfully');
971 } catch (error) {
972 console.error('[MxChat Chart] Error creating chart:', error);
973 }
974 }
975
976 // Initialize chart on page load (dashboard is shown by default)
977 setTimeout(function() {
978 initActivityChart();
979 }, 100);
980
981 // Reinitialize chart on window resize
982 let resizeTimeout;
983 $(window).on('resize', function() {
984 clearTimeout(resizeTimeout);
985 resizeTimeout = setTimeout(function() {
986 initActivityChart();
987 }, 250);
988 });
989 });
990