PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.6.3
MxChat – AI Chatbot & Content Generation for WordPress v2.6.3
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/mxchat_transcripts.js +372 -1127 3.2.12.6.3 View file →
@@ -1,835 +1,306 @@
1 -/**
2 - * MxChat Transcripts Page JavaScript - v3.0
3 - * Split-panel layout with chat list and conversation view
4 - */
5 1 jQuery(document).ready(function($) {
6 - // ==========================================================================
7 - // Sidebar Navigation
8 - // ==========================================================================
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 -
2 + // Current page state
140 3 let currentPage = 1;
141 - const perPage = 50;
4 + const perPage = 50; // Display 50 sessions per page
142 5 let totalPages = 1;
143 - let currentSessionId = null;
144 - let transcriptsLoaded = false;
6 +
7 + // Track selected sessions for bulk delete
145 8 let selectedSessions = new Set();
146 - let currentSortOrder = 'desc'; // newest first
147 9
148 - // Load chat list on page load
149 - loadChatList(1, '');
10 + // Select/Deselect All functionality
11 + const selectButton = $('#mxchat-select-all-transcripts');
12 + let isSelected = false;
150 13
151 - // Search functionality with debounce
152 - let searchTimeout;
153 - $('#mxch-search-transcripts').on('input', function() {
154 - clearTimeout(searchTimeout);
155 - const searchTerm = $(this).val().toLowerCase();
14 + selectButton.click(function() {
15 + isSelected = !isSelected;
16 + $(this).toggleClass('selected');
17 +
18 + // Update button text
19 + const buttonText = $(this).find('.button-text');
20 + buttonText.text(isSelected ? 'Deselect All' : 'Select All');
21 +
22 + // Update session selection (for current page)
23 + $('.mxchat-session-header').each(function() {
24 + const sessionId = $(this).data('session-id');
25 + const sessionContainer = $(this).closest('.mxchat-session');
26 + if (isSelected) {
27 + selectedSessions.add(sessionId);
28 + sessionContainer.addClass('selected');
29 + } else {
30 + selectedSessions.delete(sessionId);
31 + sessionContainer.removeClass('selected');
32 + }
33 + });
34 +
35 + updateDeleteButtonState();
36 + });
156 37
157 - searchTimeout = setTimeout(function() {
38 + // Search functionality
39 + $('#mxchat-search-transcripts').on('input', function() {
40 + var searchTerm = $(this).val().toLowerCase();
41 +
42 + if (searchTerm.length > 0) {
43 + // Reset to first page when searching
158 44 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');
45 +
46 + // Load with search filter
47 + loadTranscripts(currentPage, searchTerm);
186 48 } else {
187 - selectedSessions.clear();
188 - $('.mxch-chat-item').removeClass('selected');
189 - $('#mxch-chat-list').removeClass('selection-mode');
49 + // Reset to first page with no search term
50 + currentPage = 1;
51 + loadTranscripts(currentPage, '');
190 52 }
191 -
192 - updateSelectionUI();
193 53 });
194 54
195 - // Update selection UI
196 - function updateSelectionUI() {
197 - const count = selectedSessions.size;
198 - const $countEl = $('#mxch-selected-count');
199 - const $deleteBtn = $('#mxch-delete-selected');
55 + // Initial load of transcripts
56 + loadTranscripts(currentPage, '');
200 57
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) {
58 + // Function to load transcripts with pagination
59 + function loadTranscripts(page, searchTerm = '') {
60 + $('#mxchat-transcripts').html('<div class="mxchat-loading">Loading transcripts...</div>');
61 +
239 62 $.ajax({
240 63 url: ajaxurl,
241 64 type: 'POST',
242 65 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 66 action: 'mxchat_fetch_chat_history',
290 67 page: page,
291 68 per_page: perPage,
292 - search: searchTerm,
293 - sort_order: currentSortOrder
69 + search: searchTerm
294 70 },
295 71 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 - }
72 + $('#mxchat-transcripts').html(response.html);
73 + currentPage = response.page;
74 + totalPages = response.total_pages;
75 +
76 + // Reset selection state when page changes (but keep selectedSessions for bulk operations)
77 + isSelected = false;
78 + selectButton.removeClass('selected');
79 + selectButton.find('.button-text').text('Select All');
80 +
81 + // Restore selection state for sessions on this page
82 + $('.mxchat-session-header').each(function() {
83 + const sessionId = $(this).data('session-id');
84 + const sessionContainer = $(this).closest('.mxchat-session');
85 + if (selectedSessions.has(sessionId)) {
86 + sessionContainer.addClass('selected');
87 + }
88 + });
89 +
90 + updateDeleteButtonState();
91 +
92 + // Add click handlers to pagination buttons
93 + $('.mxchat-pagination-button').on('click', function() {
94 + var pageNum = $(this).data('page');
95 + loadTranscripts(pageNum, searchTerm);
96 +
97 + // Scroll to top of transcripts
98 + $('html, body').animate({
99 + scrollTop: $('#mxchat-transcripts').offset().top - 50
100 + }, 300);
101 + });
102 +
103 + // Re-attach event handlers for newly loaded content
104 + attachDynamicEventHandlers();
309 105 },
310 - error: function() {
311 - $container.html('<div class="mxch-list-empty"><p>Error loading chats</p></div>');
106 + error: function(xhr, status, error) {
107 + $('#mxchat-transcripts').html('<div class="mxchat-error">Error loading chat transcripts. Please try again.</div>');
108 + console.error("AJAX Error: " + status + " - " + error);
312 109 }
313 110 });
314 111 }
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 - `;
112 +
113 + // Attach event handlers to dynamically loaded content
114 + function attachDynamicEventHandlers() {
115 + // Handle individual delete button clicks
116 + $('.mxchat-delete-btn').off('click').on('click', function(e) {
117 + e.preventDefault();
118 + e.stopPropagation();
119 +
120 + const sessionId = $(this).data('session-id');
121 +
122 + if (!confirm("Are you sure you want to delete this chat session? This action cannot be undone.")) {
123 + return;
124 + }
125 +
126 + // Delete single session
127 + deleteSessions([sessionId]);
341 128 });
129 +
130 + // Handle session header clicks for selection (bulk delete)
131 + $('.mxchat-session-header').off('click').on('click', function(e) {
132 + // Don't trigger if clicking the delete button
133 + if ($(e.target).hasClass('mxchat-delete-btn') || $(e.target).closest('.mxchat-delete-btn').length) {
134 + return;
135 + }
342 136
343 - $container.html(html);
137 + const sessionId = $(this).data('session-id');
138 + const sessionContainer = $(this).closest('.mxchat-session');
344 139
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')) {
140 + if (selectedSessions.has(sessionId)) {
141 + selectedSessions.delete(sessionId);
142 + sessionContainer.removeClass('selected');
143 + } else {
352 144 selectedSessions.add(sessionId);
353 - $item.addClass('selected');
354 - } else {
355 - selectedSessions.delete(sessionId);
356 - $item.removeClass('selected');
145 + sessionContainer.addClass('selected');
357 146 }
358 147
359 - updateSelectionUI();
148 + updateDeleteButtonState();
360 149 });
361 150
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;
151 + // Handle clicks on Sources link for RAG context
152 + $('.mxchat-rag-link').off('click').on('click', function(e) {
153 + e.preventDefault();
154 + e.stopPropagation();
366 155
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();
156 + const messageId = $(this).closest('.mxchat-message').data('message-id');
157 + if (messageId) {
158 + openRagContextModal(messageId);
159 + }
376 160 });
377 -
378 - // Update selection UI after render
379 - updateSelectionUI();
380 161 }
381 -
382 - // Update chat count display
383 - function updateChatCount(start, end, total) {
384 - if (total === 0) {
385 - $('#mxch-chat-count').text('0 chats');
162 +
163 + // Update delete button state based on selections
164 + function updateDeleteButtonState() {
165 + const deleteButton = $('.delete-chats-button');
166 + if (selectedSessions.size > 0) {
167 + deleteButton.prop('disabled', false);
386 168 } else {
387 - $('#mxch-chat-count').text(`${start}-${end} / ${total} chats`);
169 + deleteButton.prop('disabled', true);
388 170 }
389 171 }
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 -
172 +
173 + // Function to delete sessions
174 + function deleteSessions(sessionIds) {
440 175 $.ajax({
441 176 url: ajaxurl,
442 177 type: 'POST',
443 178 data: {
444 - action: 'mxchat_fetch_conversation',
445 - session_id: sessionId
179 + action: 'mxchat_delete_chat_history',
180 + delete_session_ids: sessionIds,
181 + security: $('#mxchat_delete_chat_nonce').val()
446 182 },
447 183 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 - }
184 + var jsonResponse = JSON.parse(response);
185 + if (jsonResponse.success) {
186 + alert("Success: " + jsonResponse.success);
187 +
188 + // Remove deleted sessions from selectedSessions
189 + sessionIds.forEach(id => selectedSessions.delete(id));
190 +
191 + } else if (jsonResponse.error) {
192 + alert("Error: " + jsonResponse.error);
456 193 } else {
457 - $('#mxch-messages-area').html('<div class="mxch-messages-error">Failed to load conversation</div>');
194 + //console.log("Unexpected response format.");
458 195 }
196 +
197 + // Reload the current page of transcripts
198 + loadTranscripts(currentPage);
459 199 },
460 - error: function() {
461 - $('#mxch-messages-area').html('<div class="mxch-messages-error">Error loading conversation</div>');
200 + error: function(xhr, status, error) {
201 + //console.error("AJAX Error: " + status + " - " + error);
202 + //console.log(xhr.responseText);
203 + alert("An error occurred while deleting chat sessions. Please try again.");
462 204 }
463 205 });
464 206 }
465 207
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('-');
208 + // Delete form submission (bulk delete)
209 + $('#mxchat-delete-form').submit(function(e) {
210 + e.preventDefault();
211 +
212 + if (selectedSessions.size === 0) {
213 + alert("Please select at least one chat session to delete.");
214 + return;
481 215 }
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.')) {
216 +
217 + // Confirm deletion
218 + if (!confirm(`Are you sure you want to delete the selected ${selectedSessions.size} chat session(s)? This action cannot be undone.`)) {
570 219 return;
571 220 }
572 -
573 - deleteSession(currentSessionId);
221 +
222 + // Convert Set to Array and delete
223 + deleteSessions(Array.from(selectedSessions));
574 224 });
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);
225 +
226 + // Export functionality - this remains unchanged as it should export all transcripts
227 + $('#mxchat-export-transcripts').on('click', function() {
228 + var $button = $(this);
618 229 $button.prop('disabled', true).addClass('loading');
619 230
620 - const $form = $('<form>', {
621 - method: 'post',
622 - action: ajaxurl
231 + // Create a form and submit it
232 + var $form = $('<form>', {
233 + 'method': 'post',
234 + 'action': ajaxurl
623 235 });
624 236
625 237 $form.append($('<input>', {
626 - type: 'hidden',
627 - name: 'action',
628 - value: 'mxchat_export_transcripts'
238 + 'type': 'hidden',
239 + 'name': 'action',
240 + 'value': 'mxchat_export_transcripts'
629 241 }));
630 242
631 243 $form.append($('<input>', {
632 - type: 'hidden',
633 - name: 'security',
634 - value: mxchatAdmin.export_nonce
244 + 'type': 'hidden',
245 + 'name': 'security',
246 + 'value': mxchatAdmin.export_nonce
635 247 }));
636 248
637 249 $form.appendTo('body').submit();
638 250
251 + // Re-enable the button after a short delay
639 252 setTimeout(function() {
640 253 $button.prop('disabled', false).removeClass('loading');
641 254 }, 2000);
642 255 });
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());
256 +
257 + // Chat Email Notification Modal functionality
258 +
259 + // Open modal
260 + $('#mxchat-chat-email-notification-btn').on('click', function(e) {
261 + e.preventDefault();
262 + $('#mxchat-chat-email-notification-modal').fadeIn(300);
662 263 });
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 - });
264 +
265 + // Close modal
266 + $('.mxchat-chat-notification-modal-close, .mxchat-chat-notification-modal-cancel').on('click', function() {
267 + $('#mxchat-chat-email-notification-modal').fadeOut(300);
268 + });
269 +
270 + // Close modal on outside click
271 + $('#mxchat-chat-email-notification-modal').on('click', function(e) {
272 + if ($(e.target).is('#mxchat-chat-email-notification-modal')) {
273 + $(this).fadeOut(300);
672 274 }
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 275 });
276 +
277 + // Handle form submission - Let WordPress handle it normally for settings
278 + $('#mxchat-chat-email-notification-form').on('submit', function(e) {
279 + // Don't prevent default - let the form submit normally to WordPress options.php
280 + var $submitButton = $(this).find('button[type="submit"]');
281 + var originalText = $submitButton.text();
779 282
780 - // Show original button click handler
781 - $('#mxch-show-original-btn').on('click', function() {
782 - if (!originalMessages) return;
283 + // Just show a loading state
284 + $submitButton.text('Saving...').prop('disabled', true);
783 285
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();
286 + // The form will submit normally and reload the page
794 287 });
795 288
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 - }
289 + // ========== RAG Context Modal Functions ==========
803 290
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 -
291 + // Open RAG context modal and fetch data
812 292 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');
293 + const $modal = $('#mxchat-rag-context-modal');
294 + const $loading = $modal.find('.mxchat-rag-loading');
295 + const $content = $modal.find('.mxchat-rag-content');
817 296
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);
297 + // Show modal with loading state
298 + $modal.fadeIn(300);
828 299 $loading.show();
829 - $sourcesContent.html('');
830 - $actionsContent.html('');
300 + $content.html('');
831 301
302 + // Fetch RAG context via AJAX
832 303 $.ajax({
833 304 url: ajaxurl,
834 305 type: 'POST',
835 306 data: {
@@ -837,206 +308,195 @@
837 308 message_id: messageId
838 309 },
839 310 success: function(response) {
840 311 $loading.hide();
841 -
842 312 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 - }
313 + renderRagContext(response.data, $content);
859 314 } 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>');
315 + $content.html('<div class="mxchat-rag-error">Unable to load document context.</div>');
862 316 }
863 317 },
864 318 error: function() {
865 319 $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>');
320 + $content.html('<div class="mxchat-rag-error">Error loading document context. Please try again.</div>');
868 321 }
869 322 });
870 323 }
871 324
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 -
325 + // Render RAG context data in the modal - grouped by URL
886 326 function renderRagContext(data, $container) {
887 327 let html = '';
888 328
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>';
329 + // Summary section
330 + html += '<div class="mxchat-rag-summary">';
331 + html += '<div class="mxchat-rag-summary-item">';
332 + html += '<span class="mxchat-rag-label">Knowledge Base:</span> ';
333 + html += '<span class="mxchat-rag-value">' + escapeHtml(data.knowledge_base_type || 'WordPress Database') + '</span>';
900 334 html += '</div>';
335 + html += '<div class="mxchat-rag-summary-item">';
336 + html += '<span class="mxchat-rag-label">Similarity Threshold:</span> ';
337 + html += '<span class="mxchat-rag-value">' + Math.round((data.similarity_threshold || 0.35) * 100) + '%</span>';
338 + html += '</div>';
339 + html += '<div class="mxchat-rag-summary-item">';
340 + html += '<span class="mxchat-rag-label">Documents Checked:</span> ';
341 + html += '<span class="mxchat-rag-value">' + (data.total_documents_checked || 0) + '</span>';
342 + html += '</div>';
343 + html += '</div>';
901 344
902 - const groupedByUrl = {};
345 + // Top matches section - grouped by URL
346 + if (data.top_matches && data.top_matches.length > 0) {
347 + // Group matches by source URL
348 + const groupedByUrl = {};
349 + data.top_matches.forEach(function(match) {
350 + const url = match.source_display || 'Unknown';
351 + if (!groupedByUrl[url]) {
352 + groupedByUrl[url] = {
353 + url: url,
354 + isUrl: url.startsWith('http'),
355 + bestScore: 0,
356 + usedForContext: false,
357 + totalChunks: match.total_chunks || 1,
358 + matchedChunks: [],
359 + isChunked: match.is_chunk || false,
360 + roleRestriction: match.role_restriction
361 + };
362 + }
903 363
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 - }
364 + // Track best score
365 + if (match.similarity_percentage > groupedByUrl[url].bestScore) {
366 + groupedByUrl[url].bestScore = match.similarity_percentage;
367 + }
915 368
916 - if (match.similarity_percentage > groupedByUrl[url].bestScore) {
917 - groupedByUrl[url].bestScore = match.similarity_percentage;
918 - }
369 + // Track if any chunk was used
370 + if (match.used_for_context) {
371 + groupedByUrl[url].usedForContext = true;
372 + }
919 373
920 - if (match.used_for_context) {
921 - groupedByUrl[url].usedForContext = true;
922 - }
374 + // Add chunk info
375 + groupedByUrl[url].matchedChunks.push({
376 + chunkIndex: match.chunk_index,
377 + score: match.similarity_percentage,
378 + usedForContext: match.used_for_context,
379 + aboveThreshold: match.above_threshold,
380 + contentPreview: match.content_preview
381 + });
382 + });
923 383
924 - groupedByUrl[url].matchedChunks.push({
925 - chunkIndex: match.chunk_index,
926 - score: match.similarity_percentage,
927 - usedForContext: match.used_for_context
384 + // Convert to array and sort by best score
385 + const urlGroups = Object.values(groupedByUrl).sort(function(a, b) {
386 + return b.bestScore - a.bestScore;
928 387 });
929 - });
930 388
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' : '';
389 + // Count unique URLs used
390 + const usedUrlCount = urlGroups.filter(function(g) { return g.usedForContext; }).length;
934 391
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>';
392 + html += '<div class="mxchat-rag-matches">';
393 + html += '<h3>Retrieved Documents</h3>';
394 + html += '<p class="mxchat-rag-matches-summary">' + usedUrlCount + ' entr' + (usedUrlCount === 1 ? 'y' : 'ies') + ' used for response';
395 + html += ' <span style="color: #64748b; font-size: 12px;">(from ' + data.top_matches.length + ' chunk matches)</span></p>';
938 396
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';
397 + urlGroups.forEach(function(group, groupIndex) {
398 + const cardClass = group.usedForContext ? 'mxchat-rag-match-used' : 'mxchat-rag-match-below';
399 + const statusIcon = group.usedForContext ? '✅' : '❌';
400 + const statusLabel = group.usedForContext ? 'Used for Response' : 'Not Used';
943 401
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>';
402 + // Build chunk summary badge
403 + let chunkBadge = '';
404 + if (group.isChunked && group.totalChunks > 1) {
405 + const usedChunkCount = group.matchedChunks.filter(function(c) { return c.usedForContext; }).length;
406 + chunkBadge = '<span class="mxchat-rag-chunk-badge">' + usedChunkCount + '/' + group.totalChunks + ' chunks</span>';
407 + }
947 408
948 - if (group.matchedChunks.length > 1) {
949 - html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
950 - }
409 + html += '<div class="mxchat-rag-match-card ' + cardClass + '">';
410 + html += '<div class="mxchat-rag-match-header">';
411 + html += '<span class="mxchat-rag-match-score">' + group.bestScore + '%</span>';
412 + html += chunkBadge;
413 + html += '<span class="mxchat-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">';
414 + html += statusIcon + ' ' + statusLabel;
415 + html += '</span>';
416 + html += '</div>';
951 417
952 - html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
953 - html += '</div>';
418 + html += '<div class="mxchat-rag-match-source">';
419 + if (group.isUrl) {
420 + html += '<a href="' + escapeHtml(group.url) + '" target="_blank" rel="noopener noreferrer">';
421 + html += '🔗 ' + escapeHtml(group.url);
422 + html += '</a>';
423 + } else {
424 + html += '📄 ' + escapeHtml(group.url);
425 + }
426 + html += '</div>';
954 427
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 - });
428 + // Show role restriction if not public
429 + if (group.roleRestriction && group.roleRestriction !== 'public') {
430 + html += '<div class="mxchat-rag-match-meta">';
431 + html += '<span class="mxchat-rag-role-badge">🔒 ' + escapeHtml(group.roleRestriction) + '</span>';
432 + html += '</div>';
433 + }
964 434
965 - html += '</div>';
966 - $container.html(html);
967 - }
435 + // Expandable chunk details if multiple chunks
436 + if (group.matchedChunks.length > 1) {
437 + html += '<div class="mxchat-rag-chunk-toggle" data-group="' + groupIndex + '">▶ Show ' + group.matchedChunks.length + ' matched chunks</div>';
438 + html += '<div class="mxchat-rag-chunk-details" data-group="' + groupIndex + '">';
968 439
969 - function renderActionsContext(data, $container) {
970 - let html = '';
440 + // Sort chunks by index
441 + const sortedChunks = group.matchedChunks.slice().sort(function(a, b) {
442 + return (a.chunkIndex || 0) - (b.chunkIndex || 0);
443 + });
971 444
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 - }
445 + sortedChunks.forEach(function(chunk) {
446 + const chunkNum = (chunk.chunkIndex !== null && chunk.chunkIndex !== undefined) ? chunk.chunkIndex + 1 : '?';
447 + const chunkClass = chunk.usedForContext ? 'chunk-used' : 'chunk-not-used';
448 + const chunkIcon = chunk.usedForContext ? '✓' : '○';
978 449
979 - const actions = data.action_analysis;
980 - const triggeredAction = actions.find(a => a.triggered);
981 - const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
450 + html += '<div class="mxchat-rag-chunk-row ' + chunkClass + '">';
451 + html += '<span class="mxchat-rag-chunk-icon">' + chunkIcon + '</span>';
452 + html += '<span class="mxchat-rag-chunk-num">Chunk ' + chunkNum + '</span>';
453 + html += '<span class="mxchat-rag-chunk-score">' + chunk.score + '%</span>';
454 + html += '</div>';
455 + });
982 456
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>';
457 + html += '</div>';
458 + }
991 459
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>';
460 + html += '</div>';
461 + });
996 462
997 - actions.forEach(function(action) {
998 - let cardClass = 'mxch-rag-match-below';
999 - let statusIcon = '&#10007;';
1000 - let statusLabel = 'Below Threshold';
463 + html += '</div>';
464 + } else {
465 + html += '<div class="mxchat-rag-no-matches">No document matches found for this response.</div>';
466 + }
1001 467
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>';
468 + // Approved URLs section
469 + if (data.approved_urls && data.approved_urls.length > 0) {
470 + html += '<div class="mxchat-rag-urls">';
471 + html += '<h3>Approved URLs for Citations (' + data.approved_urls.length + ')</h3>';
472 + html += '<ul class="mxchat-rag-url-list">';
473 + data.approved_urls.forEach(function(url) {
474 + html += '<li><a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(url) + '</a></li>';
475 + });
476 + html += '</ul>';
1017 477 html += '</div>';
478 + }
1018 479
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>';
480 + $container.html(html);
1023 481
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>';
482 + // Add click handlers for chunk toggles
483 + $container.find('.mxchat-rag-chunk-toggle').on('click', function() {
484 + const groupId = $(this).data('group');
485 + const $details = $container.find('.mxchat-rag-chunk-details[data-group="' + groupId + '"]');
486 + const isExpanded = $details.is(':visible');
1031 487
1032 - html += '</div>';
488 + if (isExpanded) {
489 + $details.slideUp(200);
490 + $(this).text('▶ Show ' + $details.find('.mxchat-rag-chunk-row').length + ' matched chunks');
491 + } else {
492 + $details.slideDown(200);
493 + $(this).text('▼ Hide chunks');
494 + }
1033 495 });
1034 -
1035 - html += '</div>';
1036 - $container.html(html);
1037 496 }
1038 497
498 + // Helper function to escape HTML
1039 499 function escapeHtml(text) {
1040 500 if (!text) return '';
1041 501 const div = document.createElement('div');
1042 502 div.textContent = text;
@@ -1042,238 +502,23 @@
1042 502 div.textContent = text;
1043 503 return div.innerHTML;
1044 504 }
1045 505
1046 - // Close RAG modal
1047 - $('.mxch-modal-close').on('click', function() {
1048 - $(this).closest('.mxch-modal-overlay').fadeOut(200);
506 + // Close RAG context modal
507 + $('.mxchat-rag-modal-close').on('click', function() {
508 + $('#mxchat-rag-context-modal').fadeOut(300);
1049 509 });
1050 510
1051 - $('.mxch-modal-overlay').on('click', function(e) {
1052 - if ($(e.target).is('.mxch-modal-overlay')) {
1053 - $(this).fadeOut(200);
511 + // Close modal on outside click
512 + $('#mxchat-rag-context-modal').on('click', function(e) {
513 + if ($(e.target).is('#mxchat-rag-context-modal')) {
514 + $(this).fadeOut(300);
1054 515 }
1055 516 });
1056 517
518 + // Close modal on Escape key
1057 519 $(document).on('keydown', function(e) {
1058 520 if (e.key === 'Escape') {
1059 - $('.mxch-modal-overlay').fadeOut(200);
521 + $('#mxchat-rag-context-modal').fadeOut(300);
1060 522 }
1061 523 });
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 -});
524 +});