PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.24
MxChat – AI Chatbot & Content Generation for WordPress v3.2.24
3.2.25 3.2.24 3.2.23 3.2.22 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 All 156 releases
mxchat-basic / js / knowledge-processing.js

knowledge-processing.js in MxChat – AI Chatbot & Content Generation for WordPress 3.2.24, at js/knowledge-processing.js

2,414 lines 102.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 jQuery(document).ready(function($) {
2 // ========================================
3 // UNIQUE URL GENERATOR FOR DIRECT CONTENT
4 // ========================================
5
6 // Show/hide generate button based on checkbox
7 $('#mxchat-unique-url-toggle').on('change', function() {
8 const $button = $('#mxchat-generate-unique-url');
9 const $urlInput = $('#article_url');
10
11 if ($(this).is(':checked')) {
12 $button.show();
13 // If URL field is empty and checkbox is enabled, generate immediately
14 if (!$urlInput.val()) {
15 generateUniqueUrl();
16 }
17 } else {
18 $button.hide();
19 }
20 });
21
22 // Generate unique URL when button is clicked
23 $('#mxchat-generate-unique-url').on('click', function() {
24 generateUniqueUrl();
25 });
26
27 // Function to generate unique URL
28 function generateUniqueUrl() {
29 const $urlInput = $('#article_url');
30 let baseUrl = $urlInput.val().trim();
31
32 // If no URL provided, use a default
33 if (!baseUrl) {
34 baseUrl = window.location.origin;
35 }
36
37 // Split URL into base and hash fragment
38 let hashFragment = '';
39 if (baseUrl.includes('#')) {
40 const parts = baseUrl.split('#');
41 baseUrl = parts[0];
42 hashFragment = '#' + parts[1];
43 }
44
45 // Remove any existing ref parameter (matches ref=anything up to & or end of string)
46 baseUrl = baseUrl.replace(/[?&]ref=[^&]+(&|$)/, function(match, ending) {
47 // If it ends with &, keep it; otherwise remove the whole thing
48 return ending === '&' ? '&' : '';
49 });
50
51 // Clean up any trailing ? or & from URL
52 baseUrl = baseUrl.replace(/[?&]$/, '');
53
54 // Generate unique reference (timestamp only for cleaner URLs)
55 const timestamp = Date.now();
56 const uniqueRef = timestamp;
57
58 // Add the unique reference as a query parameter (before hash)
59 const separator = baseUrl.includes('?') ? '&' : '?';
60 const uniqueUrl = baseUrl + separator + 'ref=' + uniqueRef + hashFragment;
61
62 // Update the input field
63 $urlInput.val(uniqueUrl);
64
65 // Visual feedback
66 $urlInput.css('background-color', '#e7f7e7');
67 setTimeout(function() {
68 $urlInput.css('background-color', '');
69 }, 1000);
70 }
71
72 // ========================================
73 // QUEUE PROCESSING SYSTEM
74 // ========================================
75
76 let isProcessingQueue = false;
77 let currentQueueId = null;
78 let currentQueueType = null;
79
80 // Process 5 items at a time
81 const BATCH_SIZE = 5;
82
83 // Check if we should start queue processing on page load
84 checkForActiveQueues();
85
86 // Form submission handler - triggers queue processing
87 $('#mxchat-url-form').on('submit', function(e) {
88 //console.log('MxChat: Form submitted, queue will be created');
89
90 // Don't prevent default - let form submit normally
91 // But schedule a check after redirect
92 localStorage.setItem('mxchat_check_queue_after_submit', Date.now().toString());
93 });
94
95 // Check if we just submitted a form and need to start processing
96 const justSubmitted = localStorage.getItem('mxchat_check_queue_after_submit');
97 if (justSubmitted) {
98 const submitTime = parseInt(justSubmitted);
99 const now = Date.now();
100
101 // If submitted within last 10 seconds, wait for queue to be created
102 if (now - submitTime < 10000) {
103 //console.log('MxChat: Form was just submitted, waiting for queue creation...');
104 localStorage.removeItem('mxchat_check_queue_after_submit');
105
106 // Show processing message
107 if ($('.mxchat-processing-message').length === 0) {
108 const message = $('<div class="mxchat-processing-message" style="text-align: center; padding: 15px; background: #f0f7ff; border-radius: 8px; margin-top: 15px;">⏳ Queue created! Processing will start in a moment...</div>');
109 $('.mxchat-import-section').after(message);
110 }
111
112 // Check for queue multiple times with increasing delays
113 setTimeout(function() { checkForActiveQueues(); }, 1000);
114 setTimeout(function() { checkForActiveQueues(); }, 2000);
115 setTimeout(function() { checkForActiveQueues(); }, 3000);
116 setTimeout(function() { checkForActiveQueues(); }, 5000);
117 }
118 }
119
120 function checkForActiveQueues() {
121 $.ajax({
122 url: ajaxurl,
123 type: 'POST',
124 data: {
125 action: 'mxchat_get_status_updates',
126 nonce: mxchatAdmin.status_nonce
127 },
128 success: function(response) {
129 //console.log('MxChat: Checking for active queues...', response);
130
131 if (response.sitemap_queue_id && response.sitemap_status) {
132 if (response.sitemap_status.status === 'processing') {
133 //console.log('MxChat: Found active sitemap queue:', response.sitemap_queue_id);
134 startQueueProcessing(response.sitemap_queue_id, 'sitemap');
135 } else if (response.sitemap_status.status === 'complete') {
136 //console.log('MxChat: Sitemap queue already complete');
137 // ADD THIS LINE:
138 $('.mxchat-processing-message').remove();
139 // Show completed status card (no auto-refresh)
140 updateSitemapStatus(response.sitemap_status);
141 }
142 }
143
144 if (response.pdf_queue_id && response.pdf_status) {
145 if (response.pdf_status.status === 'processing') {
146 //console.log('MxChat: Found active PDF queue:', response.pdf_queue_id);
147 startQueueProcessing(response.pdf_queue_id, 'pdf');
148 } else if (response.pdf_status.status === 'complete') {
149 //console.log('MxChat: PDF queue already complete');
150 // ADD THIS LINE:
151 $('.mxchat-processing-message').remove();
152 // Show completed status card (no auto-refresh)
153 updatePdfStatus(response.pdf_status);
154 }
155 }
156
157 // ADD THIS: If no queues found at all, remove the message
158 if (!response.sitemap_queue_id && !response.pdf_queue_id) {
159 $('.mxchat-processing-message').remove();
160 }
161 },
162 error: function(xhr, status, error) {
163 console.error('MxChat: Error checking for active queues:', error);
164 // ADD THIS: Remove message on error too
165 $('.mxchat-processing-message').remove();
166 }
167 });
168 }
169
170 /**
171 * Start processing a queue
172 */
173 function startQueueProcessing(queueId, queueType) {
174 if (isProcessingQueue) {
175 //console.log('MxChat: Already processing a queue, skipping');
176 return;
177 }
178
179 isProcessingQueue = true;
180 currentQueueId = queueId;
181 currentQueueType = queueType;
182
183 //console.log('MxChat: Starting queue processing:', queueId, queueType);
184
185 // Remove any "waiting" messages
186 $('.mxchat-processing-message').remove();
187
188 // Create or update status card
189 createOrUpdateStatusCard(queueType);
190
191 // Start real-time entries polling if function exists
192 if (typeof startEntriesPolling === 'function') {
193 startEntriesPolling();
194 }
195
196 // Start the batch processing loop
197 processNextBatch();
198 }
199
200 /**
201 * Process the next batch of items (5 at a time)
202 */
203 function processNextBatch() {
204 if (!isProcessingQueue) {
205 //console.log('MxChat: Processing stopped');
206 return;
207 }
208
209 // Fetch the next batch of items
210 const fetchPromises = [];
211
212 for (let i = 0; i < BATCH_SIZE; i++) {
213 const promise = $.ajax({
214 url: ajaxurl,
215 type: 'POST',
216 data: {
217 action: 'mxchat_get_next_queue_item',
218 nonce: mxchatAdmin.queue_nonce,
219 queue_id: currentQueueId
220 }
221 });
222 fetchPromises.push(promise);
223 }
224
225 // Wait for all fetch requests to complete
226 Promise.all(fetchPromises).then(function(responses) {
227 // Filter out completed/error responses and extract items
228 const items = [];
229 let queueComplete = false;
230
231 for (let response of responses) {
232 if (response.success && response.data && !response.data.complete) {
233 items.push(response.data.item);
234 } else if (response.data && response.data.complete) {
235 queueComplete = true;
236 }
237 }
238
239 // If no items to process, queue is done
240 if (items.length === 0) {
241 if (queueComplete) {
242 handleQueueComplete();
243 } else {
244 verifyQueueCompletion();
245 }
246 return;
247 }
248
249 // Process all items in this batch simultaneously
250 const processPromises = items.map(item => processQueueItem(item));
251
252 // Wait for all items to finish processing
253 Promise.all(processPromises).then(function() {
254 // Update progress after batch completes
255 updateQueueProgress();
256
257 // If we got fewer items than batch size, queue might be done
258 if (items.length < BATCH_SIZE || queueComplete) {
259 verifyQueueCompletion();
260 } else {
261 // Process next batch immediately
262 processNextBatch();
263 }
264 }).catch(function(error) {
265 console.error('MxChat: Error processing batch:', error);
266 // Continue anyway
267 updateQueueProgress();
268 setTimeout(function() {
269 processNextBatch();
270 }, 1000);
271 });
272
273 }).catch(function(error) {
274 console.error('MxChat: Error fetching batch:', error);
275 // Verify queue status before retrying
276 setTimeout(function() {
277 verifyQueueCompletion();
278 }, 2000);
279 });
280 }
281
282 /**
283 * Verify if queue is actually complete
284 * Prevents infinite loops when last item fails
285 */
286 function verifyQueueCompletion() {
287 //console.log('MxChat: Verifying queue completion status...');
288
289 $.ajax({
290 url: ajaxurl,
291 type: 'POST',
292 data: {
293 action: 'mxchat_get_queue_status',
294 nonce: mxchatAdmin.queue_nonce,
295 queue_id: currentQueueId
296 },
297 success: function(response) {
298 if (response.success) {
299 const status = response.data;
300
301 // If no pending or processing items, queue is done
302 if (status.pending === 0 && status.processing === 0) {
303 //console.log('MxChat: Queue verified as complete');
304 handleQueueComplete();
305 } else {
306 // Still has items, try to continue
307 //console.log('MxChat: Queue still has pending items, continuing...');
308 processNextBatch();
309 }
310 } else {
311 // Can't verify, assume complete to prevent infinite loop
312 //console.log('MxChat: Could not verify queue status, assuming complete');
313 handleQueueComplete();
314 }
315 },
316 error: function() {
317 // Can't verify, assume complete to prevent infinite loop
318 //console.log('MxChat: Network error verifying queue, assuming complete');
319 handleQueueComplete();
320 }
321 });
322 }
323
324 /**
325 * Process a single queue item
326 * Returns a Promise that resolves when processing is complete
327 */
328 function processQueueItem(item) {
329 return $.ajax({
330 url: ajaxurl,
331 type: 'POST',
332 data: {
333 action: 'mxchat_process_queue_item',
334 nonce: mxchatAdmin.queue_nonce,
335 item_id: item.id,
336 item_type: item.type,
337 item_data: item.data,
338 bot_id: item.bot_id
339 }
340 }).then(function(response) {
341 if (response.success) {
342 // Item processed successfully
343 //console.log('MxChat: Item processed successfully:', item.id);
344 return true;
345 } else {
346 // Item failed but we KEEP GOING
347 console.warn('MxChat: Item processing failed (will continue):', item.type, item.id);
348 console.warn('MxChat: Error details:', response.data);
349 return false;
350 }
351 }).catch(function(xhr, status, error) {
352 // Network error - log it but KEEP GOING
353 console.error('MxChat: AJAX/Network error processing item:', item.id, error);
354 return false;
355 });
356 }
357
358 /**
359 * Update queue progress (fetches latest stats)
360 */
361 function updateQueueProgress() {
362 $.ajax({
363 url: ajaxurl,
364 type: 'POST',
365 data: {
366 action: 'mxchat_get_queue_status',
367 nonce: mxchatAdmin.queue_nonce,
368 queue_id: currentQueueId
369 },
370 success: function(response) {
371 if (response.success) {
372 const status = response.data;
373
374 // Update the appropriate status card
375 if (currentQueueType === 'pdf') {
376 updatePdfStatusFromQueue(status);
377 } else {
378 updateSitemapStatusFromQueue(status);
379 }
380 }
381 }
382 });
383 }
384
385 /**
386 * Handle queue completion
387 * NO AUTO-REFRESH - Show completed card with errors until dismissed
388 */
389 function handleQueueComplete() {
390 isProcessingQueue = false;
391
392 // Stop real-time entries polling if function exists
393 if (typeof stopEntriesPolling === 'function') {
394 stopEntriesPolling();
395 }
396
397 // Refresh the knowledge base table with properly grouped entries
398 refreshKnowledgeBaseTable();
399
400 // Get final status with error details
401 $.ajax({
402 url: ajaxurl,
403 type: 'POST',
404 data: {
405 action: 'mxchat_get_queue_status',
406 nonce: mxchatAdmin.queue_nonce,
407 queue_id: currentQueueId
408 },
409 success: function(response) {
410 if (response.success) {
411 const status = response.data;
412
413 // Show completed status card (NO REFRESH)
414 if (currentQueueType === 'pdf') {
415 showCompletedPdfCard(status);
416 } else {
417 showCompletedSitemapCard(status);
418 }
419
420 // Mark the queue as complete on server
421 markQueueAsComplete(currentQueueId);
422
423 // Show notification based on results
424 if (status.failed > 0) {
425 showNotification('warning',
426 `Processing completed: ${status.completed} succeeded, ${status.failed} failed. ` +
427 `Review errors below and dismiss when ready.`
428 );
429 } else {
430 showNotification('success',
431 `Processing completed successfully! All ${status.completed} items processed. ` +
432 `Dismiss the status card when ready.`
433 );
434 }
435
436 // NO AUTO-REFRESH - User must manually dismiss
437 } else {
438 // Couldn't get final status, just show generic completion
439 showNotification('success', 'Processing completed! Refresh page to see final results.');
440 }
441 },
442 error: function() {
443 // Error getting final status
444 showNotification('success', 'Processing completed! Refresh page to see final results.');
445 }
446 });
447 }
448
449 /**
450 * Show completed PDF card with full error details (NO RETRY BUTTON)
451 */
452 function showCompletedPdfCard(status) {
453 let $card = $('.mxchat-status-card:contains("PDF Processing")');
454
455 if ($card.length === 0) {
456 return;
457 }
458
459 // Remove processing UI elements
460 $card.find('.mxchat-stop-form').remove();
461 $card.find('.mxchat-status-warning').remove();
462
463 // Update header with completion badge
464 $card.find('.mxchat-status-badge').remove();
465 if (status.failed > 0) {
466 $card.find('.mxchat-status-header h4').after(
467 '<span class="mxchat-status-badge mxchat-status-warning" style="margin-left: 10px; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: normal;">⚠️ Completed with ' +
468 status.failed + ' failures - Refresh to view entries</span>'
469 );
470 } else {
471 $card.find('.mxchat-status-header h4').after(
472 '<span class="mxchat-status-badge mxchat-status-success" style="margin-left: 10px; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: normal;">✓ Complete - Refresh to view entries</span>'
473 );
474 }
475
476 // Add dismiss button
477 addDismissButton($card);
478
479 // Update progress bar to 100%
480 $card.find('.mxchat-progress-fill').css('width', '100%');
481
482 // Update details with final stats
483 let detailsHtml = '<div style="background: #f0f7ff; padding: 15px; border-radius: 4px; margin-bottom: 15px;">';
484 detailsHtml += '<h4 style="margin: 0 0 10px 0;">📊 Final Results</h4>';
485 detailsHtml += '<p style="margin: 5px 0;"><strong>Total Pages:</strong> ' + status.total + '</p>';
486 detailsHtml += '<p style="margin: 5px 0; color: #2ea44f;"><strong>✓ Successfully Processed:</strong> ' + status.completed + '</p>';
487
488 if (status.failed > 0) {
489 detailsHtml += '<p style="margin: 5px 0; color: #cf222e;"><strong>✗ Failed:</strong> ' + status.failed + '</p>';
490 }
491
492 detailsHtml += '</div>';
493
494 // Add error details if there are failures (NO RETRY BUTTON)
495 if (status.failed > 0 && status.failed_items && status.failed_items.length > 0) {
496 detailsHtml += '<div class="mxchat-error-summary" style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin-top: 15px;">';
497 detailsHtml += '<h4 style="margin: 0 0 10px 0; color: #856404;">⚠️ Failed Pages</h4>';
498
499 detailsHtml += '<div style="margin-top: 10px; max-height: 400px; overflow-y: auto;">';
500 detailsHtml += '<table style="width: 100%; border-collapse: collapse;">';
501 detailsHtml += '<thead><tr style="background: #f5f5f5;"><th style="padding: 8px; text-align: left;">Page</th><th style="padding: 8px; text-align: left;">Error</th><th style="padding: 8px; text-align: left;">Attempts</th></tr></thead>';
502 detailsHtml += '<tbody>';
503
504 status.failed_items.forEach(function(item) {
505 let data;
506 try { data = JSON.parse(item.item_data); } catch(e) { data = {}; }
507 const pageNum = data.page_number || 'Unknown';
508
509 detailsHtml += '<tr style="border-bottom: 1px solid #ddd;">';
510 detailsHtml += '<td style="padding: 8px;">Page ' + pageNum + '</td>';
511 detailsHtml += '<td style="padding: 8px; word-break: break-word;">' + (item.error_message || 'Unknown error') + '</td>';
512 detailsHtml += '<td style="padding: 8px;">' + item.attempts + '</td>';
513 detailsHtml += '</tr>';
514 });
515
516 detailsHtml += '</tbody></table>';
517 detailsHtml += '</div>';
518
519 detailsHtml += '</div>';
520 }
521
522 $card.find('.mxchat-status-details').html(detailsHtml);
523 }
524
525 /**
526 * Show completed sitemap card with full error details (NO RETRY BUTTON)
527 */
528 function showCompletedSitemapCard(status) {
529 let $card = $('.mxchat-status-card:contains("Sitemap Processing")');
530
531 if ($card.length === 0) {
532 return;
533 }
534
535 // Remove processing UI elements
536 $card.find('.mxchat-stop-form').remove();
537 $card.find('.mxchat-status-warning').remove();
538
539 // Update header with completion badge
540 $card.find('.mxchat-status-badge').remove();
541 if (status.failed > 0) {
542 $card.find('.mxchat-status-header h4').after(
543 '<span class="mxchat-status-badge mxchat-status-warning" style="margin-left: 10px; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: normal;">⚠️ Completed with ' +
544 status.failed + ' failures - Refresh to view entries</span>'
545 );
546 } else {
547 $card.find('.mxchat-status-header h4').after(
548 '<span class="mxchat-status-badge mxchat-status-success" style="margin-left: 10px; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: normal;">✓ Complete - Refresh to view entries</span>'
549 );
550 }
551
552 // Add dismiss button
553 addDismissButton($card);
554
555 // Update progress bar to 100%
556 $card.find('.mxchat-progress-fill').css('width', '100%');
557
558 // Update details with final stats
559 let detailsHtml = '<div style="background: #f0f7ff; padding: 15px; border-radius: 4px; margin-bottom: 15px;">';
560 detailsHtml += '<h4 style="margin: 0 0 10px 0;">📊 Final Results</h4>';
561 detailsHtml += '<p style="margin: 5px 0;"><strong>Total URLs:</strong> ' + status.total + '</p>';
562 detailsHtml += '<p style="margin: 5px 0; color: #2ea44f;"><strong>✓ Successfully Processed:</strong> ' + status.completed + '</p>';
563
564 if (status.failed > 0) {
565 detailsHtml += '<p style="margin: 5px 0; color: #cf222e;"><strong>✗ Failed:</strong> ' + status.failed + '</p>';
566 }
567
568 detailsHtml += '</div>';
569
570 // Add error details if there are failures (NO RETRY BUTTON)
571 if (status.failed > 0 && status.failed_items && status.failed_items.length > 0) {
572 detailsHtml += '<div class="mxchat-error-summary" style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin-top: 15px;">';
573 detailsHtml += '<h4 style="margin: 0 0 10px 0; color: #856404;">⚠️ Failed URLs</h4>';
574
575 detailsHtml += '<div style="margin-top: 10px; max-height: 400px; overflow-y: auto;">';
576 detailsHtml += '<table style="width: 100%; border-collapse: collapse;">';
577 detailsHtml += '<thead><tr style="background: #f5f5f5;"><th style="padding: 8px; text-align: left;">URL</th><th style="padding: 8px; text-align: left;">Error</th><th style="padding: 8px; text-align: left;">Attempts</th></tr></thead>';
578 detailsHtml += '<tbody>';
579
580 status.failed_items.forEach(function(item) {
581 let data;
582 try { data = JSON.parse(item.item_data); } catch(e) { data = {}; }
583 const url = data.url || data.pdf_url || 'Unknown URL';
584 const pageInfo = data.page_number ? ' (page ' + data.page_number + ')' : '';
585 const displayUrl = (url.length > 60 ? url.substring(0, 57) + '...' : url) + pageInfo;
586
587 detailsHtml += '<tr style="border-bottom: 1px solid #ddd;">';
588 detailsHtml += '<td style="padding: 8px;"><a href="' + url + '" target="_blank" style="color: #0073aa; text-decoration: none;">' + displayUrl + '</a></td>';
589 detailsHtml += '<td style="padding: 8px; word-break: break-word;">' + (item.error_message || 'Unknown error') + '</td>';
590 detailsHtml += '<td style="padding: 8px;">' + item.attempts + '</td>';
591 detailsHtml += '</tr>';
592 });
593
594 detailsHtml += '</tbody></table>';
595 detailsHtml += '</div>';
596
597 detailsHtml += '</div>';
598 }
599
600 $card.find('.mxchat-status-details').html(detailsHtml);
601 }
602
603 /**
604 * Mark queue as complete on server side
605 * This prevents it from auto-starting on page refresh
606 */
607 function markQueueAsComplete(queueId) {
608 // This is a fire-and-forget call to update queue status
609 $.ajax({
610 url: ajaxurl,
611 type: 'POST',
612 data: {
613 action: 'mxchat_mark_queue_complete',
614 nonce: mxchatAdmin.queue_nonce,
615 queue_id: queueId
616 },
617 success: function(response) {
618 //console.log('MxChat: Queue marked as complete on server');
619 },
620 error: function() {
621 //console.log('MxChat: Could not mark queue as complete, but continuing');
622 }
623 });
624 }
625
626 /**
627 * Stop processing button handler
628 */
629 $(document).on('submit', '.mxchat-stop-form', function() {
630 //console.log('MxChat: Stop processing requested');
631 isProcessingQueue = false;
632 currentQueueId = null;
633 currentQueueType = null;
634 });
635
636 /**
637 * Create or update status card
638 */
639 function createOrUpdateStatusCard(queueType) {
640 const cardTitle = queueType === 'pdf' ? 'PDF Processing Status' : 'Sitemap Processing Status';
641 let $card = $('.mxchat-status-card:contains("' + cardTitle + '")');
642
643 if ($card.length === 0) {
644 // Create new card
645 let html = '<div class="mxchat-status-card">';
646 html += '<div class="mxchat-status-header">';
647 html += '<h4>' + cardTitle + '</h4>';
648 html += '<div class="mxchat-status-warning" style="background: #fff3cd; color: #856404; padding: 8px 12px; border-radius: 4px; font-size: 13px; margin: 10px 0;">';
649 html += '⚠️ <strong>Keep this tab open</strong> - Processing runs in your browser';
650 html += '</div>';
651 html += '<form method="post" class="mxchat-stop-form" action="' +
652 mxchatAdmin.admin_url + 'admin-post.php?action=mxchat_stop_processing">';
653 html += '<input type="hidden" name="mxchat_stop_processing_nonce" value="' +
654 mxchatAdmin.stop_nonce + '">';
655 html += '<button type="submit" name="stop_processing" class="mxchat-button-secondary">';
656 html += 'Stop Processing</button></form>';
657 html += '</div>';
658 html += '<div class="mxchat-progress-bar">';
659 html += '<div class="mxchat-progress-fill" style="width: 0%"></div>';
660 html += '</div>';
661 html += '<div class="mxchat-status-details">';
662 html += '<p>Initializing...</p>';
663 html += '</div>';
664 html += '</div>';
665
666 // Insert card
667 let $importSection = $('.mxchat-import-section');
668 if ($importSection.length > 0) {
669 $importSection.after($(html));
670 }
671 }
672 }
673
674 /**
675 * Update PDF status from queue data (DURING PROCESSING)
676 */
677 function updatePdfStatusFromQueue(status) {
678 let $card = $('.mxchat-status-card:contains("PDF Processing")');
679
680 if ($card.length === 0) {
681 return;
682 }
683
684 // Update progress bar
685 $card.find('.mxchat-progress-fill').css('width', status.percentage + '%');
686
687 // Update details
688 let detailsHtml = '<p>Progress: ' + (status.completed + status.failed) + ' of ' +
689 status.total + ' pages (' + status.percentage + '%)</p>';
690
691 if (status.completed > 0) {
692 detailsHtml += '<p><strong>✓ Processed successfully:</strong> ' + status.completed + '</p>';
693 }
694
695 if (status.failed > 0) {
696 detailsHtml += '<p class="error-count"><strong>✗ Failed pages:</strong> ' + status.failed + '</p>';
697 }
698
699 detailsHtml += '<p><strong>Status:</strong> Processing</p>';
700
701 $card.find('.mxchat-status-details').html(detailsHtml);
702 }
703
704 /**
705 * Update sitemap status from queue data (DURING PROCESSING)
706 */
707 function updateSitemapStatusFromQueue(status) {
708 let $card = $('.mxchat-status-card:contains("Sitemap Processing")');
709
710 if ($card.length === 0) {
711 return;
712 }
713
714 // Update progress bar
715 $card.find('.mxchat-progress-fill').css('width', status.percentage + '%');
716
717 // Update details
718 let detailsHtml = '<p>Progress: ' + (status.completed + status.failed) + ' of ' +
719 status.total + ' URLs (' + status.percentage + '%)</p>';
720
721 if (status.completed > 0) {
722 detailsHtml += '<p><strong>✓ Processed successfully:</strong> ' + status.completed + '</p>';
723 }
724
725 if (status.failed > 0) {
726 detailsHtml += '<p class="error-count"><strong>✗ Failed URLs:</strong> ' + status.failed + '</p>';
727 }
728
729 detailsHtml += '<p><strong>Status:</strong> Processing</p>';
730
731 $card.find('.mxchat-status-details').html(detailsHtml);
732 }
733
734 // ========================================
735 // STATUS UPDATES FOR COMPLETED QUEUES (FROM SERVER)
736 // ========================================
737
738 /**
739 * Dismiss completed status button handler
740 */
741 $(document).on('click', '.mxchat-dismiss-button', function() {
742 const $button = $(this);
743 const $card = $button.closest('.mxchat-status-card');
744
745 let cardType = $card.data('card-type');
746 if (!cardType) {
747 cardType = $card.find('h4').text().includes('PDF') ? 'pdf' : 'sitemap';
748 }
749
750 $card.fadeOut(300, function() {
751 $(this).remove();
752 });
753
754 $.ajax({
755 url: ajaxurl,
756 type: 'POST',
757 data: {
758 action: 'mxchat_clear_queue',
759 nonce: mxchatAdmin.queue_nonce,
760 queue_id: $card.data('queue-id') || ''
761 },
762 success: function(response) {
763 //console.log('MxChat: Queue cleared');
764 }
765 });
766 });
767
768 /**
769 * Update PDF status card (for already completed queues on page load)
770 */
771 function updatePdfStatus(status) {
772 let $pdfCard = $('.mxchat-status-card:contains("PDF Processing")');
773
774 if ($pdfCard.length === 0 && status) {
775 createPdfStatusCard(status);
776 $pdfCard = $('.mxchat-status-card:contains("PDF Processing")');
777 }
778
779 if ($pdfCard.length > 0 && status.status === 'complete') {
780 // Show as completed (same as showCompletedPdfCard but from server data)
781 showCompletedPdfCard(status);
782 }
783 }
784
785 /**
786 * Update sitemap status card (for already completed queues on page load)
787 */
788 function updateSitemapStatus(status) {
789 let $sitemapCard = $('.mxchat-status-card:contains("Sitemap Processing")');
790
791 if ($sitemapCard.length === 0 && status) {
792 createSitemapStatusCard(status);
793 $sitemapCard = $('.mxchat-status-card:contains("Sitemap Processing")');
794 }
795
796 if ($sitemapCard.length > 0 && status.status === 'complete') {
797 // Show as completed (same as showCompletedSitemapCard but from server data)
798 showCompletedSitemapCard(status);
799 }
800 }
801
802 /**
803 * Create PDF status card
804 */
805 function createPdfStatusCard(status) {
806 let html = '<div class="mxchat-status-card" data-queue-id="' + (status.queue_id || '') + '">';
807 html += '<div class="mxchat-status-header">';
808 html += '<h4>PDF Processing Status</h4>';
809 html += '</div>';
810 html += '<div class="mxchat-progress-bar">';
811 html += '<div class="mxchat-progress-fill" style="width: ' + status.percentage + '%"></div>';
812 html += '</div>';
813 html += '<div class="mxchat-status-details">';
814 html += '<p>Progress: ' + status.processed_pages + ' of ' + status.total_pages + ' pages</p>';
815 html += '</div>';
816 html += '</div>';
817
818 $('.mxchat-import-section').after($(html));
819 }
820
821 /**
822 * Create sitemap status card
823 */
824 function createSitemapStatusCard(status) {
825 let html = '<div class="mxchat-status-card" data-queue-id="' + (status.queue_id || '') + '">';
826 html += '<div class="mxchat-status-header">';
827 html += '<h4>Sitemap Processing Status</h4>';
828 html += '</div>';
829 html += '<div class="mxchat-progress-bar">';
830 html += '<div class="mxchat-progress-fill" style="width: ' + status.percentage + '%"></div>';
831 html += '</div>';
832 html += '<div class="mxchat-status-details">';
833 html += '<p>Progress: ' + status.processed_urls + ' of ' + status.total_urls + ' URLs</p>';
834 html += '</div>';
835 html += '</div>';
836
837 $('.mxchat-import-section').after($(html));
838 }
839
840 /**
841 * Add dismiss button to completed cards
842 */
843 function addDismissButton($card) {
844 if ($card.find('.mxchat-dismiss-button').length === 0) {
845 const dismissButton = $('<button type="button" class="mxchat-dismiss-button" style="padding: 6px 12px; background: #666; color: white; border: none; border-radius: 3px; cursor: pointer; margin-left: 10px;">Dismiss</button>');
846 $card.find('.mxchat-status-header').append(dismissButton);
847 }
848 }
849
850 /**
851 * Show notification helper
852 */
853 function showNotification(type, message) {
854 const $notification = $('<div class="mxchat-kb-notification ' + type + '">' + message + '</div>');
855 $('.mxchat-content, body').first().prepend($notification);
856
857 setTimeout(function() {
858 $notification.fadeOut(300, function() {
859 $(this).remove();
860 });
861 }, 5000);
862 }
863
864 // ========================================
865 // ROLE-BASED CONTENT RESTRICTIONS (Keep existing code)
866 // ========================================
867
868 if ($('#mxchat-mappings-container').length > 0) {
869 loadTagRoleMappings();
870 }
871
872 $('#mxchat-add-tag-role').on('click', function() {
873 const tagSlug = $('#mxchat-tag-input').val().trim();
874 const roleRestriction = $('#mxchat-role-select').val();
875
876 if (!tagSlug) {
877 alert('Please enter a tag name');
878 return;
879 }
880
881 const $btn = $(this);
882 $btn.prop('disabled', true).html('<span class="dashicons dashicons-update-alt"></span> Adding...');
883
884 $.ajax({
885 url: ajaxurl,
886 type: 'POST',
887 data: {
888 action: 'mxchat_add_tag_role_mapping',
889 nonce: mxchatAdmin.settings_nonce,
890 tag_slug: tagSlug,
891 role_restriction: roleRestriction
892 },
893 success: function(response) {
894 if (response.success) {
895 $('#mxchat-tag-input').val('');
896 $('#mxchat-role-select').val('public');
897 loadTagRoleMappings();
898 showNotification('success', 'Tag-role mapping added successfully!');
899 } else {
900 alert('Error: ' + response.data);
901 }
902 $btn.prop('disabled', false).html('<span class="dashicons dashicons-plus-alt"></span> Add Mapping');
903 },
904 error: function() {
905 alert('Network error occurred');
906 $btn.prop('disabled', false).html('<span class="dashicons dashicons-plus-alt"></span> Add Mapping');
907 }
908 });
909 });
910
911 $(document).on('click', '.mxchat-delete-mapping', function() {
912 if (!confirm('Are you sure you want to delete this mapping?')) {
913 return;
914 }
915
916 const $btn = $(this);
917 const $row = $btn.closest('tr');
918 const tagSlug = $btn.data('tag-slug');
919
920 $btn.html('<span class="dashicons dashicons-update-alt"></span> Deleting...');
921 $row.addClass('mxchat-row-deleting');
922
923 $.ajax({
924 url: ajaxurl,
925 type: 'POST',
926 data: {
927 action: 'mxchat_delete_tag_role_mapping',
928 nonce: mxchatAdmin.settings_nonce,
929 tag_slug: tagSlug
930 },
931 success: function(response) {
932 if (response.success) {
933 $row.fadeOut(300, function() {
934 $(this).remove();
935 if ($('.mxchat-mappings-table tbody tr').length === 0) {
936 $('.mxchat-mappings-table').hide();
937 $('#mxchat-no-mappings').show();
938 }
939 });
940 showNotification('success', 'Mapping deleted successfully!');
941 } else {
942 alert('Error: ' + response.data);
943 $btn.html('<span class="dashicons dashicons-trash"></span> Delete');
944 $row.removeClass('mxchat-row-deleting');
945 }
946 },
947 error: function() {
948 alert('Network error occurred');
949 $btn.html('<span class="dashicons dashicons-trash"></span> Delete');
950 $row.removeClass('mxchat-row-deleting');
951 }
952 });
953 });
954
955 $('#mxchat-bulk-update-roles').on('click', function() {
956 if (!confirm('This will update role restrictions for all existing content with mapped tags. Continue?')) {
957 return;
958 }
959
960 const $btn = $(this);
961 const $progress = $('#mxchat-bulk-update-progress');
962 const $result = $('#mxchat-bulk-update-result');
963
964 $progress.show();
965 $result.hide();
966 $btn.prop('disabled', true);
967
968 $progress.find('.mxchat-progress-text').text('Starting bulk update...');
969 $progress.find('.mxchat-progress-fill').css('width', '0%');
970
971 $.ajax({
972 url: ajaxurl,
973 type: 'POST',
974 data: {
975 action: 'mxchat_bulk_update_tag_roles',
976 nonce: mxchatAdmin.settings_nonce
977 },
978 success: function(response) {
979 $progress.hide();
980 $btn.prop('disabled', false);
981
982 if (response.success) {
983 $result.removeClass('error').addClass('success');
984
985 let resultHtml = '<h5>Bulk Update Complete</h5>';
986 resultHtml += '<p><strong>Total Updated:</strong> ' + response.data.updated_count + '</p>';
987 resultHtml += '<p><strong>Tags Processed:</strong> ' + response.data.tags_processed + '</p>';
988
989 if (response.data.details && response.data.details.length > 0) {
990 resultHtml += '<ul>';
991 response.data.details.forEach(function(detail) {
992 resultHtml += '<li>' + detail + '</li>';
993 });
994 resultHtml += '</ul>';
995 }
996
997 $result.html(resultHtml).show();
998 showNotification('success', 'Bulk update completed successfully!');
999 } else {
1000 $result.removeClass('success').addClass('error');
1001 $result.html('<h5>Update Failed</h5><p>' + response.data + '</p>').show();
1002 }
1003 },
1004 error: function() {
1005 $progress.hide();
1006 $btn.prop('disabled', false);
1007 $result.removeClass('success').addClass('error');
1008 $result.html('<h5>Network Error</h5><p>Please try again.</p>').show();
1009 }
1010 });
1011 });
1012
1013 function loadTagRoleMappings() {
1014 const $container = $('#mxchat-mappings-container');
1015 $container.html('<div class="mxchat-loading-mappings"><span class="mxchat-role-spinner is-active"></span> Loading mappings...</div>');
1016
1017 $.ajax({
1018 url: ajaxurl,
1019 type: 'POST',
1020 data: {
1021 action: 'mxchat_get_tag_role_mappings',
1022 nonce: mxchatAdmin.settings_nonce
1023 },
1024 success: function(response) {
1025 if (response.success && response.data.mappings.length > 0) {
1026 $('#mxchat-no-mappings').hide();
1027
1028 let html = '<table class="mxchat-mappings-table">';
1029 html += '<thead><tr><th>Tag</th><th>Role Restriction</th><th>Posts with Tag</th><th>Actions</th></tr></thead><tbody>';
1030
1031 response.data.mappings.forEach(function(mapping) {
1032 html += '<tr>';
1033 html += '<td><span class="mxchat-tag-badge"><span class="dashicons dashicons-tag"></span>' + mapping.tag_slug + '</span></td>';
1034 html += '<td><span class="mxchat-role-badge ' + mapping.role_restriction + '">' + mapping.role_label + '</span></td>';
1035 html += '<td><span class="mxchat-post-count"><span class="dashicons dashicons-admin-post"></span>' + mapping.post_count + '</span></td>';
1036 html += '<td><div class="mxchat-mapping-actions"><button class="mxchat-delete-mapping" data-tag-slug="' + mapping.tag_slug + '"><span class="dashicons dashicons-trash"></span> Delete</button></div></td>';
1037 html += '</tr>';
1038 });
1039
1040 html += '</tbody></table>';
1041 $container.html(html);
1042 } else {
1043 $container.html('');
1044 $('#mxchat-no-mappings').show();
1045 }
1046 },
1047 error: function() {
1048 $container.html('<div class="mxchat-error">Failed to load mappings. Please refresh the page.</div>');
1049 }
1050 });
1051 }
1052
1053 // ========================================
1054 // PINECONE DELETE HANDLER (Keep existing code)
1055 // ========================================
1056
1057 $(document).on('click', '.delete-button-ajax', function(e) {
1058 e.preventDefault();
1059
1060 if (!confirm('Are you sure you want to delete this entry?')) {
1061 return;
1062 }
1063
1064 var $button = $(this);
1065 var $row = $button.closest('tr');
1066 var vectorId = $button.data('vector-id');
1067 var botId = $button.data('bot-id') || 'default';
1068 var nonce = $button.data('nonce');
1069
1070 $button.prop('disabled', true);
1071 $button.find('.dashicons').removeClass('dashicons-trash').addClass('dashicons-update-alt');
1072 $row.addClass('mxchat-row-deleting');
1073
1074 $.ajax({
1075 url: ajaxurl,
1076 type: 'POST',
1077 data: {
1078 action: 'mxchat_delete_pinecone_prompt',
1079 nonce: nonce,
1080 vector_id: vectorId,
1081 bot_id: botId
1082 },
1083 success: function(response) {
1084 if (response.success) {
1085 $row.fadeOut(500, function() {
1086 $(this).remove();
1087 // Update entry count displays (header and sidebar)
1088 var $countSpan = $('#mxchat-entry-count');
1089 if ($countSpan.length) {
1090 var currentCount = readEntryCount($countSpan);
1091 if (!isNaN(currentCount)) {
1092 var newCount = Math.max(0, currentCount - 1);
1093 updateEntryCount(newCount);
1094 }
1095 }
1096 });
1097
1098 $('<div class="notice notice-success is-dismissible"><p>Entry deleted successfully from Pinecone.</p></div>')
1099 .insertAfter('.mxchat-hero')
1100 .delay(3000)
1101 .fadeOut();
1102 } else {
1103 $button.prop('disabled', false);
1104 $button.find('.dashicons').removeClass('dashicons-update-alt').addClass('dashicons-trash');
1105 $row.removeClass('mxchat-row-deleting');
1106 alert('Error: ' + response.data);
1107 }
1108 },
1109 error: function() {
1110 $button.prop('disabled', false);
1111 $button.find('.dashicons').removeClass('dashicons-update-alt').addClass('dashicons-trash');
1112 $row.removeClass('mxchat-row-deleting');
1113 alert('Network error occurred');
1114 }
1115 });
1116 });
1117
1118 // ========================================
1119 // WORDPRESS DATABASE DELETE HANDLER (AJAX)
1120 // ========================================
1121
1122 $(document).on('click', '.delete-button-wordpress', function(e) {
1123 e.preventDefault();
1124
1125 if (!confirm('Are you sure you want to delete this entry?')) {
1126 return;
1127 }
1128
1129 var $button = $(this);
1130 var $row = $button.closest('tr');
1131 var entryId = $button.data('entry-id');
1132 var nonce = $button.data('nonce');
1133
1134 $button.prop('disabled', true);
1135 $button.find('.dashicons').removeClass('dashicons-trash').addClass('dashicons-update-alt spin');
1136 $row.addClass('mxchat-row-deleting');
1137
1138 $.ajax({
1139 url: ajaxurl,
1140 type: 'POST',
1141 data: {
1142 action: 'mxchat_delete_wordpress_prompt',
1143 nonce: nonce,
1144 entry_id: entryId
1145 },
1146 success: function(response) {
1147 if (response.success) {
1148 $row.fadeOut(500, function() {
1149 $(this).remove();
1150 // Update entry count displays (header and sidebar)
1151 var $countSpan = $('#mxchat-entry-count');
1152 if ($countSpan.length) {
1153 var currentCount = readEntryCount($countSpan);
1154 if (!isNaN(currentCount)) {
1155 var newCount = Math.max(0, currentCount - 1);
1156 updateEntryCount(newCount);
1157 }
1158 }
1159 });
1160
1161 $('<div class="notice notice-success is-dismissible"><p>Entry deleted successfully.</p></div>')
1162 .insertAfter('.mxchat-hero')
1163 .delay(3000)
1164 .fadeOut();
1165 } else {
1166 $button.prop('disabled', false);
1167 $button.find('.dashicons').removeClass('dashicons-update-alt spin').addClass('dashicons-trash');
1168 $row.removeClass('mxchat-row-deleting');
1169 alert('Error: ' + response.data);
1170 }
1171 },
1172 error: function() {
1173 $button.prop('disabled', false);
1174 $button.find('.dashicons').removeClass('dashicons-update-alt spin').addClass('dashicons-trash');
1175 $row.removeClass('mxchat-row-deleting');
1176 alert('Network error occurred');
1177 }
1178 });
1179 });
1180
1181 // ========================================
1182 // BULK SELECTION FOR KNOWLEDGE ENTRIES
1183 // ========================================
1184
1185 var selectedKnowledgeEntries = new Set();
1186
1187 // Update selection UI
1188 function updateKnowledgeSelectionUI() {
1189 var count = selectedKnowledgeEntries.size;
1190 var $countEl = $('#mxchat-selected-entry-count');
1191 var $deleteBtn = $('#mxchat-delete-selected-entries');
1192 var $deleteAllForm = $('#mxchat-delete-all-form');
1193
1194 if (count > 0) {
1195 // Show Delete Selected button, hide Delete All form
1196 $deleteAllForm.hide();
1197 $deleteBtn.show();
1198 $countEl.text('(' + count + ')');
1199 } else {
1200 // Show Delete All form, hide Delete Selected button
1201 $deleteAllForm.show();
1202 $deleteBtn.hide();
1203 $countEl.text('');
1204 }
1205
1206 // Update select all checkbox state
1207 var totalItems = $('.mxchat-entry-checkbox').length;
1208 var checkedItems = $('.mxchat-entry-checkbox:checked').length;
1209 $('.mxchat-entry-checkbox-all').prop('checked', totalItems > 0 && checkedItems === totalItems);
1210 $('.mxchat-entry-checkbox-all').prop('indeterminate', checkedItems > 0 && checkedItems < totalItems);
1211 }
1212
1213 // Select all checkbox handler
1214 $(document).on('change', '.mxchat-entry-checkbox-all', function() {
1215 var isChecked = $(this).is(':checked');
1216
1217 // Sync all select-all checkboxes
1218 $('.mxchat-entry-checkbox-all').prop('checked', isChecked);
1219
1220 $('.mxchat-entry-checkbox').prop('checked', isChecked);
1221
1222 if (isChecked) {
1223 $('.mxchat-entry-checkbox').each(function() {
1224 var entryData = {
1225 id: $(this).data('entry-id'),
1226 source: $(this).data('source'),
1227 sourceUrl: $(this).data('source-url'),
1228 isGroup: $(this).data('is-group'),
1229 chunkCount: $(this).data('chunk-count') || 1
1230 };
1231 selectedKnowledgeEntries.add(JSON.stringify(entryData));
1232 $(this).closest('tr').addClass('selected');
1233 });
1234 } else {
1235 selectedKnowledgeEntries.clear();
1236 $('tr.selected').removeClass('selected');
1237 }
1238
1239 updateKnowledgeSelectionUI();
1240 });
1241
1242 // Individual checkbox handler
1243 $(document).on('change', '.mxchat-entry-checkbox', function() {
1244 var $checkbox = $(this);
1245 var $row = $checkbox.closest('tr');
1246 var entryData = {
1247 id: $checkbox.data('entry-id'),
1248 source: $checkbox.data('source'),
1249 sourceUrl: $checkbox.data('source-url'),
1250 isGroup: $checkbox.data('is-group'),
1251 chunkCount: $checkbox.data('chunk-count') || 1
1252 };
1253 var entryKey = JSON.stringify(entryData);
1254
1255 if ($checkbox.is(':checked')) {
1256 selectedKnowledgeEntries.add(entryKey);
1257 $row.addClass('selected');
1258 } else {
1259 selectedKnowledgeEntries.delete(entryKey);
1260 $row.removeClass('selected');
1261 }
1262
1263 updateKnowledgeSelectionUI();
1264 });
1265
1266 // Bulk delete button handler
1267 $(document).on('click', '#mxchat-delete-selected-entries', function() {
1268 var count = selectedKnowledgeEntries.size;
1269 if (count === 0) return;
1270
1271 if (!confirm('Are you sure you want to delete ' + count + ' selected entries? This action cannot be undone.')) {
1272 return;
1273 }
1274
1275 var $button = $(this);
1276 var nonce = $button.data('nonce');
1277 var botId = $button.data('bot-id');
1278
1279 // Parse selected entries
1280 var entries = Array.from(selectedKnowledgeEntries).map(function(entryStr) {
1281 return JSON.parse(entryStr);
1282 });
1283
1284 // Show loading state
1285 $button.prop('disabled', true);
1286 $button.find('.mxchat-bulk-delete-text').text('Deleting...');
1287 $button.find('.dashicons').removeClass('dashicons-trash').addClass('dashicons-update spin');
1288
1289 // Mark rows as deleting
1290 entries.forEach(function(entry) {
1291 $('#prompt-' + entry.id).addClass('mxchat-row-deleting');
1292 });
1293
1294 $.ajax({
1295 url: ajaxurl,
1296 type: 'POST',
1297 timeout: 120000, // 120 seconds — matches server-side set_time_limit
1298 data: {
1299 action: 'mxchat_bulk_delete_knowledge',
1300 entries: entries,
1301 bot_id: botId,
1302 nonce: nonce
1303 },
1304 success: function(response) {
1305 if (response.success) {
1306 var data = response.data;
1307
1308 // Remove successful rows
1309 if (data.success_ids && data.success_ids.length > 0) {
1310 data.success_ids.forEach(function(id) {
1311 var $row = $('#prompt-' + id);
1312 // Also remove child chunk rows if it's a group
1313 var groupId = $row.data('group-id');
1314 if (groupId) {
1315 $('.mxchat-chunk-row.' + groupId).fadeOut(300, function() {
1316 $(this).remove();
1317 });
1318 }
1319 $row.fadeOut(300, function() {
1320 $(this).remove();
1321 });
1322 });
1323 }
1324
1325 // Handle failed entries
1326 if (data.failed_ids && data.failed_ids.length > 0) {
1327 data.failed_ids.forEach(function(id) {
1328 $('#prompt-' + id).removeClass('mxchat-row-deleting').addClass('mxchat-row-error');
1329 });
1330 }
1331
1332 // Show result message
1333 var successCount = data.success_ids ? data.success_ids.length : 0;
1334 var failedCount = data.failed_ids ? data.failed_ids.length : 0;
1335 var message = 'Deleted ' + successCount + ' entries.';
1336 if (failedCount > 0) {
1337 message += ' ' + failedCount + ' entries failed to delete.';
1338 }
1339
1340 $('<div class="notice notice-success is-dismissible"><p>' + message + '</p></div>')
1341 .insertAfter('.mxchat-hero')
1342 .delay(5000)
1343 .fadeOut();
1344
1345 // Clear selection
1346 selectedKnowledgeEntries.clear();
1347 updateKnowledgeSelectionUI();
1348
1349 // Update entry count displays (header and sidebar)
1350 if (successCount > 0) {
1351 var $countSpan = $('#mxchat-entry-count');
1352 if ($countSpan.length) {
1353 var currentCount = readEntryCount($countSpan);
1354 if (!isNaN(currentCount)) {
1355 var newCount = Math.max(0, currentCount - successCount);
1356 updateEntryCount(newCount);
1357 }
1358 }
1359 }
1360
1361 } else {
1362 alert('Error: ' + (response.data || 'Unknown error'));
1363 $('tr.mxchat-row-deleting').removeClass('mxchat-row-deleting');
1364 }
1365 },
1366 error: function(jqXHR, textStatus) {
1367 var message = 'An error occurred while deleting entries.';
1368 if (textStatus === 'timeout') {
1369 message = 'The deletion request timed out. Please refresh the page to check which entries were deleted, then try again for any remaining.';
1370 } else if (textStatus === 'error' && jqXHR.status === 0) {
1371 message = 'Network error: The server took too long to respond. Please refresh and try deleting fewer entries at a time.';
1372 } else if (jqXHR.responseJSON && jqXHR.responseJSON.data) {
1373 message = 'Error: ' + jqXHR.responseJSON.data;
1374 }
1375 alert(message);
1376 $('tr.mxchat-row-deleting').removeClass('mxchat-row-deleting');
1377 },
1378 complete: function() {
1379 $button.prop('disabled', selectedKnowledgeEntries.size === 0);
1380 $button.find('.mxchat-bulk-delete-text').text('Delete Selected');
1381 $button.find('.dashicons').removeClass('dashicons-update spin').addClass('dashicons-trash');
1382 }
1383 });
1384 });
1385
1386 // Clear selection when page changes
1387 $(document).on('click', '.mxchat-page-link', function() {
1388 selectedKnowledgeEntries.clear();
1389 updateKnowledgeSelectionUI();
1390 });
1391
1392 // ========================================
1393 // AJAX PAGINATION FOR KNOWLEDGE BASE ENTRIES
1394 // ========================================
1395
1396 // Handle pagination link clicks
1397 $(document).on('click', '.mxchat-page-link', function(e) {
1398 e.preventDefault();
1399
1400 var $link = $(this);
1401 var page = $link.data('page');
1402 var $paginationWrapper = $('#mxchat-kb-pagination');
1403 var $tbody = $('#mxchat-entries-tbody');
1404
1405 if (!page || $link.hasClass('loading')) {
1406 return;
1407 }
1408
1409 // Show loading state
1410 $link.addClass('loading');
1411 $tbody.css('opacity', '0.5');
1412
1413 // Add loading indicator to pagination
1414 var $loadingIndicator = $('<span class="mxchat-pagination-loading"><span class="dashicons dashicons-update spin"></span></span>');
1415 $paginationWrapper.find('.mxchat-ajax-pagination').append($loadingIndicator);
1416
1417 // Get search and filter values from pagination wrapper
1418 var searchQuery = $paginationWrapper.data('search') || '';
1419 var contentType = $paginationWrapper.data('content-type') || '';
1420
1421 $.ajax({
1422 url: ajaxurl,
1423 type: 'POST',
1424 data: {
1425 action: 'mxchat_paginate_entries',
1426 nonce: mxchatAdmin.entries_nonce,
1427 bot_id: mxchatAdmin.bot_id || 'default',
1428 page: page,
1429 search: searchQuery,
1430 content_type: contentType
1431 },
1432 success: function(response) {
1433 $link.removeClass('loading');
1434 $tbody.css('opacity', '1');
1435 $loadingIndicator.remove();
1436
1437 if (response.success && response.data) {
1438 // Update the table body with new HTML
1439 $tbody.html(response.data.html);
1440
1441 // Update the pagination - find the inner container
1442 if (response.data.pagination_html) {
1443 // Replace the inner pagination div content
1444 $paginationWrapper.html(response.data.pagination_html);
1445 }
1446
1447 // Update data attributes on wrapper (preserve search/filter for next pagination)
1448 $paginationWrapper.attr('data-current-page', response.data.page);
1449 if (response.data.total_pages) {
1450 $paginationWrapper.attr('data-total-pages', response.data.total_pages);
1451 }
1452 // Preserve search and content_type on the wrapper from the inner pagination div
1453 var $innerPagination = $paginationWrapper.find('.mxchat-ajax-pagination');
1454 if ($innerPagination.length) {
1455 $paginationWrapper.attr('data-search', $innerPagination.data('search') || '');
1456 $paginationWrapper.attr('data-content-type', $innerPagination.data('content-type') || '');
1457 }
1458
1459 // Scroll to top of the table smoothly
1460 $('html, body').animate({
1461 scrollTop: $('#knowledge-base').offset().top - 50
1462 }, 300);
1463
1464 // Update URL hash to stay on knowledge-base tab
1465 if (window.history && window.history.replaceState) {
1466 window.history.replaceState(null, '', window.location.pathname + window.location.search + '#knowledge-base');
1467 }
1468
1469 // Show success feedback
1470 showNotification('success', 'Page ' + response.data.page + ' loaded');
1471 } else {
1472 showNotification('error', 'Failed to load page: ' + (response.data?.message || 'Unknown error'));
1473 }
1474 },
1475 error: function(xhr, status, error) {
1476 $link.removeClass('loading');
1477 $tbody.css('opacity', '1');
1478 $loadingIndicator.remove();
1479 showNotification('error', 'Network error while loading page');
1480 }
1481 });
1482 });
1483
1484 // ========================================
1485 // PINECONE REFRESH ENTRIES BUTTON
1486 // ========================================
1487
1488 $('#mxchat-refresh-pinecone-entries').on('click', function() {
1489 var $button = $(this);
1490 var $icon = $button.find('.dashicons');
1491 var $tbody = $('#mxchat-entries-tbody');
1492 var $paginationWrapper = $('#mxchat-kb-pagination');
1493
1494 // Show loading state
1495 $button.prop('disabled', true);
1496 $icon.addClass('spin');
1497 $tbody.css('opacity', '0.5');
1498
1499 $.ajax({
1500 url: ajaxurl,
1501 type: 'POST',
1502 data: {
1503 action: 'mxchat_refresh_pinecone_entries',
1504 nonce: mxchatAdmin.entries_nonce,
1505 bot_id: mxchatAdmin.bot_id || 'default',
1506 page: 1
1507 },
1508 success: function(response) {
1509 $button.prop('disabled', false);
1510 $icon.removeClass('spin');
1511 $tbody.css('opacity', '1');
1512
1513 if (response.success && response.data) {
1514 // Update the table body with new HTML
1515 $tbody.html(response.data.html);
1516
1517 // Update the pagination (same pattern as refreshKnowledgeBaseTable)
1518 if ($paginationWrapper.length) {
1519 if (response.data.pagination_html) {
1520 $paginationWrapper.html(response.data.pagination_html);
1521 $paginationWrapper.attr('style', 'padding: 16px; border-top: 1px solid var(--mxch-card-border); text-align: center;');
1522 } else {
1523 $paginationWrapper.html('');
1524 $paginationWrapper.attr('style', '');
1525 }
1526 }
1527
1528 // Update the count display
1529 if (response.data.total_count !== undefined) {
1530 updateEntryCount(response.data.total_count, response.data.count_unit);
1531 }
1532
1533 // Show success feedback
1534 showNotification('success', 'Entries refreshed successfully!');
1535 } else {
1536 showNotification('error', 'Failed to refresh entries: ' + (response.data?.message || 'Unknown error'));
1537 }
1538 },
1539 error: function(xhr, status, error) {
1540 $button.prop('disabled', false);
1541 $icon.removeClass('spin');
1542 $tbody.css('opacity', '1');
1543 showNotification('error', 'Network error while refreshing entries');
1544 }
1545 });
1546 });
1547
1548 // ========================================
1549 // ACCORDION FUNCTIONALITY
1550 // ========================================
1551
1552 // Handle expand/collapse toggle
1553 $(document).on('click', '.mxchat-expand-toggle', function(e) {
1554 e.preventDefault();
1555 e.stopPropagation();
1556
1557 const $button = $(this);
1558 const $wrapper = $button.closest('.mxchat-accordion-wrapper');
1559 const $preview = $wrapper.find('.mxchat-content-preview');
1560 const $fullContent = $wrapper.find('.mxchat-content-full');
1561
1562 // Toggle expanded state
1563 if ($fullContent.is(':visible')) {
1564 // Collapse
1565 $fullContent.slideUp(300);
1566 $button.removeClass('expanded');
1567 } else {
1568 // Expand
1569 $fullContent.slideDown(300);
1570 $button.addClass('expanded');
1571 }
1572 });
1573
1574 // Click anywhere on preview to toggle (expand or collapse)
1575 $(document).on('click', '.mxchat-content-preview', function(e) {
1576 // Only trigger if not clicking the button directly
1577 if (!$(e.target).closest('.mxchat-expand-toggle').length) {
1578 const $preview = $(this);
1579 const $wrapper = $preview.closest('.mxchat-accordion-wrapper');
1580 const $button = $preview.find('.mxchat-expand-toggle');
1581
1582 // Only trigger if there's a button (meaning content is long enough to expand)
1583 if ($button.length) {
1584 $button.trigger('click');
1585 }
1586 }
1587 });
1588
1589 // ========================================
1590 // CHUNK GROUP TOGGLE FUNCTIONALITY
1591 // ========================================
1592
1593 // Handle chunk group expand/collapse toggle
1594 $(document).on('click', '.mxchat-chunk-toggle', function(e) {
1595 e.preventDefault();
1596 e.stopPropagation();
1597
1598 const $button = $(this);
1599 const groupId = $button.data('group-id');
1600 const $chunkRows = $('.mxchat-chunk-row.' + groupId);
1601
1602 // Toggle expanded state
1603 if ($button.hasClass('expanded')) {
1604 // Collapse
1605 $chunkRows.slideUp(300);
1606 $button.removeClass('expanded');
1607 } else {
1608 // Expand
1609 $chunkRows.slideDown(300);
1610 $button.addClass('expanded');
1611 }
1612 });
1613
1614 // Handle delete button for chunk groups
1615 $(document).on('click', '.delete-button-group', function(e) {
1616 e.preventDefault();
1617 e.stopPropagation();
1618
1619 const $button = $(this);
1620 const sourceUrl = $button.data('source-url');
1621 const chunkCount = $button.data('chunk-count');
1622 const dataSource = $button.data('data-source');
1623 const botId = $button.data('bot-id');
1624 const nonce = $button.data('nonce');
1625
1626 // Confirm deletion
1627 if (!confirm('Are you sure you want to delete all ' + chunkCount + ' chunks for this URL? This action cannot be undone.')) {
1628 return;
1629 }
1630
1631 // Show loading state
1632 $button.prop('disabled', true);
1633 $button.find('.dashicons').removeClass('dashicons-trash').addClass('dashicons-update spin');
1634
1635 // Make AJAX request to delete all chunks for this URL
1636 $.ajax({
1637 url: ajaxurl,
1638 type: 'POST',
1639 data: {
1640 action: 'mxchat_delete_chunks_by_url',
1641 source_url: sourceUrl,
1642 data_source: dataSource,
1643 bot_id: botId,
1644 nonce: nonce
1645 },
1646 success: function(response) {
1647 if (response.success) {
1648 // Remove the group header row and all chunk rows
1649 const $headerRow = $button.closest('tr');
1650 const groupId = $headerRow.data('group-id');
1651 $('.mxchat-chunk-row.' + groupId).fadeOut(300, function() {
1652 $(this).remove();
1653 });
1654 $headerRow.fadeOut(300, function() {
1655 $(this).remove();
1656 });
1657 } else {
1658 var errorMsg = response.data || 'Unknown error';
1659 if (typeof response.data === 'object' && response.data.message) {
1660 errorMsg = response.data.message;
1661 }
1662 alert('Error deleting chunks: ' + errorMsg);
1663 $button.prop('disabled', false);
1664 $button.find('.dashicons').removeClass('dashicons-update spin').addClass('dashicons-trash');
1665 }
1666 },
1667 error: function(xhr, status, error) {
1668 console.error('AJAX error:', xhr.responseText);
1669 alert('Error deleting chunks: ' + (error || 'Server error'));
1670 $button.prop('disabled', false);
1671 $button.find('.dashicons').removeClass('dashicons-update spin').addClass('dashicons-trash');
1672 }
1673 });
1674 });
1675
1676 // ========================================
1677 // REAL-TIME KNOWLEDGE ENTRIES UPDATE
1678 // ========================================
1679
1680 let lastEntryId = 0;
1681 let entriesPollingInterval = null;
1682 let isEntriesPolling = false;
1683
1684 // Initialize: get the highest ID from the current table
1685 function initializeLastEntryId() {
1686 const $tbody = $('#mxchat-entries-tbody');
1687 if ($tbody.length === 0) return;
1688
1689 // Get the highest ID from the table
1690 $tbody.find('tr').each(function() {
1691 const idText = $(this).find('td:first').text().trim();
1692 const id = parseInt(idText);
1693 if (!isNaN(id) && id > lastEntryId) {
1694 lastEntryId = id;
1695 }
1696 });
1697 }
1698
1699 // Poll for new entries during processing
1700 function startEntriesPolling() {
1701 if (entriesPollingInterval) return; // Already polling
1702
1703 isEntriesPolling = true;
1704
1705 // Fetch immediately, then start interval
1706 fetchNewEntries();
1707
1708 entriesPollingInterval = setInterval(function() {
1709 fetchNewEntries();
1710 }, 3000); // Poll every 3 seconds
1711 }
1712
1713 function stopEntriesPolling() {
1714 if (entriesPollingInterval) {
1715 clearInterval(entriesPollingInterval);
1716 entriesPollingInterval = null;
1717 }
1718 isEntriesPolling = false;
1719 }
1720
1721 // Track Pinecone count for change detection
1722 var lastPineconeCount = 0;
1723 var pineconeRefreshPending = false;
1724
1725 // Fetch new entries from server
1726 function fetchNewEntries() {
1727 if (!mxchatAdmin.entries_nonce) {
1728 return;
1729 }
1730
1731 $.ajax({
1732 url: ajaxurl,
1733 type: 'POST',
1734 data: {
1735 action: 'mxchat_get_recent_entries',
1736 nonce: mxchatAdmin.entries_nonce,
1737 last_id: lastEntryId,
1738 bot_id: mxchatAdmin.bot_id || 'default',
1739 limit: 20
1740 },
1741 success: function(response) {
1742 if (response.success && response.data) {
1743 // Update count
1744 if (response.data.total_count !== undefined) {
1745 updateEntryCount(response.data.total_count, response.data.count_unit);
1746 }
1747
1748 // Handle Pinecone data source differently
1749 if (response.data.data_source === 'pinecone') {
1750 var newCount = response.data.total_count || 0;
1751
1752 // If count changed and we haven't scheduled a refresh yet
1753 if (newCount !== lastPineconeCount && !pineconeRefreshPending) {
1754 lastPineconeCount = newCount;
1755
1756 // Schedule a table refresh after processing completes
1757 // Show a "refresh to see entries" message
1758 var $tbody = $('#mxchat-entries-tbody');
1759 var $refreshNotice = $tbody.find('.mxchat-pinecone-refresh-notice');
1760
1761 if ($refreshNotice.length === 0 && newCount > 0) {
1762 var noticeHtml = '<tr class="mxchat-pinecone-refresh-notice">' +
1763 '<td colspan="4" style="padding: 20px; text-align: center; background: #f0f7ff; border-bottom: 1px solid var(--mxch-card-border);">' +
1764 '<span class="dashicons dashicons-update" style="color: #7873f5; margin-right: 8px;"></span>' +
1765 '<strong>' + newCount + ' entries in Pinecone.</strong> ' +
1766 '<a href="#" class="mxchat-refresh-table-link" style="color: #7873f5; text-decoration: underline;">Refresh to see new entries</a>' +
1767 '</td></tr>';
1768 $tbody.prepend(noticeHtml);
1769
1770 // Handle refresh link click
1771 $tbody.find('.mxchat-refresh-table-link').on('click', function(e) {
1772 e.preventDefault();
1773 location.reload();
1774 });
1775 } else if ($refreshNotice.length > 0) {
1776 // Update the count in existing notice
1777 $refreshNotice.find('strong').text(newCount + ' entries in Pinecone.');
1778 }
1779 }
1780 } else {
1781 // WordPress DB - Track new entries and show refresh notice
1782 // Don't add rows individually during processing as they need to be grouped by source_url
1783 if (response.data.entries && response.data.entries.length > 0) {
1784 // Update last ID to track progress
1785 if (response.data.max_id > lastEntryId) {
1786 lastEntryId = response.data.max_id;
1787 }
1788
1789 // Show/update refresh notice (similar to Pinecone handling)
1790 var $tbody = $('#mxchat-entries-tbody');
1791 var $refreshNotice = $tbody.find('.mxchat-wordpress-refresh-notice');
1792 var newCount = response.data.total_count || 0;
1793
1794 if ($refreshNotice.length === 0 && newCount > 0) {
1795 var noticeHtml = '<tr class="mxchat-wordpress-refresh-notice mxchat-new-entry">' +
1796 '<td colspan="4" style="padding: 16px 20px; text-align: center; background: linear-gradient(135deg, rgba(120, 115, 245, 0.08) 0%, rgba(167, 139, 250, 0.05) 100%); border-bottom: 1px solid var(--mxch-card-border);">' +
1797 '<span class="dashicons dashicons-update spin" style="color: #7873f5; margin-right: 8px;"></span>' +
1798 '<strong style="color: var(--mxch-text-primary);">Processing... <span class="mxchat-processing-count">' + newCount + '</span> entries</strong>' +
1799 '</td></tr>';
1800 $tbody.prepend(noticeHtml);
1801 } else if ($refreshNotice.length > 0) {
1802 // Update the count in existing notice
1803 $refreshNotice.find('.mxchat-processing-count').text(newCount);
1804 }
1805 } else {
1806 // Update last ID even if no new entries
1807 if (response.data.max_id > lastEntryId) {
1808 lastEntryId = response.data.max_id;
1809 }
1810 }
1811 }
1812 }
1813 },
1814 error: function(xhr, status, error) {
1815 console.error('MxChat: Error fetching new entries:', error, xhr.responseText);
1816 }
1817 });
1818 }
1819
1820 // Read the heading's count from its data attribute, not from the text: the
1821 // text now carries a thousands separator and a unit ("(1,240 vectors)") on
1822 // the Pinecone cursor path (plan dd6e10).
1823 function readEntryCount($countSpan) {
1824 var stored = parseInt($countSpan.attr('data-count'), 10);
1825 if (!isNaN(stored)) {
1826 return stored;
1827 }
1828 var match = $countSpan.text().match(/\(([\d,]+)/);
1829 return match ? parseInt(match[1].replace(/,/g, ''), 10) : NaN;
1830 }
1831
1832 // Update the entry count display. `unit` is 'vectors' on the Pinecone
1833 // cursor path and '' otherwise; omitted = keep whatever the span carries.
1834 function updateEntryCount(count, unit) {
1835 // Update main table count
1836 const $countSpan = $('#mxchat-entry-count');
1837 if ($countSpan.length) {
1838 if (unit === undefined) {
1839 unit = $countSpan.attr('data-unit') || '';
1840 }
1841 var shown = Number(count).toLocaleString();
1842 $countSpan.attr('data-count', count).attr('data-unit', unit);
1843 $countSpan.text('(' + shown + (unit === 'vectors' ? ' vectors' : '') + ')');
1844
1845 // Flash animation to indicate update
1846 $countSpan.addClass('mxchat-count-updated');
1847 setTimeout(function() {
1848 $countSpan.removeClass('mxchat-count-updated');
1849 }, 1000);
1850 }
1851
1852 // Update sidebar badge count
1853 const $sidebarCount = $('#mxchat-sidebar-count');
1854 if ($sidebarCount.length) {
1855 $sidebarCount.text(count);
1856
1857 // Flash animation for sidebar too
1858 $sidebarCount.addClass('mxchat-count-updated');
1859 setTimeout(function() {
1860 $sidebarCount.removeClass('mxchat-count-updated');
1861 }, 1000);
1862 }
1863 }
1864
1865 // Refresh the knowledge base table via AJAX pagination
1866 // This ensures entries are properly grouped by source_url
1867 function refreshKnowledgeBaseTable() {
1868 var $paginationWrapper = $('#mxchat-kb-pagination');
1869 var $tbody = $('#mxchat-entries-tbody');
1870
1871 if ($tbody.length === 0) {
1872 return;
1873 }
1874
1875 // Remove any processing notice
1876 $tbody.find('.mxchat-wordpress-refresh-notice, .mxchat-pinecone-refresh-notice').remove();
1877
1878 // Show loading state
1879 $tbody.css('opacity', '0.5');
1880
1881 $.ajax({
1882 url: ajaxurl,
1883 type: 'POST',
1884 data: {
1885 action: 'mxchat_paginate_entries',
1886 nonce: mxchatAdmin.entries_nonce,
1887 bot_id: mxchatAdmin.bot_id || 'default',
1888 page: 1 // Always go to first page to see newest entries
1889 },
1890 success: function(response) {
1891 $tbody.css('opacity', '1');
1892
1893 if (response.success && response.data) {
1894 // Update the table body with properly grouped HTML
1895 $tbody.html(response.data.html);
1896
1897 // Update the pagination
1898 if ($paginationWrapper.length) {
1899 if (response.data.pagination_html) {
1900 // Add pagination content and styling
1901 $paginationWrapper.html(response.data.pagination_html);
1902 $paginationWrapper.attr('style', 'padding: 16px; border-top: 1px solid var(--mxch-card-border); text-align: center;');
1903 } else {
1904 // No pagination needed - clear and hide
1905 $paginationWrapper.html('');
1906 $paginationWrapper.attr('style', '');
1907 }
1908 }
1909
1910 // Update count display
1911 if (response.data.total_count !== undefined) {
1912 updateEntryCount(response.data.total_count, response.data.count_unit);
1913 }
1914 }
1915 },
1916 error: function(xhr, status, error) {
1917 $tbody.css('opacity', '1');
1918 console.error('MxChat: Error refreshing table:', error);
1919 }
1920 });
1921 }
1922
1923 // Expose refreshKnowledgeBaseTable for external access (content-selector.js)
1924 window.refreshKnowledgeBaseTable = refreshKnowledgeBaseTable;
1925
1926 // Add new entries to the table (kept for backwards compatibility but not used during processing)
1927 function addNewEntriesToTable(entries) {
1928 const $tbody = $('#mxchat-entries-tbody');
1929 if ($tbody.length === 0) return;
1930
1931 // Remove "no entries" message if present
1932 const $noEntries = $tbody.find('td[colspan="4"]').closest('tr');
1933 if ($noEntries.length) {
1934 $noEntries.remove();
1935 }
1936
1937 // Add entries in reverse order (oldest first, so newest ends up at top)
1938 entries.reverse().forEach(function(entry) {
1939 // Check if entry already exists
1940 if ($tbody.find('tr[data-entry-id="' + entry.id + '"]').length > 0) {
1941 return;
1942 }
1943
1944 const sourceHtml = entry.has_link
1945 ? '<a href="' + entry.source_url + '" target="_blank" style="color: var(--mxch-primary); text-decoration: none;"><span class="dashicons dashicons-external" style="font-size: 14px;"></span> View</a>'
1946 : '<span style="color: var(--mxch-text-muted);">Manual</span>';
1947
1948 const deleteUrl = mxchatAdmin.admin_url + 'admin-post.php?action=mxchat_delete_prompt&id=' + entry.id + '&bot_id=' + encodeURIComponent(mxchatAdmin.bot_id || 'default') + '&_wpnonce=' + entry.delete_nonce;
1949
1950 // Check if content needs expand button (content longer than preview)
1951 const needsExpand = entry.content_length > entry.preview_length;
1952
1953 // Build accordion-style content cell (matching initial page load structure)
1954 let contentHtml = '<div class="mxchat-accordion-wrapper">' +
1955 '<div class="mxchat-content-preview">' +
1956 '<span class="preview-text">' + entry.preview + '</span>';
1957
1958 if (needsExpand) {
1959 contentHtml += '<button class="mxchat-expand-toggle" type="button">' +
1960 '<span class="dashicons dashicons-arrow-down-alt2"></span>' +
1961 '</button>';
1962 }
1963
1964 contentHtml += '</div>' +
1965 '<div class="mxchat-content-full" style="display: none;">' +
1966 '<div class="content-view">' + entry.full_content + '</div>' +
1967 '</div>' +
1968 '</div>';
1969
1970 const $row = $('<tr id="prompt-' + entry.id + '" data-entry-id="' + entry.id + '" data-source="wordpress" style="border-bottom: 1px solid var(--mxch-card-border); display: none;">' +
1971 '<td style="padding: 12px 16px; font-size: 13px;">' + entry.id + '</td>' +
1972 '<td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">' + contentHtml + '</td>' +
1973 '<td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">' + sourceHtml + '</td>' +
1974 '<td style="padding: 12px 16px; white-space: nowrap;">' +
1975 '<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"' +
1976 ' data-source-url="' + (entry.source_url || '') + '"' +
1977 ' data-entry-id="' + entry.id + '"' +
1978 ' data-data-source="wordpress"' +
1979 ' data-bot-id="' + (entry.bot_id || 'default') + '"' +
1980 ' data-nonce="' + (entry.edit_nonce || '') + '"' +
1981 ' title="Edit content">' +
1982 '<span class="dashicons dashicons-edit" style="font-size: 14px;"></span>' +
1983 '</button>' +
1984 '<a href="' + deleteUrl + '" class="mxch-btn mxch-btn-ghost mxch-btn-sm" style="color: var(--mxch-error);" onclick="return confirm(\'Delete this entry?\');">' +
1985 '<span class="dashicons dashicons-trash" style="font-size: 14px;"></span>' +
1986 '</a>' +
1987 '</td>' +
1988 '</tr>');
1989
1990 // Add highlight class and prepend to tbody
1991 $row.addClass('mxchat-new-entry');
1992 $tbody.prepend($row);
1993 $row.slideDown(300);
1994
1995 // Remove highlight after animation
1996 setTimeout(function() {
1997 $row.removeClass('mxchat-new-entry');
1998 }, 2000);
1999 });
2000 }
2001
2002 // Initialize entry ID tracking
2003 initializeLastEntryId();
2004
2005 // Check on page load if there's already active processing (e.g., page was refreshed during processing)
2006 if ($('.mxchat-status-card').length > 0) {
2007 // Check if processing is active via AJAX
2008 $.ajax({
2009 url: ajaxurl,
2010 type: 'POST',
2011 data: {
2012 action: 'mxchat_get_status_updates',
2013 nonce: mxchatAdmin.status_nonce
2014 },
2015 success: function(response) {
2016 if (response.is_processing) {
2017 startEntriesPolling();
2018 }
2019 }
2020 });
2021 }
2022
2023 // Expose functions for external access
2024 window.mxchatEntriesPolling = {
2025 start: startEntriesPolling,
2026 stop: stopEntriesPolling,
2027 fetch: fetchNewEntries
2028 };
2029
2030 // ========================================
2031 // SITEMAP DETECTION FUNCTIONALITY
2032 // ========================================
2033
2034 let sitemapDetectionInitialized = false;
2035
2036 /**
2037 * Initialize sitemap detection when Sitemap Import is clicked
2038 */
2039 function initSitemapDetection() {
2040 if (sitemapDetectionInitialized) return;
2041
2042 const loadingEl = document.getElementById('mxchat-sitemaps-loading');
2043 const detectedEl = document.getElementById('mxchat-detected-sitemaps');
2044 const noSitemapsEl = document.getElementById('mxchat-no-sitemaps');
2045 const listEl = document.getElementById('mxchat-sitemaps-list');
2046 const refreshBtn = document.getElementById('mxchat-refresh-sitemaps');
2047 const nonceEl = document.getElementById('mxchat-detect-sitemaps-nonce');
2048
2049 if (!loadingEl || !nonceEl) return;
2050
2051 sitemapDetectionInitialized = true;
2052
2053 function detectSitemaps() {
2054 // Show loading
2055 loadingEl.style.display = 'block';
2056 if (detectedEl) detectedEl.style.display = 'none';
2057 if (noSitemapsEl) noSitemapsEl.style.display = 'none';
2058
2059 // Disable refresh button
2060 if (refreshBtn) {
2061 refreshBtn.disabled = true;
2062 var refreshIcon = refreshBtn.querySelector('.dashicons');
2063 if (refreshIcon) refreshIcon.classList.add('spin');
2064 }
2065
2066 $.ajax({
2067 url: ajaxurl,
2068 type: 'POST',
2069 data: {
2070 action: 'mxchat_detect_sitemaps',
2071 nonce: nonceEl.value
2072 },
2073 timeout: 60000, // 60 second timeout for slow servers
2074 success: function(data) {
2075 loadingEl.style.display = 'none';
2076
2077 // Re-enable refresh button
2078 if (refreshBtn) {
2079 refreshBtn.disabled = false;
2080 var refreshIcon = refreshBtn.querySelector('.dashicons');
2081 if (refreshIcon) refreshIcon.classList.remove('spin');
2082 }
2083
2084 if (data.success && data.data && data.data.sitemaps && data.data.sitemaps.length > 0) {
2085 renderSitemaps(data.data.sitemaps);
2086 if (detectedEl) detectedEl.style.display = 'block';
2087 } else {
2088 if (noSitemapsEl) {
2089 noSitemapsEl.style.display = 'block';
2090 $(noSitemapsEl).data('was-shown', true);
2091 }
2092 }
2093 },
2094 error: function(xhr, status, error) {
2095 loadingEl.style.display = 'none';
2096 if (noSitemapsEl) {
2097 noSitemapsEl.style.display = 'block';
2098 $(noSitemapsEl).data('was-shown', true);
2099 }
2100 if (refreshBtn) {
2101 refreshBtn.disabled = false;
2102 var refreshIcon = refreshBtn.querySelector('.dashicons');
2103 if (refreshIcon) refreshIcon.classList.remove('spin');
2104 }
2105 }
2106 });
2107 }
2108
2109 function renderSitemaps(sitemaps) {
2110 if (!listEl) return;
2111
2112 var html = '';
2113 var botIdEl = document.getElementById('mxchat-sitemap-bot-id');
2114 var botId = botIdEl ? botIdEl.value : '';
2115
2116 sitemaps.forEach(function(sitemap) {
2117 if (sitemap.type === 'index' && sitemap.sub_sitemaps && sitemap.sub_sitemaps.length > 0) {
2118 // Render sitemap index with sub-sitemaps
2119 html += '<div class="mxchat-sitemap-group">';
2120 html += '<div class="mxchat-sitemap-group-header">';
2121 html += '<div style="display: flex; align-items: center; gap: 10px;">';
2122 html += '<span class="dashicons dashicons-arrow-right-alt2" style="transition: transform 0.2s;"></span>';
2123 html += '<span class="dashicons dashicons-list-view" style="color: #7873f5;"></span>';
2124 html += '<div>';
2125 html += '<strong style="font-size: 13px;">Sitemap Index</strong>';
2126 html += '<span style="color: #666; font-size: 12px; margin-left: 8px;">' + sitemap.source + '</span>';
2127 html += '</div>';
2128 html += '</div>';
2129 html += '<span style="background: rgba(120, 115, 245, 0.1); color: #7873f5; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600;">';
2130 html += sitemap.sub_sitemaps.length + ' sitemaps';
2131 html += '</span>';
2132 html += '</div>';
2133 html += '<div class="mxchat-sitemap-sub-list">';
2134 sitemap.sub_sitemaps.forEach(function(sub) {
2135 html += renderSitemapRow(sub, botId, true);
2136 });
2137 html += '</div>';
2138 html += '</div>';
2139 } else if (sitemap.type !== 'index') {
2140 // Render standalone sitemap
2141 html += renderSitemapRow(sitemap, botId, false);
2142 }
2143 });
2144
2145 listEl.innerHTML = html;
2146
2147 // Add click handlers for group toggles
2148 $(listEl).find('.mxchat-sitemap-group-header').on('click', function() {
2149 var $group = $(this).parent();
2150 var $subList = $group.find('.mxchat-sitemap-sub-list');
2151 var $arrow = $(this).find('.dashicons-arrow-right-alt2');
2152
2153 $group.toggleClass('expanded');
2154
2155 if ($group.hasClass('expanded')) {
2156 $subList.slideDown(200);
2157 $arrow.css('transform', 'rotate(90deg)');
2158 } else {
2159 $subList.slideUp(200);
2160 $arrow.css('transform', 'rotate(0deg)');
2161 }
2162 });
2163
2164 // Add click handlers for process buttons
2165 $(listEl).find('.mxchat-process-sitemap-btn').on('click', function() {
2166 var url = $(this).data('url');
2167 var type = $(this).data('sitemap-type');
2168 processSitemap(url, type, this);
2169 });
2170 }
2171
2172 function renderSitemapRow(sitemap, botId, isSubItem) {
2173 var typeLabels = {
2174 'content': 'Content',
2175 'taxonomy': 'Taxonomy',
2176 'author': 'Authors'
2177 };
2178 var typeLabel = typeLabels[sitemap.type] || sitemap.type;
2179 var displayName = sitemap.name || sitemap.url.split('/').pop();
2180 var urlCount = sitemap.url_count || 0;
2181 var paddingLeft = isSubItem ? '40px' : '16px';
2182
2183 var html = '<div class="mxchat-sitemap-row" style="padding-left: ' + paddingLeft + ';">';
2184 html += '<div style="display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0;">';
2185 html += '<span class="dashicons dashicons-media-text" style="color: #666; flex-shrink: 0;"></span>';
2186 html += '<div style="min-width: 0; flex: 1;">';
2187 html += '<div style="font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="' + sitemap.url + '">';
2188 html += displayName;
2189 html += '</div>';
2190 html += '<div style="font-size: 11px; color: #666;">';
2191 html += '<span style="background: #f0f0f0; padding: 1px 6px; border-radius: 3px; margin-right: 8px;">' + typeLabel + '</span>';
2192 if (urlCount > 0) {
2193 html += urlCount + ' URLs';
2194 }
2195 html += '</div>';
2196 html += '</div>';
2197 html += '</div>';
2198 html += '<button type="button" class="mxchat-process-sitemap-btn mxch-btn mxch-btn-primary mxch-btn-sm" data-url="' + sitemap.url + '" data-sitemap-type="' + sitemap.type + '">';
2199 html += '<span class="dashicons dashicons-download" style="font-size: 14px; margin-top: 3px;"></span> Process';
2200 html += '</button>';
2201 html += '</div>';
2202
2203 return html;
2204 }
2205
2206 function processSitemap(url, type, buttonEl) {
2207 var $button = $(buttonEl);
2208 var originalHtml = $button.html();
2209
2210 // Update button to show loading
2211 $button.prop('disabled', true);
2212 $button.html('<span class="dashicons dashicons-update spin" style="font-size: 14px; margin-top: 3px;"></span> Processing...');
2213
2214 // Fill in the sitemap URL form and submit
2215 var $form = $('#mxchat-url-form');
2216 var $urlInput = $('#sitemap_url');
2217 var $importType = $('#import_type');
2218
2219 if ($urlInput.length) {
2220 $urlInput.val(url);
2221 }
2222
2223 if ($importType.length) {
2224 $importType.val('sitemap');
2225 }
2226
2227 // Add a hidden submit field if not present (required by the PHP handler)
2228 if ($form.find('input[name="submit_sitemap"]').length === 0) {
2229 $form.append('<input type="hidden" name="submit_sitemap" value="1">');
2230 }
2231
2232 // Submit the form
2233 $form.submit();
2234 }
2235
2236
2237 // Refresh button handler
2238 if (refreshBtn) {
2239 $(refreshBtn).on('click', detectSitemaps);
2240 }
2241
2242 // Start detection
2243 detectSitemaps();
2244 }
2245
2246 // Expose initSitemapDetection globally so it can be called from the import options handler
2247 window.mxchatInitSitemapDetection = initSitemapDetection;
2248
2249 // ========================================
2250 // ADMIN NOTICE DISMISS FUNCTIONALITY
2251 // ========================================
2252
2253 // Initialize dismissible notices - add dismiss button if missing
2254 function initDismissibleNotices() {
2255 $('.notice.is-dismissible').each(function() {
2256 var $notice = $(this);
2257
2258 // Skip if already has a dismiss button
2259 if ($notice.find('.notice-dismiss').length > 0) {
2260 return;
2261 }
2262
2263 // Add dismiss button
2264 var $dismissButton = $('<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>');
2265 $notice.append($dismissButton);
2266 });
2267 }
2268
2269 // Initialize on page load
2270 initDismissibleNotices();
2271
2272 // Use event delegation for dismiss button clicks - works for existing and dynamically added notices
2273 $(document).on('click', '.notice.is-dismissible .notice-dismiss', function(e) {
2274 e.preventDefault();
2275 e.stopPropagation();
2276
2277 var $notice = $(this).closest('.notice');
2278 $notice.fadeTo(100, 0, function() {
2279 $notice.slideUp(100, function() {
2280 $notice.remove();
2281 });
2282 });
2283 });
2284
2285 // Re-initialize when new notices are added dynamically (e.g., via AJAX)
2286 $(document).on('DOMNodeInserted', function(e) {
2287 if ($(e.target).hasClass('notice') && $(e.target).hasClass('is-dismissible')) {
2288 setTimeout(initDismissibleNotices, 10);
2289 }
2290 });
2291
2292 // ========================================
2293 // CUSTOM META-KEY DISCOVERY PICKER (plan-mxchat-20260709-fe8e4e)
2294 // Mirror the ACF picker's discover-and-click UX for non-ACF post meta: scan
2295 // published content for meta keys, render clickable chips, and append chosen
2296 // keys into the existing whitelist textarea (its autosave then persists them).
2297 // ========================================
2298 (function() {
2299 var $scanBtn = $('#mxchat-scan-meta-btn');
2300 if (!$scanBtn.length) { return; }
2301
2302 var $results = $('#mxchat-meta-scan-results');
2303 var $textarea = $('#mxchat_custom_meta_whitelist');
2304 var $internal = $('#mxchat-scan-internal');
2305 var $label = $scanBtn.find('.mxchat-scan-label');
2306 var defaultLabel = $label.text();
2307
2308 function currentKeys() {
2309 return ($textarea.val() || '')
2310 .split('\n')
2311 .map(function(k) { return k.trim(); })
2312 .filter(function(k) { return k.length; });
2313 }
2314
2315 function addKey(key) {
2316 var keys = currentKeys();
2317 if (keys.indexOf(key) !== -1) { return false; }
2318 keys.push(key);
2319 $textarea.val(keys.join('\n'));
2320 // Fire the existing autosave (bound on 'change' in mxchat-admin.js).
2321 $textarea.trigger('change');
2322 return true;
2323 }
2324
2325 function escapeHtml(s) {
2326 return String(s).replace(/[&<>"']/g, function(c) {
2327 return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
2328 });
2329 }
2330
2331 function humanize(key) {
2332 var h = key.replace(/^_+/, '').replace(/[_-]+/g, ' ').trim();
2333 return h.length ? h.charAt(0).toUpperCase() + h.slice(1) : key;
2334 }
2335
2336 function renderChips(keys) {
2337 if (!keys || !keys.length) {
2338 $results.html('<p class="mxchat-meta-scan-empty">' +
2339 'No custom meta keys found on your published content. Try enabling internal keys, or add keys manually below.' +
2340 '</p>');
2341 return;
2342 }
2343 var existing = currentKeys();
2344 var html = '<div class="mxchat-meta-chip-grid">';
2345 keys.forEach(function(item) {
2346 var already = existing.indexOf(item.key) !== -1;
2347 html += '<button type="button" class="mxchat-meta-chip' + (already ? ' is-added' : '') +
2348 '" data-key="' + escapeHtml(item.key) + '"' + (already ? ' aria-pressed="true"' : '') + '>' +
2349 '<span class="mxchat-meta-chip-main">' +
2350 '<span class="mxchat-meta-chip-name">' + escapeHtml(humanize(item.key)) + '</span>' +
2351 '<span class="mxchat-meta-chip-key">' + escapeHtml(item.key) + '</span>' +
2352 '</span>' +
2353 '<span class="mxchat-meta-chip-meta">on ' + item.count + ' post' + (item.count === 1 ? '' : 's') +
2354 (item.sample ? ' · e.g. ' + escapeHtml(item.sample) : '') + '</span>' +
2355 '<span class="mxchat-meta-chip-tick">' + (already ? '✓ Added' : '+ Add') + '</span>' +
2356 '</button>';
2357 });
2358 html += '</div>';
2359 $results.html(html);
2360 }
2361
2362 $scanBtn.on('click', function() {
2363 var nonce = $scanBtn.data('nonce') ||
2364 (window.mxchatAdmin && mxchatAdmin.settings_nonce) || '';
2365 var ajaxUrl = (window.mxchatAdmin && (mxchatAdmin.ajax_url || mxchatAdmin.ajaxurl)) ||
2366 window.ajaxurl || '/wp-admin/admin-ajax.php';
2367
2368 $scanBtn.prop('disabled', true);
2369 $label.text('Scanning…');
2370 $results.html('<p class="mxchat-meta-scan-loading">Scanning your content for meta keys…</p>');
2371
2372 $.post(ajaxUrl, {
2373 action: 'mxchat_scan_custom_meta_keys',
2374 _ajax_nonce: nonce,
2375 include_internal: $internal.is(':checked') ? '1' : '0'
2376 }).done(function(resp) {
2377 if (resp && resp.success && resp.data && resp.data.keys) {
2378 renderChips(resp.data.keys);
2379 } else {
2380 var msg = (resp && resp.data && resp.data.message) ? resp.data.message : 'Scan failed. Please try again.';
2381 $results.html('<p class="mxchat-meta-scan-empty">' + escapeHtml(msg) + '</p>');
2382 }
2383 }).fail(function() {
2384 $results.html('<p class="mxchat-meta-scan-empty">Scan request failed. Please try again.</p>');
2385 }).always(function() {
2386 $scanBtn.prop('disabled', false);
2387 $label.text(defaultLabel);
2388 });
2389 });
2390
2391 // Chip click -> add the raw key to the whitelist textarea (dedup) + autosave.
2392 $results.on('click', '.mxchat-meta-chip', function() {
2393 var $chip = $(this);
2394 if ($chip.hasClass('is-added')) { return; }
2395 var key = String($chip.data('key'));
2396 if (addKey(key)) {
2397 $chip.addClass('is-added').attr('aria-pressed', 'true');
2398 $chip.find('.mxchat-meta-chip-tick').text('✓ Added');
2399 }
2400 });
2401 })();
2402
2403 // ========================================
2404 // VIDEO CARDS — threshold follows the master switch (plan f52492)
2405 // ========================================
2406 // Presentation only. The server gate does NOT consult the threshold when
2407 // the switch is off, so hiding the field never changes behavior — it just
2408 // stops the page offering a number that is currently inert. Mirrors the
2409 // YouTube import form's own show/hide idiom on this same page. Autosave is
2410 // untouched: both fields keep their own change handler in mxchat-admin.js.
2411 $('#mxchat_video_embed_enabled').on('change', function() {
2412 $('#mxchat-video-embed-threshold-field').toggle($(this).is(':checked'));
2413 });
2414 });