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 +368 -1726 3.2.182.6.3 View file →
@@ -1,931 +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 - // Use .attr() — jQuery's .data() coerces "null"/"true"/numeric strings to JS types,
183 - // which causes fetch/delete of those sessions to silently fail.
184 - selectedSessions.add($(this).attr('data-session-id'));
185 - $(this).addClass('selected');
186 - });
187 - $('#mxch-chat-list').addClass('selection-mode');
45 +
46 + // Load with search filter
47 + loadTranscripts(currentPage, searchTerm);
188 48 } else {
189 - selectedSessions.clear();
190 - $('.mxch-chat-item').removeClass('selected');
191 - $('#mxch-chat-list').removeClass('selection-mode');
49 + // Reset to first page with no search term
50 + currentPage = 1;
51 + loadTranscripts(currentPage, '');
192 52 }
193 -
194 - updateSelectionUI();
195 53 });
196 54
197 - // Update selection UI
198 - function updateSelectionUI() {
199 - const count = selectedSessions.size;
200 - const $countEl = $('#mxch-selected-count');
201 - const $deleteBtn = $('#mxch-delete-selected');
55 + // Initial load of transcripts
56 + loadTranscripts(currentPage, '');
202 57
203 - if (count > 0) {
204 - $countEl.text(count + ' selected').addClass('has-selection');
205 - $deleteBtn.prop('disabled', false);
206 - $('#mxch-chat-list').addClass('selection-mode');
207 - } else {
208 - $countEl.removeClass('has-selection');
209 - $deleteBtn.prop('disabled', true);
210 - $('#mxch-chat-list').removeClass('selection-mode');
211 - }
212 -
213 - // Update select all checkbox state
214 - const totalItems = $('.mxch-chat-checkbox').length;
215 - const checkedItems = $('.mxch-chat-checkbox:checked').length;
216 - $('#mxch-select-all').prop('checked', totalItems > 0 && checkedItems === totalItems);
217 - $('#mxch-select-all').prop('indeterminate', checkedItems > 0 && checkedItems < totalItems);
218 - }
219 -
220 - // Sort button — opens a small menu with 4 sort modes (plan-a5b006 adds rating sorts).
221 - $('#mxch-sort-btn').on('click', function(e) {
222 - e.stopPropagation();
223 - const $btn = $(this);
224 - let $menu = $('#mxch-sort-menu');
225 - if (!$menu.length) {
226 - $menu = $(
227 - '<div id="mxch-sort-menu" class="mxch-sort-menu" role="menu">' +
228 - ' <button type="button" class="mxch-sort-option" data-sort="desc" role="menuitem">Newest first</button>' +
229 - ' <button type="button" class="mxch-sort-option" data-sort="asc" role="menuitem">Oldest first</button>' +
230 - ' <button type="button" class="mxch-sort-option" data-sort="rating_positive" role="menuitem"><span class="mxch-sort-option-icon mxch-rating-thumb-up"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H7"/><path d="M3 10h4"/></svg></span>Positive ratings first</button>' +
231 - ' <button type="button" class="mxch-sort-option" data-sort="rating_negative" role="menuitem"><span class="mxch-sort-option-icon mxch-rating-thumb-down"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H17"/><path d="M21 14h-4"/></svg></span>Negative ratings first</button>' +
232 - '</div>'
233 - );
234 - $('body').append($menu);
235 - $menu.on('click', '.mxch-sort-option', function() {
236 - currentSortOrder = $(this).data('sort');
237 - $menu.find('.mxch-sort-option').removeClass('active');
238 - $(this).addClass('active');
239 - $menu.hide();
240 - loadChatList(1, $('#mxch-search-transcripts').val());
241 - });
242 - $(document).on('click.mxchSortMenu', function(ev) {
243 - if (!$(ev.target).closest('#mxch-sort-menu, #mxch-sort-btn').length) {
244 - $menu.hide();
245 - }
246 - });
247 - }
248 - $menu.find('.mxch-sort-option').removeClass('active');
249 - $menu.find('[data-sort="' + currentSortOrder + '"]').addClass('active');
250 - const offset = $btn.offset();
251 - const btnHeight = $btn.outerHeight();
252 - $menu.css({
253 - position: 'absolute',
254 - top: (offset.top + btnHeight + 4) + 'px',
255 - left: offset.left + 'px'
256 - }).toggle();
257 - });
258 -
259 - // Render a rating badge for a session row. Uses inline SVG so the glyph
260 - // renders the same across OSes (emoji fonts vary). Tooltip surfaces the
261 - // optional feedback text.
262 - function renderRatingBadge(session) {
263 - const value = (session && typeof session.rating_value === 'number') ? session.rating_value : null;
264 - const feedback = (session && session.rating_feedback) ? session.rating_feedback : '';
265 - if (value === 1) {
266 - const title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat positively';
267 - return '<span class="mxch-chat-rating mxch-chat-rating-up" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H7"/><path d="M3 10h4"/></svg></span>';
268 - }
269 - if (value === -1) {
270 - const title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat negatively';
271 - return '<span class="mxch-chat-rating mxch-chat-rating-down" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H17"/><path d="M21 14h-4"/></svg></span>';
272 - }
273 - return '<span class="mxch-chat-rating mxch-chat-rating-empty" title="No rating yet" aria-label="No rating yet">—</span>';
274 - }
275 -
276 - // Delete selected button — opens the shared confirm modal with the "also delete lead" checkbox.
277 - $('#mxch-delete-selected').on('click', function() {
278 - const count = selectedSessions.size;
279 - if (count === 0) return;
280 - openTranscriptConfirm(Array.from(selectedSessions), count);
281 - });
282 -
283 - // Transcript delete confirm (shared by bulk + individual) ---------------------------
284 - let transcriptConfirmSessionIds = [];
285 -
286 - function openTranscriptConfirm(sessionIds, count) {
287 - transcriptConfirmSessionIds = sessionIds.slice();
288 - const n = count || sessionIds.length;
289 - $('#mxch-transcript-confirm-title').text(n === 1 ? 'Delete conversation?' : 'Delete ' + n + ' conversations?');
290 - $('#mxch-transcript-confirm-body').text(
291 - n === 1
292 - ? 'This removes the conversation and its messages.'
293 - : 'This removes ' + n + ' conversations and their messages.'
294 - );
295 - $('#mxch-transcript-also-delete-lead').prop('checked', false);
296 - $('#mxch-transcript-confirm').fadeIn(120);
297 - }
298 -
299 - function closeTranscriptConfirm() {
300 - $('#mxch-transcript-confirm').fadeOut(120);
301 - transcriptConfirmSessionIds = [];
302 - }
303 -
304 - $(document).on('click', '[data-mxch-transcript-close]', closeTranscriptConfirm);
305 -
306 - $('#mxch-transcript-confirm-go').on('click', function() {
307 - const ids = transcriptConfirmSessionIds.slice();
308 - if (!ids.length) { closeTranscriptConfirm(); return; }
309 - const alsoDeleteLead = $('#mxch-transcript-also-delete-lead').is(':checked');
310 - closeTranscriptConfirm();
311 - deleteMultipleSessions(ids, alsoDeleteLead);
312 - });
313 -
314 - // Delete multiple sessions
315 - function deleteMultipleSessions(sessionIds, alsoDeleteLead) {
58 + // Function to load transcripts with pagination
59 + function loadTranscripts(page, searchTerm = '') {
60 + $('#mxchat-transcripts').html('<div class="mxchat-loading">Loading transcripts...</div>');
61 +
316 62 $.ajax({
317 63 url: ajaxurl,
318 64 type: 'POST',
319 65 data: {
320 - action: 'mxchat_delete_chat_history',
321 - delete_session_ids: sessionIds,
322 - also_delete_lead: alsoDeleteLead ? '1' : '0',
323 - security: $('#mxchat_delete_chat_nonce').val()
324 - },
325 - success: function(response) {
326 - try {
327 - const jsonResponse = typeof response === 'object' ? response : JSON.parse(response);
328 -
329 - if (jsonResponse.success) {
330 - // Clear selection
331 - selectedSessions.clear();
332 - $('#mxch-select-all').prop('checked', false);
333 - updateSelectionUI();
334 -
335 - // If current conversation was deleted, reset panel
336 - if (sessionIds.includes(currentSessionId)) {
337 - currentSessionId = null;
338 - $('#mxch-conversation-content').hide();
339 - $('#mxch-conversation-empty').show();
340 - $('#mxch-details-drawer').hide();
341 - }
342 -
343 - // Reload list
344 - loadChatList(currentPage, $('#mxch-search-transcripts').val());
345 -
346 - // The Leads tab shares this data (Chat deleted pill, nav badge,
347 - // stats) — invalidate it so switching tabs re-fetches instead of
348 - // showing stale "active lead" rows.
349 - invalidateLeadsData();
350 - } else if (jsonResponse.error) {
351 - alert('Error: ' + jsonResponse.error);
352 - }
353 - } catch (e) {
354 - alert('An error occurred while processing the response.');
355 - }
356 - },
357 - error: function() {
358 - alert('An error occurred while deleting conversations.');
359 - }
360 - });
361 - }
362 -
363 - // Load chat list function
364 - function loadChatList(page, searchTerm) {
365 - const $container = $('#mxch-chat-list');
366 - $container.html('<div class="mxch-list-loading"><span class="spinner is-active"></span></div>');
367 -
368 - $.ajax({
369 - url: ajaxurl,
370 - type: 'POST',
371 - data: {
372 66 action: 'mxchat_fetch_chat_history',
373 67 page: page,
374 68 per_page: perPage,
375 - search: searchTerm,
376 - sort_order: currentSortOrder
69 + search: searchTerm
377 70 },
378 71 success: function(response) {
379 - transcriptsLoaded = true;
380 -
381 - if (response.success && response.sessions && response.sessions.length > 0) {
382 - renderChatList(response.sessions);
383 - currentPage = response.page;
384 - totalPages = response.total_pages;
385 - updateChatCount(response.showing_start, response.showing_end, response.total_sessions);
386 - renderPagination(response.page, response.total_pages, searchTerm);
387 - } else {
388 - $container.html('<div class="mxch-list-empty"><p>No chats found</p></div>');
389 - updateChatCount(0, 0, 0);
390 - $('#mxch-pagination').html('');
391 - }
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();
392 105 },
393 - error: function() {
394 - $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);
395 109 }
396 110 });
397 111 }
398 -
399 - // Render chat list items
400 - function renderChatList(sessions) {
401 - const $container = $('#mxch-chat-list');
402 - let html = '';
403 -
404 - sessions.forEach(function(session) {
405 - const isActive = session.session_id === currentSessionId ? ' active' : '';
406 - const isSelected = selectedSessions.has(session.session_id) ? ' selected' : '';
407 - const isChecked = selectedSessions.has(session.session_id) ? ' checked' : '';
408 - html += `
409 - <div class="mxch-chat-item${isActive}${isSelected}" data-session-id="${escapeHtml(session.session_id)}">
410 - <input type="checkbox" class="mxch-chat-checkbox"${isChecked}>
411 - <div class="mxch-chat-avatar">
412 - <span>${escapeHtml(session.initials)}</span>
413 - </div>
414 - <div class="mxch-chat-info">
415 - <div class="mxch-chat-name">${escapeHtml(session.display_name)}</div>
416 - <div class="mxch-chat-preview">${escapeHtml(session.preview)}</div>
417 - </div>
418 - <div class="mxch-chat-meta">
419 - ${renderRatingBadge(session)}
420 - <span class="mxch-chat-time">${escapeHtml(session.time_display)}</span>
421 - <span class="mxch-chat-count">${session.message_count}</span>
422 - </div>
423 - </div>
424 - `;
425 - });
426 -
427 - $container.html(html);
428 -
429 - // Attach checkbox handlers
430 - $('.mxch-chat-checkbox').on('click', function(e) {
431 - e.stopPropagation(); // Prevent triggering chat item click
432 - const $item = $(this).closest('.mxch-chat-item');
433 - const sessionId = $item.attr('data-session-id');
434 -
435 - if ($(this).is(':checked')) {
436 - selectedSessions.add(sessionId);
437 - $item.addClass('selected');
438 - } else {
439 - selectedSessions.delete(sessionId);
440 - $item.removeClass('selected');
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;
441 124 }
442 -
443 - updateSelectionUI();
125 +
126 + // Delete single session
127 + deleteSessions([sessionId]);
444 128 });
445 -
446 - // Attach click handlers for selecting chat
447 - $('.mxch-chat-item').on('click', function(e) {
448 - // Don't trigger if clicking on checkbox
449 - if ($(e.target).is('.mxch-chat-checkbox')) return;
450 -
451 - const sessionId = $(this).attr('data-session-id');
452 - selectChat(sessionId);
453 -
454 - // Update active state
455 - $('.mxch-chat-item').removeClass('active');
456 - $(this).addClass('active');
457 -
458 - // Show conversation panel on mobile
459 - showMobileConversationPanel();
460 - });
461 -
462 - // Update selection UI after render
463 - updateSelectionUI();
464 - }
465 -
466 - // Update chat count display
467 - function updateChatCount(start, end, total) {
468 - if (total === 0) {
469 - $('#mxch-chat-count').text('0 chats');
470 - } else {
471 - $('#mxch-chat-count').text(`${start}-${end} / ${total} chats`);
472 - }
473 - }
474 -
475 - // Render pagination
476 - function renderPagination(currentPage, totalPages, searchTerm) {
477 - const $container = $('#mxch-pagination');
478 -
479 - if (totalPages <= 1) {
480 - $container.html('');
481 - return;
482 - }
483 -
484 - let html = '<div class="mxch-pagination-btns">';
485 -
486 - if (currentPage > 1) {
487 - html += `<button class="mxch-page-btn" data-page="${currentPage - 1}">&laquo;</button>`;
488 - }
489 -
490 - html += `<span class="mxch-page-info">${currentPage} / ${totalPages}</span>`;
491 -
492 - if (currentPage < totalPages) {
493 - html += `<button class="mxch-page-btn" data-page="${currentPage + 1}">&raquo;</button>`;
494 - }
495 -
496 - html += '</div>';
497 - $container.html(html);
498 -
499 - // Pagination click handlers
500 - $('.mxch-page-btn').on('click', function() {
501 - const pageNum = $(this).data('page');
502 - loadChatList(pageNum, searchTerm);
503 - });
504 - }
505 -
506 - // ==========================================================================
507 - // Conversation Panel
508 - // ==========================================================================
509 -
510 - // Select and load a chat conversation
511 - function selectChat(sessionId) {
512 - currentSessionId = sessionId;
513 -
514 - // Reset translation state when selecting new chat
515 - if (typeof resetTranslationState === 'function') {
516 - resetTranslationState();
517 - }
518 -
519 - // Show loading in conversation panel
520 - $('#mxch-conversation-empty').hide();
521 - $('#mxch-conversation-content').show();
522 - $('#mxch-messages-area').html('<div class="mxch-messages-loading"><span class="spinner is-active"></span> Loading conversation...</div>');
523 -
524 - $.ajax({
525 - url: ajaxurl,
526 - type: 'POST',
527 - data: {
528 - action: 'mxchat_fetch_conversation',
529 - session_id: sessionId
530 - },
531 - success: function(response) {
532 - if (response.success) {
533 - renderConversation(response);
534 - // Load saved translation after rendering
535 - if (typeof loadSavedTranslation === 'function') {
536 - setTimeout(function() {
537 - loadSavedTranslation(sessionId);
538 - }, 100);
539 - }
540 - } else {
541 - $('#mxch-messages-area').html('<div class="mxch-messages-error">Failed to load conversation</div>');
542 - }
543 - },
544 - error: function() {
545 - $('#mxch-messages-area').html('<div class="mxch-messages-error">Error loading conversation</div>');
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;
546 135 }
547 - });
548 - }
549 136
550 - // Render conversation content
551 - function renderConversation(data) {
552 - // Update header
553 - $('#mxch-user-avatar span').text(data.user.initials);
554 - $('#mxch-user-name').text(data.user.name);
555 - $('#mxch-user-meta').text(data.user.sub);
137 + const sessionId = $(this).data('session-id');
138 + const sessionContainer = $(this).closest('.mxchat-session');
556 139
557 - // Update details drawer
558 - $('#mxch-detail-messages').text(data.message_count);
559 - $('#mxch-detail-started').text(data.started);
560 -
561 - if (data.page.url) {
562 - $('#mxch-detail-page').html(`<a href="${escapeHtml(data.page.url)}" target="_blank">${escapeHtml(data.page.title || data.page.url)}</a>`);
563 - } else {
564 - $('#mxch-detail-page').text('-');
565 - }
566 -
567 - if (data.user.email) {
568 - $('#mxch-detail-email').text(data.user.email);
569 - $('#mxch-detail-email-row').show();
570 - } else {
571 - $('#mxch-detail-email-row').hide();
572 - }
573 -
574 - // Feedback row — .text() (not .html()) for XSS-safe display of user-submitted text.
575 - // rating_feedback is already sanitize_text_field()'d + mb_substr(200) on save.
576 - var feedback = (data && data.rating_feedback) ? String(data.rating_feedback).trim() : '';
577 - if (feedback) {
578 - $('#mxch-detail-feedback').text(feedback);
579 - $('#mxch-detail-feedback-row').show();
580 - } else {
581 - $('#mxch-detail-feedback-row').hide();
582 - }
583 -
584 - // Clicked links
585 - if (data.clicked_urls && data.clicked_urls.length > 0) {
586 - let linksHtml = '';
587 - data.clicked_urls.forEach(function(url) {
588 - linksHtml += `<a href="${escapeHtml(url)}" target="_blank" class="mxch-clicked-link">${escapeHtml(url)}</a>`;
589 - });
590 - $('#mxch-clicked-links').html(linksHtml);
591 - $('#mxch-clicked-section').show();
592 - } else {
593 - $('#mxch-clicked-section').hide();
594 - }
595 -
596 - // Render messages
597 - let messagesHtml = '';
598 - data.messages.forEach(function(msg) {
599 - if (msg.is_user) {
600 - messagesHtml += `
601 - <div class="mxch-message mxch-message-user" data-message-id="${msg.id}">
602 - <div class="mxch-message-row">
603 - <div class="mxch-message-bubble">
604 - ${msg.content}
605 - </div>
606 - </div>
607 - <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>
608 - </div>
609 - `;
140 + if (selectedSessions.has(sessionId)) {
141 + selectedSessions.delete(sessionId);
142 + sessionContainer.removeClass('selected');
610 143 } else {
611 - // Live-agent replies (role 'agent') get their own label and never
612 - // carry a Sources link; bot/assistant rows render exactly as before.
613 - const isAgent = !!msg.is_agent;
614 - const ragLink = (!isAgent && msg.has_rag) ? `<a href="#" class="mxch-rag-link" data-message-id="${msg.id}">Sources</a>` : '';
615 - const senderLabel = isAgent
616 - ? '<span class="mxch-agent-label">Live Agent</span>'
617 - : '<span class="mxch-bot-label">AI Assistant</span>';
618 - messagesHtml += `
619 - <div class="mxch-message mxch-message-bot${isAgent ? ' mxch-message-agent' : ''}" data-message-id="${msg.id}">
620 - <div class="mxch-message-header">
621 - ${senderLabel}
622 - ${ragLink}
623 - </div>
624 - <div class="mxch-message-row">
625 - <div class="mxch-message-bubble">
626 - ${msg.content}
627 - </div>
628 - </div>
629 - <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>
630 - </div>
631 - `;
144 + selectedSessions.add(sessionId);
145 + sessionContainer.addClass('selected');
632 146 }
147 +
148 + updateDeleteButtonState();
633 149 });
634 150
635 - $('#mxch-messages-area').html(messagesHtml);
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();
636 155
637 - // Scroll to bottom
638 - const $area = $('#mxch-messages-area');
639 - $area.scrollTop($area[0].scrollHeight);
640 -
641 - // Attach RAG link handlers
642 - $('.mxch-rag-link').on('click', function(e) {
643 - e.preventDefault();
644 - const messageId = $(this).data('message-id');
156 + const messageId = $(this).closest('.mxchat-message').data('message-id');
645 157 if (messageId) {
646 158 openRagContextModal(messageId);
647 159 }
648 160 });
649 161 }
650 -
651 - // Toggle details drawer
652 - $('#mxch-toggle-details').on('click', function() {
653 - const $drawer = $('#mxch-details-drawer');
654 - const $btn = $(this);
655 -
656 - if ($drawer.is(':visible')) {
657 - $drawer.slideUp(200);
658 - $btn.removeClass('active');
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);
659 168 } else {
660 - $drawer.slideDown(200);
661 - $btn.addClass('active');
169 + deleteButton.prop('disabled', true);
662 170 }
663 - });
664 -
665 - // Delete current chat — opens the shared confirm modal.
666 - $('#mxch-delete-current').on('click', function() {
667 - if (!currentSessionId) return;
668 - openTranscriptConfirm([currentSessionId], 1);
669 - });
670 -
671 - // Delete session function
672 - function deleteSession(sessionId, alsoDeleteLead) {
171 + }
172 +
173 + // Function to delete sessions
174 + function deleteSessions(sessionIds) {
673 175 $.ajax({
674 176 url: ajaxurl,
675 177 type: 'POST',
676 178 data: {
677 179 action: 'mxchat_delete_chat_history',
678 - delete_session_ids: [sessionId],
679 - also_delete_lead: alsoDeleteLead ? '1' : '0',
180 + delete_session_ids: sessionIds,
680 181 security: $('#mxchat_delete_chat_nonce').val()
681 182 },
682 183 success: function(response) {
683 - try {
684 - const jsonResponse = typeof response === 'object' ? response : JSON.parse(response);
685 -
686 - if (jsonResponse.success) {
687 - // Reset conversation panel
688 - currentSessionId = null;
689 - $('#mxch-conversation-content').hide();
690 - $('#mxch-conversation-empty').show();
691 - $('#mxch-details-drawer').hide();
692 -
693 - // Reload list
694 - loadChatList(currentPage, $('#mxch-search-transcripts').val());
695 - } else if (jsonResponse.error) {
696 - alert('Error: ' + jsonResponse.error);
697 - }
698 - } catch (e) {
699 - alert('An error occurred while processing the response.');
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);
193 + } else {
194 + //console.log("Unexpected response format.");
700 195 }
196 +
197 + // Reload the current page of transcripts
198 + loadTranscripts(currentPage);
701 199 },
702 - error: function() {
703 - alert('An error occurred while deleting the conversation.');
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.");
704 204 }
705 205 });
706 206 }
707 207
708 - // ==========================================================================
709 - // Export Functionality
710 - // ==========================================================================
711 -
712 - $('#mxch-export-btn, #mxch-export-current').on('click', function() {
713 - const $button = $(this);
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;
215 + }
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.`)) {
219 + return;
220 + }
221 +
222 + // Convert Set to Array and delete
223 + deleteSessions(Array.from(selectedSessions));
224 + });
225 +
226 + // Export functionality - this remains unchanged as it should export all transcripts
227 + $('#mxchat-export-transcripts').on('click', function() {
228 + var $button = $(this);
714 229 $button.prop('disabled', true).addClass('loading');
715 230
716 - const $form = $('<form>', {
717 - method: 'post',
718 - action: ajaxurl
231 + // Create a form and submit it
232 + var $form = $('<form>', {
233 + 'method': 'post',
234 + 'action': ajaxurl
719 235 });
720 236
721 237 $form.append($('<input>', {
722 - type: 'hidden',
723 - name: 'action',
724 - value: 'mxchat_export_transcripts'
238 + 'type': 'hidden',
239 + 'name': 'action',
240 + 'value': 'mxchat_export_transcripts'
725 241 }));
726 242
727 243 $form.append($('<input>', {
728 - type: 'hidden',
729 - name: 'security',
730 - value: mxchatAdmin.export_nonce
244 + 'type': 'hidden',
245 + 'name': 'security',
246 + 'value': mxchatAdmin.export_nonce
731 247 }));
732 248
733 249 $form.appendTo('body').submit();
734 250
251 + // Re-enable the button after a short delay
735 252 setTimeout(function() {
736 253 $button.prop('disabled', false).removeClass('loading');
737 254 }, 2000);
738 255 });
739 -
740 - // ==========================================================================
741 - // Translation Functionality
742 - // ==========================================================================
743 -
744 - // Store original messages for reverting
745 - let originalMessages = null;
746 - let isTranslated = false;
747 - let currentTranslationLang = null;
748 -
749 - // Load saved language preference from localStorage
750 - const savedLang = localStorage.getItem('mxch_translate_lang');
751 - if (savedLang) {
752 - $('#mxch-translate-lang').val(savedLang);
753 - }
754 -
755 - // Save language preference when changed
756 - $('#mxch-translate-lang').on('change', function() {
757 - 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);
758 263 });
759 -
760 - // Apply translations to messages
761 - function applyTranslations(translations) {
762 - // Store original messages if not already stored
763 - if (!originalMessages) {
764 - originalMessages = [];
765 - $('#mxch-messages-area .mxch-message-bubble').each(function() {
766 - originalMessages.push($(this).html());
767 - });
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);
768 274 }
769 -
770 - // Apply translations
771 - translations.forEach(function(item) {
772 - const $bubble = $('#mxch-messages-area .mxch-message-bubble').eq(item.index);
773 - if ($bubble.length) {
774 - $bubble.html(item.translated);
775 - $bubble.addClass('translated');
776 - }
777 - });
778 -
779 - isTranslated = true;
780 - $('#mxch-show-original-btn').show();
781 - }
782 -
783 - // Load saved translation for current session
784 - function loadSavedTranslation(sessionId) {
785 - $.ajax({
786 - url: ajaxurl,
787 - type: 'POST',
788 - data: {
789 - action: 'mxchat_get_transcript_translation',
790 - session_id: sessionId
791 - },
792 - success: function(response) {
793 - if (response.success && response.has_translation) {
794 - currentTranslationLang = response.language;
795 - applyTranslations(response.translations);
796 - // Update language selector to show saved language
797 - $('#mxch-translate-lang').val(response.language);
798 - }
799 - }
800 - });
801 - }
802 -
803 - // Translate button click handler
804 - $('#mxch-translate-btn').on('click', function() {
805 - if (!currentSessionId) return;
806 -
807 - const $btn = $(this);
808 - const targetLang = $('#mxch-translate-lang').val();
809 -
810 - // Disable button and show loading state
811 - $btn.prop('disabled', true);
812 - $btn.find('.mxch-translate-text').text('Translating...');
813 - $btn.find('svg').addClass('mxch-translate-spinner');
814 -
815 - // Store original messages before translation
816 - if (!originalMessages) {
817 - originalMessages = [];
818 - $('#mxch-messages-area .mxch-message-bubble').each(function() {
819 - originalMessages.push($(this).html());
820 - });
821 - }
822 -
823 - // If already translated, restore originals first before re-translating
824 - if (isTranslated) {
825 - $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
826 - if (originalMessages[index]) {
827 - $(this).html(originalMessages[index]);
828 - $(this).removeClass('translated');
829 - }
830 - });
831 - }
832 -
833 - // Collect all message content (from originals)
834 - const messages = [];
835 - originalMessages.forEach(function(html, index) {
836 - // Create temp element to get text content
837 - const $temp = $('<div>').html(html);
838 - messages.push({
839 - index: index,
840 - content: $temp.text().trim()
841 - });
842 - });
843 -
844 - // Send translation request
845 - $.ajax({
846 - url: ajaxurl,
847 - type: 'POST',
848 - data: {
849 - action: 'mxchat_translate_messages',
850 - session_id: currentSessionId,
851 - target_lang: targetLang,
852 - messages: JSON.stringify(messages),
853 - security: mxchatAdmin.translate_nonce || ''
854 - },
855 - success: function(response) {
856 - if (response.success && response.translations) {
857 - currentTranslationLang = response.language;
858 - applyTranslations(response.translations);
859 - $btn.find('.mxch-translate-text').text('Translate');
860 - } else {
861 - alert(response.error || 'Translation failed. Please try again.');
862 - $btn.find('.mxch-translate-text').text('Translate');
863 - }
864 - },
865 - error: function() {
866 - alert('Translation request failed. Please try again.');
867 - $btn.find('.mxch-translate-text').text('Translate');
868 - },
869 - complete: function() {
870 - $btn.prop('disabled', false);
871 - $btn.find('svg').removeClass('mxch-translate-spinner');
872 - }
873 - });
874 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();
875 282
876 - // Show original button click handler
877 - $('#mxch-show-original-btn').on('click', function() {
878 - if (!originalMessages) return;
283 + // Just show a loading state
284 + $submitButton.text('Saving...').prop('disabled', true);
879 285
880 - // Restore original messages
881 - $('#mxch-messages-area .mxch-message-bubble').each(function(index) {
882 - if (originalMessages[index]) {
883 - $(this).html(originalMessages[index]);
884 - $(this).removeClass('translated');
885 - }
886 - });
887 -
888 - isTranslated = false;
889 - $(this).hide();
286 + // The form will submit normally and reload the page
890 287 });
891 288
892 - // Reset translation state (called when selecting new chat)
893 - function resetTranslationState() {
894 - originalMessages = null;
895 - isTranslated = false;
896 - currentTranslationLang = null;
897 - $('#mxch-show-original-btn').hide();
898 - }
289 + // ========== RAG Context Modal Functions ==========
899 290
900 - // Make functions available to selectChat
901 - window.resetTranslationState = resetTranslationState;
902 - window.loadSavedTranslation = loadSavedTranslation;
903 -
904 - // ==========================================================================
905 - // RAG Context Modal (Sources & Actions Tabs)
906 - // ==========================================================================
907 -
291 + // Open RAG context modal and fetch data
908 292 function openRagContextModal(messageId) {
909 - const $modal = $('#mxch-rag-modal');
910 - const $loading = $modal.find('.mxch-rag-loading');
911 - const $sourcesContent = $modal.find('.mxch-rag-content');
912 - 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');
913 296
914 - // Reset to Sources tab
915 - $modal.find('.mxch-context-tab').removeClass('active');
916 - $modal.find('.mxch-context-tab[data-tab="sources"]').addClass('active');
917 - $('#mxch-tab-sources').show();
918 - $('#mxch-tab-actions').hide();
919 -
920 - // Reset badge counts
921 - $('#mxch-sources-count, #mxch-actions-count').hide().text('0');
922 -
923 - $modal.fadeIn(200);
297 + // Show modal with loading state
298 + $modal.fadeIn(300);
924 299 $loading.show();
925 - $sourcesContent.html('');
926 - $actionsContent.html('');
300 + $content.html('');
927 301
302 + // Fetch RAG context via AJAX
928 303 $.ajax({
929 304 url: ajaxurl,
930 305 type: 'POST',
931 306 data: {
@@ -933,950 +308,217 @@
933 308 message_id: messageId
934 309 },
935 310 success: function(response) {
936 311 $loading.hide();
937 -
938 312 if (response.success && response.data) {
939 - // Render sources tab
940 - renderRagContext(response.data, $sourcesContent);
941 -
942 - // Render actions tab
943 - renderActionsContext(response.data, $actionsContent);
944 -
945 - // Update badge counts
946 - const sourcesCount = response.data.top_matches ? response.data.top_matches.length : 0;
947 - const actionsCount = response.data.action_analysis ? response.data.action_analysis.length : 0;
948 -
949 - if (sourcesCount > 0) {
950 - $('#mxch-sources-count').text(sourcesCount).show();
951 - }
952 - if (actionsCount > 0) {
953 - $('#mxch-actions-count').text(actionsCount).show();
954 - }
313 + renderRagContext(response.data, $content);
955 314 } else {
956 - $sourcesContent.html('<div class="mxch-rag-error">Unable to load document context.</div>');
957 - $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>');
958 316 }
959 317 },
960 318 error: function() {
961 319 $loading.hide();
962 - $sourcesContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>');
963 - $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>');
964 321 }
965 322 });
966 323 }
967 324
968 - // Tab switching
969 - $(document).on('click', '.mxch-context-tab', function() {
970 - const $tab = $(this);
971 - const tabName = $tab.data('tab');
972 -
973 - // Update active tab
974 - $('.mxch-context-tab').removeClass('active');
975 - $tab.addClass('active');
976 -
977 - // Show/hide content
978 - $('.mxch-tab-content').hide();
979 - $('#mxch-tab-' + tabName).show();
980 - });
981 -
325 + // Render RAG context data in the modal - grouped by URL
982 326 function renderRagContext(data, $container) {
983 327 let html = '';
984 328
985 - // Check if we have any source data
986 - if (!data.top_matches || data.top_matches.length === 0) {
987 - html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>';
988 - $container.html(html);
989 - return;
990 - }
991 -
992 - // Hybrid keyword boost (38ffa1): rows carry matched_via + fused_rank
993 - // when hybrid retrieval was on for this response. Cosine % stays the
994 - // anchor; the chip explains WHY a low-% row ranked high.
995 - const hybridOn = data.top_matches.some(function(m) { return m.matched_via; });
996 -
997 - html += '<div class="mxch-rag-summary">';
998 - 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>';
999 - 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>';
1000 - 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>';
1001 - if (hybridOn) {
1002 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Retrieval:</span> <span class="mxch-rag-value">Hybrid</span></div>';
1003 - }
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>';
1004 334 html += '</div>';
1005 -
1006 - const groupedByUrl = {};
1007 -
1008 - data.top_matches.forEach(function(match) {
1009 - const url = match.source_display || 'Unknown';
1010 - if (!groupedByUrl[url]) {
1011 - groupedByUrl[url] = {
1012 - url: url,
1013 - isUrl: url.startsWith('http'),
1014 - bestScore: 0,
1015 - usedForContext: false,
1016 - matchedChunks: [],
1017 - bestFusedRank: Infinity,
1018 - viaSet: {}
1019 - };
1020 - }
1021 -
1022 - if (match.similarity_percentage > groupedByUrl[url].bestScore) {
1023 - groupedByUrl[url].bestScore = match.similarity_percentage;
1024 - }
1025 -
1026 - if (match.used_for_context) {
1027 - groupedByUrl[url].usedForContext = true;
1028 - }
1029 -
1030 - if (match.fused_rank && match.fused_rank < groupedByUrl[url].bestFusedRank) {
1031 - groupedByUrl[url].bestFusedRank = match.fused_rank;
1032 - }
1033 - if (match.matched_via) {
1034 - groupedByUrl[url].viaSet[match.matched_via] = true;
1035 - }
1036 -
1037 - groupedByUrl[url].matchedChunks.push({
1038 - chunkIndex: match.chunk_index,
1039 - score: match.similarity_percentage,
1040 - usedForContext: match.used_for_context
1041 - });
1042 - });
1043 -
1044 - // Hybrid on: order by fused rank (rows without one sort last);
1045 - // otherwise by cosine, exactly as before.
1046 - const urlGroups = Object.values(groupedByUrl).sort(function(a, b) {
1047 - if (hybridOn && a.bestFusedRank !== b.bestFusedRank) {
1048 - return a.bestFusedRank - b.bestFusedRank;
1049 - }
1050 - return b.bestScore - a.bestScore;
1051 - });
1052 - const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length;
1053 - const chunksInfo = data.total_chunks_used > 0 ? data.total_chunks_used + ' chunks sent to AI' : '';
1054 -
1055 - html += '<div class="mxch-rag-matches">';
1056 - html += '<h3>Retrieved Documents</h3>';
1057 - 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>';
1058 -
1059 - urlGroups.forEach(function(group) {
1060 - const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
1061 - const statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
1062 - const statusLabel = group.usedForContext ? 'Used' : 'Not Used';
1063 -
1064 - html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1065 - html += '<div class="mxch-rag-match-header">';
1066 - html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>';
1067 -
1068 - if (hybridOn) {
1069 - const vias = Object.keys(group.viaSet);
1070 - if (vias.length) {
1071 - const viaLabel = (vias.length > 1 || vias[0] === 'both') ? 'Both'
1072 - : (vias[0] === 'keyword' ? 'Keyword' : 'Vector');
1073 - html += '<span class="mxch-rag-via-chip mxch-rag-via-' + viaLabel.toLowerCase() + '">' + viaLabel + '</span>';
1074 - }
1075 - }
1076 -
1077 - if (group.matchedChunks.length > 1) {
1078 - html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>';
1079 - }
1080 -
1081 - html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>';
1082 - html += '</div>';
1083 -
1084 - html += '<div class="mxch-rag-match-source">';
1085 - if (group.isUrl) {
1086 - html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>';
1087 - } else {
1088 - html += escapeHtml(group.url);
1089 - }
1090 - html += '</div>';
1091 - html += '</div>';
1092 - });
1093 -
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>';
1094 338 html += '</div>';
1095 - $container.html(html);
1096 - }
1097 -
1098 - function renderActionsContext(data, $container) {
1099 - let html = '';
1100 -
1101 - // Check if we have action analysis data
1102 - if (!data.action_analysis || data.action_analysis.length === 0) {
1103 - 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>';
1104 - $container.html(html);
1105 - return;
1106 - }
1107 -
1108 - const actions = data.action_analysis;
1109 - const triggeredAction = actions.find(a => a.triggered);
1110 - const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
1111 -
1112 - // Summary section
1113 - html += '<div class="mxch-rag-summary">';
1114 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Actions Evaluated:</span> <span class="mxch-rag-value">' + actions.length + '</span></div>';
1115 - html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Above Threshold:</span> <span class="mxch-rag-value">' + actionsAboveThreshold + '</span></div>';
1116 - if (triggeredAction) {
1117 - 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>';
1118 - }
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>';
1119 342 html += '</div>';
1120 -
1121 - // Actions list
1122 - html += '<div class="mxch-rag-matches">';
1123 - html += '<h3>Action Scores</h3>';
1124 - html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">Showing all evaluated actions sorted by similarity score</p>';
1125 -
1126 - actions.forEach(function(action) {
1127 - let cardClass = 'mxch-rag-match-below';
1128 - let statusIcon = '&#10007;';
1129 - let statusLabel = 'Below Threshold';
1130 -
1131 - if (action.triggered) {
1132 - cardClass = 'mxch-action-triggered';
1133 - statusIcon = '&#9889;';
1134 - statusLabel = 'Triggered';
1135 - } else if (action.above_threshold) {
1136 - cardClass = 'mxch-rag-match-used';
1137 - statusIcon = '&#10003;';
1138 - statusLabel = 'Above Threshold';
1139 - }
1140 -
1141 - html += '<div class="mxch-rag-match-card ' + cardClass + '">';
1142 - html += '<div class="mxch-rag-match-header">';
1143 - html += '<span class="mxch-rag-match-score">' + action.similarity_percentage + '%</span>';
1144 - html += '<span class="mxch-action-threshold-badge">Threshold: ' + action.threshold_percentage + '%</span>';
1145 - html += '<span class="mxch-rag-match-status ' + (action.triggered ? 'status-triggered' : (action.above_threshold ? 'status-used' : 'status-below')) + '">' + statusIcon + ' ' + statusLabel + '</span>';
1146 - html += '</div>';
1147 -
1148 - html += '<div class="mxch-action-details">';
1149 - html += '<div class="mxch-action-label">' + escapeHtml(action.intent_label) + '</div>';
1150 - html += '<div class="mxch-action-callback"><span class="mxch-action-callback-label">Callback:</span> ' + escapeHtml(action.callback_function) + '</div>';
1151 - html += '</div>';
1152 -
1153 - // Score bar visualization
1154 - const scoreBarWidth = Math.min(action.similarity_percentage, 100);
1155 - const thresholdPos = Math.min(action.threshold_percentage, 100);
1156 - html += '<div class="mxch-action-score-bar">';
1157 - html += '<div class="mxch-action-score-fill" style="width: ' + scoreBarWidth + '%;"></div>';
1158 - html += '<div class="mxch-action-threshold-marker" style="left: ' + thresholdPos + '%;"></div>';
1159 - html += '</div>';
1160 -
1161 - html += '</div>';
1162 - });
1163 -
1164 343 html += '</div>';
1165 - $container.html(html);
1166 - }
1167 344
1168 - function escapeHtml(text) {
1169 - if (!text) return '';
1170 - const div = document.createElement('div');
1171 - div.textContent = text;
1172 - return div.innerHTML;
1173 - }
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 + }
1174 363
1175 - // Close RAG modal
1176 - $('.mxch-modal-close').on('click', function() {
1177 - $(this).closest('.mxch-modal-overlay').fadeOut(200);
1178 - });
364 + // Track best score
365 + if (match.similarity_percentage > groupedByUrl[url].bestScore) {
366 + groupedByUrl[url].bestScore = match.similarity_percentage;
367 + }
1179 368
1180 - $('.mxch-modal-overlay').on('click', function(e) {
1181 - if ($(e.target).is('.mxch-modal-overlay')) {
1182 - $(this).fadeOut(200);
1183 - }
1184 - });
1185 -
1186 - $(document).on('keydown', function(e) {
1187 - if (e.key === 'Escape') {
1188 - $('.mxch-modal-overlay').fadeOut(200);
1189 - }
1190 - });
1191 -
1192 - // ==========================================================================
1193 - // Activity Chart
1194 - // ==========================================================================
1195 -
1196 - // Simple chart implementation (no external dependencies)
1197 - class SimpleChart {
1198 - constructor(canvas, config) {
1199 - this.canvas = canvas;
1200 - this.ctx = canvas.getContext('2d');
1201 - this.config = config;
1202 - this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
1203 - this.render();
1204 - }
1205 -
1206 - destroy() {
1207 - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1208 - }
1209 -
1210 - render() {
1211 - const dpr = window.devicePixelRatio || 1;
1212 - const rect = this.canvas.getBoundingClientRect();
1213 -
1214 - this.canvas.width = rect.width * dpr;
1215 - this.canvas.height = rect.height * dpr;
1216 - this.ctx.scale(dpr, dpr);
1217 -
1218 - this.canvas.style.width = rect.width + 'px';
1219 - this.canvas.style.height = rect.height + 'px';
1220 -
1221 - const width = rect.width - this.padding.left - this.padding.right;
1222 - const height = rect.height - this.padding.top - this.padding.bottom;
1223 -
1224 - // Find max value
1225 - let maxValue = 0;
1226 - this.config.datasets.forEach(dataset => {
1227 - const max = Math.max(...dataset.data);
1228 - if (max > maxValue) maxValue = max;
1229 - });
1230 -
1231 - // Add some padding to max value
1232 - maxValue = Math.ceil(maxValue * 1.1);
1233 - if (maxValue === 0) maxValue = 10;
1234 -
1235 - // Draw grid lines
1236 - this.ctx.strokeStyle = '#e5e7eb';
1237 - this.ctx.lineWidth = 1;
1238 - const gridLines = 5;
1239 -
1240 - for (let i = 0; i <= gridLines; i++) {
1241 - const y = this.padding.top + (height / gridLines) * i;
1242 - this.ctx.beginPath();
1243 - this.ctx.moveTo(this.padding.left, y);
1244 - this.ctx.lineTo(this.padding.left + width, y);
1245 - this.ctx.stroke();
1246 -
1247 - // Draw y-axis labels
1248 - const value = maxValue - (maxValue / gridLines) * i;
1249 - this.ctx.fillStyle = '#6b7280';
1250 - this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1251 - this.ctx.textAlign = 'right';
1252 - this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
1253 - }
1254 -
1255 - // Draw datasets
1256 - this.config.datasets.forEach(dataset => {
1257 - const points = [];
1258 - const xStep = width / (this.config.labels.length - 1 || 1);
1259 -
1260 - dataset.data.forEach((value, index) => {
1261 - const x = this.padding.left + (xStep * index);
1262 - const y = this.padding.top + height - (value / maxValue * height);
1263 - points.push({ x, y, value });
1264 - });
1265 -
1266 - // Draw filled area
1267 - if (dataset.fill && dataset.backgroundColor) {
1268 - this.ctx.fillStyle = dataset.backgroundColor;
1269 - this.ctx.beginPath();
1270 - this.ctx.moveTo(points[0].x, this.padding.top + height);
1271 - points.forEach(point => {
1272 - this.ctx.lineTo(point.x, point.y);
1273 - });
1274 - this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
1275 - this.ctx.closePath();
1276 - this.ctx.fill();
369 + // Track if any chunk was used
370 + if (match.used_for_context) {
371 + groupedByUrl[url].usedForContext = true;
1277 372 }
1278 373
1279 - // Draw line
1280 - this.ctx.strokeStyle = dataset.borderColor;
1281 - this.ctx.lineWidth = 3;
1282 - this.ctx.lineCap = 'round';
1283 - this.ctx.lineJoin = 'round';
1284 -
1285 - this.ctx.beginPath();
1286 - points.forEach((point, index) => {
1287 - if (index === 0) {
1288 - this.ctx.moveTo(point.x, point.y);
1289 - } else {
1290 - this.ctx.lineTo(point.x, point.y);
1291 - }
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
1292 381 });
1293 - this.ctx.stroke();
1294 -
1295 - // Draw points
1296 - points.forEach(point => {
1297 - this.ctx.fillStyle = '#ffffff';
1298 - this.ctx.beginPath();
1299 - this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
1300 - this.ctx.fill();
1301 - this.ctx.strokeStyle = dataset.borderColor;
1302 - this.ctx.lineWidth = 2;
1303 - this.ctx.stroke();
1304 - });
1305 382 });
1306 383
1307 - // Draw x-axis labels
1308 - const xStep = width / (this.config.labels.length - 1 || 1);
1309 - this.ctx.fillStyle = '#6b7280';
1310 - this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1311 - this.ctx.textAlign = 'center';
1312 -
1313 - this.config.labels.forEach((label, index) => {
1314 - const x = this.padding.left + (xStep * index);
1315 - this.ctx.fillText(label, x, this.padding.top + height + 20);
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;
1316 387 });
1317 - }
1318 - }
1319 388
1320 - // Initialize activity chart
1321 - function initActivityChart() {
1322 - console.log('[MxChat Chart] initActivityChart called');
389 + // Count unique URLs used
390 + const usedUrlCount = urlGroups.filter(function(g) { return g.usedForContext; }).length;
1323 391
1324 - const canvas = document.getElementById('mxchat-activity-chart');
1325 - console.log('[MxChat Chart] Canvas element:', canvas);
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>';
1326 396
1327 - if (!canvas) {
1328 - console.log('[MxChat Chart] Canvas not found, aborting');
1329 - return;
1330 - }
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';
1331 401
1332 - console.log('[MxChat Chart] mxchatChartData exists:', typeof mxchatChartData !== 'undefined');
1333 - if (typeof mxchatChartData === 'undefined') {
1334 - console.log('[MxChat Chart] mxchatChartData is undefined, aborting');
1335 - return;
1336 - }
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 + }
1337 408
1338 - console.log('[MxChat Chart] Raw mxchatChartData:', mxchatChartData);
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>';
1339 417
1340 - // Check if chart already exists and destroy it
1341 - if (canvas.chartInstance) {
1342 - canvas.chartInstance.destroy();
1343 - }
1344 -
1345 - const ctx = canvas.getContext('2d');
1346 - console.log('[MxChat Chart] Canvas context:', ctx);
1347 - console.log('[MxChat Chart] Canvas dimensions:', canvas.getBoundingClientRect());
1348 -
1349 - // Create gradient for chats line
1350 - const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
1351 - chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
1352 - chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
1353 -
1354 - // Create gradient for messages line
1355 - const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
1356 - messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
1357 - messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
1358 -
1359 - // Convert wp_localize_script objects to arrays (WordPress converts indexed arrays to objects)
1360 - const labels = Object.values(mxchatChartData.labels);
1361 - const chatsData = Object.values(mxchatChartData.chats).map(Number);
1362 - const messagesData = Object.values(mxchatChartData.messages).map(Number);
1363 -
1364 - console.log('[MxChat Chart] Processed labels:', labels);
1365 - console.log('[MxChat Chart] Processed chatsData:', chatsData);
1366 - console.log('[MxChat Chart] Processed messagesData:', messagesData);
1367 -
1368 - // Create chart
1369 - try {
1370 - canvas.chartInstance = new SimpleChart(canvas, {
1371 - labels: labels,
1372 - datasets: [
1373 - {
1374 - label: 'Chats',
1375 - data: chatsData,
1376 - borderColor: '#667eea',
1377 - backgroundColor: chatsGradient,
1378 - fill: true
1379 - },
1380 - {
1381 - label: 'Messages',
1382 - data: messagesData,
1383 - borderColor: '#764ba2',
1384 - backgroundColor: messagesGradient,
1385 - fill: true
1386 - }
1387 - ]
1388 - });
1389 - console.log('[MxChat Chart] Chart created successfully');
1390 - } catch (error) {
1391 - console.error('[MxChat Chart] Error creating chart:', error);
1392 - }
1393 - }
1394 -
1395 - // Initialize chart on page load (dashboard is shown by default)
1396 - setTimeout(function() {
1397 - initActivityChart();
1398 - }, 100);
1399 -
1400 - // Reinitialize chart on window resize
1401 - let resizeTimeout;
1402 - $(window).on('resize', function() {
1403 - clearTimeout(resizeTimeout);
1404 - resizeTimeout = setTimeout(function() {
1405 - initActivityChart();
1406 - }, 250);
1407 - });
1408 -
1409 - // ==========================================================================
1410 - // Leads Tab
1411 - // ==========================================================================
1412 -
1413 - const leadsState = {
1414 - loaded: false,
1415 - page: 1,
1416 - perPage: 25,
1417 - totalPages: 1,
1418 - totalCount: 0,
1419 - selected: new Set(),
1420 - filters: {
1421 - search: '',
1422 - dateRange: 'all',
1423 - status: 'all',
1424 - pageUrl: '',
1425 - pageTitle: ''
1426 - },
1427 - pendingDelete: [],
1428 - leadsRows: [] // last-rendered rows for quick lookup
1429 - };
1430 -
1431 - function $leads() { return $('#leads'); }
1432 -
1433 - // Called after a transcript delete from the All Chats side. Marks the Leads tab
1434 - // data stale so the next tab visit re-fetches, and refreshes immediately if the
1435 - // Leads tab happens to already be visible.
1436 - function invalidateLeadsData() {
1437 - leadsState.loaded = false;
1438 - if ($('#leads').hasClass('active')) {
1439 - loadLeads(1);
1440 - }
1441 - }
1442 -
1443 - function escapeHtmlLeads(s) {
1444 - if (s === null || typeof s === 'undefined') return '';
1445 - return String(s)
1446 - .replace(/&/g, '&amp;')
1447 - .replace(/</g, '&lt;')
1448 - .replace(/>/g, '&gt;')
1449 - .replace(/"/g, '&quot;')
1450 - .replace(/'/g, '&#039;');
1451 - }
1452 -
1453 - function leadsFiltersActive() {
1454 - const f = leadsState.filters;
1455 - return f.search !== '' || f.dateRange !== 'all' || f.status !== 'all' || f.pageUrl !== '';
1456 - }
1457 -
1458 - function updateClearFiltersButton() {
1459 - if (leadsFiltersActive()) {
1460 - $('#mxch-leads-clear-filters').show();
1461 - } else {
1462 - $('#mxch-leads-clear-filters').hide();
1463 - }
1464 - }
1465 -
1466 - function setPageFilterChip(url, title) {
1467 - leadsState.filters.pageUrl = url || '';
1468 - leadsState.filters.pageTitle = title || url || '';
1469 - const $chip = $('#mxch-leads-active-page-filter');
1470 - if (url) {
1471 - $chip.find('.mxch-leads-page-chip-label').text('Page: ' + (title || url));
1472 - $chip.show();
1473 - } else {
1474 - $chip.hide();
1475 - }
1476 - updateClearFiltersButton();
1477 - }
1478 -
1479 - function loadLeads(page) {
1480 - if (typeof page === 'number') leadsState.page = page;
1481 -
1482 - const $tbody = $('#mxch-leads-tbody');
1483 - $tbody.html('<tr><td colspan="6" class="mxch-leads-loading"><span class="spinner is-active"></span></td></tr>');
1484 -
1485 - $.ajax({
1486 - url: ajaxurl,
1487 - type: 'POST',
1488 - data: {
1489 - action: 'mxchat_fetch_leads',
1490 - page: leadsState.page,
1491 - per_page: leadsState.perPage,
1492 - search: leadsState.filters.search,
1493 - date_range: leadsState.filters.dateRange,
1494 - status: leadsState.filters.status,
1495 - page_url: leadsState.filters.pageUrl
1496 - },
1497 - success: function(response) {
1498 - leadsState.loaded = true;
1499 - if (!response || !response.success) {
1500 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1501 - return;
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);
1502 425 }
1503 - leadsState.totalPages = response.total_pages || 1;
1504 - leadsState.totalCount = response.total_count || 0;
1505 - leadsState.leadsRows = response.leads || [];
426 + html += '</div>';
1506 427
1507 - renderLeadsStats(response.stats || {});
1508 - renderLeadsTopPages(response.top_pages || []);
1509 - renderLeadsTable(response.leads || []);
1510 - renderLeadsCount(response.showing_start, response.showing_end, response.total_count);
1511 - renderLeadsPagination(response.page, response.total_pages);
1512 -
1513 - // Nav badge
1514 - if (response.stats && typeof response.stats.total_leads === 'number') {
1515 - const $badge = $('#mxch-leads-nav-badge');
1516 - if (response.stats.total_leads > 0) {
1517 - $badge.text(response.stats.total_leads).show();
1518 - } else {
1519 - $badge.hide();
1520 - }
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>';
1521 433 }
1522 - },
1523 - error: function() {
1524 - $tbody.html('<tr><td colspan="7" class="mxch-leads-empty">Error loading leads</td></tr>');
1525 - }
1526 - });
1527 - }
1528 434
1529 - function renderLeadsStats(stats) {
1530 - $('#mxch-leads-stat-total').text(stats.total_leads || 0);
1531 - $('#mxch-leads-stat-new').text(stats.new_this_week || 0);
1532 - $('#mxch-leads-stat-avg').text(stats.avg_convos || 0);
1533 - const pct = stats.orphan_pct || 0;
1534 - $('#mxch-leads-stat-orphan').text(pct + '%');
1535 - const orphanCount = stats.orphan_count || 0;
1536 - $('#mxch-leads-stat-orphan-sub').text(orphanCount + (orphanCount === 1 ? ' lead captured but never chatted' : ' leads captured but never chatted'));
1537 - }
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 + '">';
1538 439
1539 - function renderLeadsTopPages(pages) {
1540 - const $wrap = $('#mxch-leads-toppages-list');
1541 - if (!pages || pages.length === 0) {
1542 - $wrap.html('<div class="mxch-leads-empty-mini">No page data yet.</div>');
1543 - return;
1544 - }
1545 - let html = '';
1546 - pages.forEach(function(p) {
1547 - const isActive = leadsState.filters.pageUrl === p.url ? ' is-active' : '';
1548 - html += `
1549 - <button type="button" class="mxch-leads-toppage-row${isActive}" data-url="${escapeHtmlLeads(p.url)}" data-title="${escapeHtmlLeads(p.title)}">
1550 - <span class="mxch-leads-toppage-title">${escapeHtmlLeads(p.title || p.url)}</span>
1551 - <span class="mxch-leads-toppage-count">${p.lead_count}</span>
1552 - </button>
1553 - `;
1554 - });
1555 - $wrap.html(html);
1556 - }
440 + // Sort chunks by index
441 + const sortedChunks = group.matchedChunks.slice().sort(function(a, b) {
442 + return (a.chunkIndex || 0) - (b.chunkIndex || 0);
443 + });
1557 444
1558 - function renderLeadsTable(rows) {
1559 - const $tbody = $('#mxch-leads-tbody');
1560 - if (!rows || rows.length === 0) {
1561 - $tbody.html(`
1562 - <tr><td colspan="6" class="mxch-leads-empty">
1563 - <div class="mxch-leads-empty-wrap">
1564 - <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
1565 - <p>No leads match the current filters.</p>
1566 - </div>
1567 - </td></tr>
1568 - `);
1569 - return;
1570 - }
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 ? '✓' : '○';
1571 449
1572 - let html = '';
1573 - rows.forEach(function(r) {
1574 - const emailKey = (r.email || '').toLowerCase();
1575 - const isChecked = leadsState.selected.has(emailKey) ? ' checked' : '';
1576 - // Status: 'active' (has conversations), 'chat_deleted' (admin removed the chat), 'orphan' (no chat ever).
1577 - const status = r.status || (r.is_orphan ? 'orphan' : 'active');
1578 - const isOrphan = (status === 'orphan');
1579 - const isChatDeleted = (status === 'chat_deleted');
1580 - const nameLine = r.name
1581 - ? `<span class="mxch-leads-lead-name">${escapeHtmlLeads(r.name)}</span>`
1582 - : '';
1583 - const leadCell = `
1584 - <div class="mxch-leads-lead-cell">
1585 - <span class="mxch-leads-lead-email" title="${escapeHtmlLeads(r.email)}">${escapeHtmlLeads(r.email)}</span>
1586 - ${nameLine}
1587 - </div>`;
1588 - let countCell;
1589 - if (isOrphan) {
1590 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-orphan">Orphan</span>`;
1591 - } else if (isChatDeleted) {
1592 - countCell = `<span class="mxch-leads-pill mxch-leads-pill-deleted" title="Chat was deleted by an admin">Chat deleted</span>`;
1593 - } else {
1594 - countCell = `<span class="mxch-leads-pill">${r.conversation_count}</span>`;
1595 - }
1596 - const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : ''));
1597 - const pageCell = r.top_page_url
1598 - ? `<a href="${escapeHtmlLeads(r.top_page_url)}" target="_blank" rel="noopener" class="mxch-leads-page-link" title="${escapeHtmlLeads(r.top_page_url)}">${escapeHtmlLeads(r.top_page_title || r.top_page_url)}</a>`
1599 - : '<span class="mxch-leads-muted">—</span>';
1600 - // View Convo only for active leads (orphans and chat_deleted have no viewable session).
1601 - const viewBtn = (status === 'active' && r.latest_session_id)
1602 - ? `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-leads-view" data-session-id="${escapeHtmlLeads(r.latest_session_id)}" title="View latest conversation">
1603 - <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
1604 - <span>View convo</span>
1605 - </button>`
1606 - : '';
1607 - const deleteBtn = `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-btn-danger-ghost mxch-leads-delete-row" data-email="${escapeHtmlLeads(r.email)}" title="Delete lead">
1608 - <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
1609 - </button>`;
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 + });
1610 456
1611 - const rowStateClass = isOrphan ? ' is-orphan' : (isChatDeleted ? ' is-chat-deleted' : '');
1612 - html += `
1613 - <tr class="mxch-leads-row${rowStateClass}" data-email="${escapeHtmlLeads(r.email)}">
1614 - <td class="mxch-leads-col-check"><input type="checkbox" class="mxch-leads-rowcheck"${isChecked}></td>
1615 - <td class="mxch-leads-col-lead">${leadCell}</td>
1616 - <td class="mxch-leads-col-count">${countCell}</td>
1617 - <td class="mxch-leads-col-last">${lastCell}</td>
1618 - <td class="mxch-leads-col-page">${pageCell}</td>
1619 - <td class="mxch-leads-col-actions">${viewBtn}${deleteBtn}</td>
1620 - </tr>
1621 - `;
1622 - });
457 + html += '</div>';
458 + }
1623 459
1624 - $tbody.html(html);
1625 - updateLeadsSelectionUI();
1626 - }
460 + html += '</div>';
461 + });
1627 462
1628 - function renderLeadsCount(start, end, total) {
1629 - if (!total) {
1630 - $('#mxch-leads-count').text('0 leads');
463 + html += '</div>';
1631 464 } else {
1632 - $('#mxch-leads-count').text(start + '-' + end + ' / ' + total + ' leads');
465 + html += '<div class="mxchat-rag-no-matches">No document matches found for this response.</div>';
1633 466 }
1634 - }
1635 467
1636 - function renderLeadsPagination(currentPage, totalPages) {
1637 - const $c = $('#mxch-leads-pagination');
1638 - if (!totalPages || totalPages <= 1) { $c.html(''); return; }
1639 - let html = '<div class="mxch-pagination-btns">';
1640 - if (currentPage > 1) {
1641 - html += `<button class="mxch-page-btn" data-page="${currentPage - 1}">&laquo;</button>`;
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>';
477 + html += '</div>';
1642 478 }
1643 - html += `<span class="mxch-page-info">${currentPage} / ${totalPages}</span>`;
1644 - if (currentPage < totalPages) {
1645 - html += `<button class="mxch-page-btn" data-page="${currentPage + 1}">&raquo;</button>`;
1646 - }
1647 - html += '</div>';
1648 - $c.html(html);
1649 - }
1650 479
1651 - function updateLeadsSelectionUI() {
1652 - const count = leadsState.selected.size;
1653 - const $countEl = $('#mxch-leads-selected-count');
1654 - const $del = $('#mxch-leads-delete-selected');
1655 - if (count > 0) {
1656 - $countEl.text(count + ' selected').addClass('has-selection');
1657 - $del.prop('disabled', false);
1658 - } else {
1659 - $countEl.text('0').removeClass('has-selection');
1660 - $del.prop('disabled', true);
1661 - }
1662 - // Selected-scope export menu items
1663 - $('#mxch-leads-export-menu button[data-scope="selected"]').prop('disabled', count === 0);
480 + $container.html(html);
1664 481
1665 - // Select-all checkbox state
1666 - const $checks = $('.mxch-leads-rowcheck');
1667 - const checked = $checks.filter(':checked').length;
1668 - const total = $checks.length;
1669 - $('#mxch-leads-select-all').prop('checked', total > 0 && checked === total);
1670 - $('#mxch-leads-select-all').prop('indeterminate', checked > 0 && checked < total);
1671 - }
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');
1672 487
1673 - // Trigger leads load when switching to the tab (works alongside the main nav handler above).
1674 - $('.mxch-nav-link[data-target="leads"], .mxch-mobile-nav-link[data-target="leads"]').on('click', function() {
1675 - if (!leadsState.loaded) {
1676 - loadLeads(1);
1677 - }
1678 - });
1679 -
1680 - // Filter: search (debounced)
1681 - let leadsSearchTimer;
1682 - $('#mxch-leads-search').on('input', function() {
1683 - clearTimeout(leadsSearchTimer);
1684 - const val = $(this).val();
1685 - leadsSearchTimer = setTimeout(function() {
1686 - leadsState.filters.search = (val || '').trim();
1687 - updateClearFiltersButton();
1688 - loadLeads(1);
1689 - }, 300);
1690 - });
1691 -
1692 - // Filter: date range
1693 - $('#mxch-leads-date-range').on('change', function() {
1694 - leadsState.filters.dateRange = $(this).val();
1695 - updateClearFiltersButton();
1696 - loadLeads(1);
1697 - });
1698 -
1699 - // Filter: status
1700 - $('#mxch-leads-status').on('change', function() {
1701 - leadsState.filters.status = $(this).val();
1702 - updateClearFiltersButton();
1703 - loadLeads(1);
1704 - });
1705 -
1706 - // Clear filters
1707 - $('#mxch-leads-clear-filters').on('click', function() {
1708 - leadsState.filters = { search: '', dateRange: 'all', status: 'all', pageUrl: '', pageTitle: '' };
1709 - $('#mxch-leads-search').val('');
1710 - $('#mxch-leads-date-range').val('all');
1711 - $('#mxch-leads-status').val('all');
1712 - setPageFilterChip('', '');
1713 - loadLeads(1);
1714 - });
1715 -
1716 - // Remove page chip
1717 - $leads().on('click', '.mxch-leads-page-chip-remove', function() {
1718 - setPageFilterChip('', '');
1719 - loadLeads(1);
1720 - });
1721 -
1722 - // Top Pages click -> set filter
1723 - $leads().on('click', '.mxch-leads-toppage-row', function() {
1724 - const url = $(this).data('url') || '';
1725 - const title = $(this).data('title') || '';
1726 - setPageFilterChip(url, title);
1727 - loadLeads(1);
1728 - });
1729 -
1730 - // Pagination click
1731 - $leads().on('click', '#mxch-leads-pagination .mxch-page-btn', function() {
1732 - const p = parseInt($(this).data('page'), 10);
1733 - if (p > 0) loadLeads(p);
1734 - });
1735 -
1736 - // Select-all
1737 - $('#mxch-leads-select-all').on('change', function() {
1738 - const on = $(this).is(':checked');
1739 - $('.mxch-leads-rowcheck').prop('checked', on);
1740 - $('.mxch-leads-row').each(function() {
1741 - const email = ($(this).data('email') || '').toString().toLowerCase();
1742 - if (on) {
1743 - leadsState.selected.add(email);
488 + if (isExpanded) {
489 + $details.slideUp(200);
490 + $(this).text('▶ Show ' + $details.find('.mxchat-rag-chunk-row').length + ' matched chunks');
1744 491 } else {
1745 - leadsState.selected.delete(email);
492 + $details.slideDown(200);
493 + $(this).text('▼ Hide chunks');
1746 494 }
1747 495 });
1748 - updateLeadsSelectionUI();
1749 - });
1750 -
1751 - // Row checkbox
1752 - $leads().on('change', '.mxch-leads-rowcheck', function() {
1753 - const email = ($(this).closest('.mxch-leads-row').data('email') || '').toString().toLowerCase();
1754 - if ($(this).is(':checked')) {
1755 - leadsState.selected.add(email);
1756 - } else {
1757 - leadsState.selected.delete(email);
1758 - }
1759 - updateLeadsSelectionUI();
1760 - });
1761 -
1762 - // View convo -> jump to All Chats tab and open the session
1763 - $leads().on('click', '.mxch-leads-view', function() {
1764 - const sid = $(this).attr('data-session-id');
1765 - if (!sid) return;
1766 - $('.mxch-nav-link[data-target="all-chats"]').trigger('click');
1767 - // selectChat is defined earlier in this closure
1768 - if (typeof selectChat === 'function') {
1769 - setTimeout(function() { selectChat(sid); }, 30);
1770 - }
1771 - });
1772 -
1773 - // Row delete -> confirm for one
1774 - $leads().on('click', '.mxch-leads-delete-row', function() {
1775 - const email = $(this).data('email');
1776 - if (!email) return;
1777 - openLeadsConfirm([String(email)]);
1778 - });
1779 -
1780 - // Bulk delete -> confirm for N
1781 - $('#mxch-leads-delete-selected').on('click', function() {
1782 - if (leadsState.selected.size === 0) return;
1783 - openLeadsConfirm(Array.from(leadsState.selected));
1784 - });
1785 -
1786 - function openLeadsConfirm(emails) {
1787 - leadsState.pendingDelete = emails;
1788 - const count = emails.length;
1789 - const msg = count === 1
1790 - ? 'Delete lead "' + emails[0] + '" and all of their conversations?'
1791 - : 'Delete ' + count + ' leads and all of their conversations?';
1792 - $('#mxch-leads-confirm-body').text(msg);
1793 - $('#mxch-leads-confirm').fadeIn(120);
1794 496 }
1795 497
1796 - function closeLeadsConfirm() {
1797 - $('#mxch-leads-confirm').fadeOut(120);
1798 - leadsState.pendingDelete = [];
498 + // Helper function to escape HTML
499 + function escapeHtml(text) {
500 + if (!text) return '';
501 + const div = document.createElement('div');
502 + div.textContent = text;
503 + return div.innerHTML;
1799 504 }
1800 505
1801 - $leads().on('click', '[data-mxch-leads-close]', closeLeadsConfirm);
1802 -
1803 - $('#mxch-leads-confirm-go').on('click', function() {
1804 - const emails = leadsState.pendingDelete.slice();
1805 - if (!emails.length) { closeLeadsConfirm(); return; }
1806 -
1807 - const $btn = $(this).prop('disabled', true).text('Deleting...');
1808 -
1809 - $.ajax({
1810 - url: ajaxurl,
1811 - type: 'POST',
1812 - data: {
1813 - action: 'mxchat_delete_leads',
1814 - security: $('#mxchat_leads_delete_nonce').val(),
1815 - emails: emails
1816 - },
1817 - success: function(response) {
1818 - $btn.prop('disabled', false).text('Delete permanently');
1819 - closeLeadsConfirm();
1820 - if (response && response.success) {
1821 - emails.forEach(function(e) { leadsState.selected.delete(e.toLowerCase()); });
1822 - loadLeads(leadsState.page);
1823 - } else {
1824 - alert((response && response.data && response.data.message) || 'Failed to delete leads.');
1825 - }
1826 - },
1827 - error: function() {
1828 - $btn.prop('disabled', false).text('Delete permanently');
1829 - alert('Network error while deleting.');
1830 - }
1831 - });
506 + // Close RAG context modal
507 + $('.mxchat-rag-modal-close').on('click', function() {
508 + $('#mxchat-rag-context-modal').fadeOut(300);
1832 509 });
1833 510
1834 - // Export dropdown
1835 - $('#mxch-leads-export-btn').on('click', function(e) {
1836 - e.stopPropagation();
1837 - $('#mxch-leads-export-menu').toggleClass('is-open');
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);
515 + }
1838 516 });
1839 517
1840 - $(document).on('click', function() {
1841 - $('#mxch-leads-export-menu').removeClass('is-open');
1842 - });
1843 -
1844 - $('#mxch-leads-export-menu').on('click', function(e) { e.stopPropagation(); });
1845 -
1846 - $('#mxch-leads-export-menu button').on('click', function() {
1847 - if ($(this).prop('disabled')) return;
1848 - const scope = $(this).data('scope') || 'all';
1849 - const fields = $(this).data('fields') || 'email_and_name';
1850 - submitLeadsExport(scope, fields);
1851 - $('#mxch-leads-export-menu').removeClass('is-open');
1852 - });
1853 -
1854 - function submitLeadsExport(scope, fields) {
1855 - const $form = $('<form>', { method: 'POST', action: ajaxurl, style: 'display:none;' });
1856 - $form.append($('<input>', { type: 'hidden', name: 'action', value: 'mxchat_export_leads' }));
1857 - $form.append($('<input>', { type: 'hidden', name: 'security', value: $('#mxchat_leads_export_nonce').val() }));
1858 - $form.append($('<input>', { type: 'hidden', name: 'scope', value: scope }));
1859 - $form.append($('<input>', { type: 'hidden', name: 'fields', value: fields }));
1860 - if (scope === 'selected') {
1861 - Array.from(leadsState.selected).forEach(function(e) {
1862 - $form.append($('<input>', { type: 'hidden', name: 'emails[]', value: e }));
1863 - });
518 + // Close modal on Escape key
519 + $(document).on('keydown', function(e) {
520 + if (e.key === 'Escape') {
521 + $('#mxchat-rag-context-modal').fadeOut(300);
1864 522 }
1865 - $form.appendTo('body').submit().remove();
1866 - }
1867 -
1868 - // Preload leads metadata on page load (for the nav badge count only) without rendering.
1869 - // We keep this light — the full fetch only runs when the tab is clicked.
1870 - $.ajax({
1871 - url: ajaxurl,
1872 - type: 'POST',
1873 - data: { action: 'mxchat_fetch_leads', page: 1, per_page: 1 },
1874 - success: function(response) {
1875 - if (response && response.success && response.stats) {
1876 - const total = response.stats.total_leads || 0;
1877 - const $badge = $('#mxch-leads-nav-badge');
1878 - if (total > 0) $badge.text(total).show();
1879 - }
1880 - }
1881 523 });
1882 -});
524 +});