PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.21
MxChat – AI Chatbot & Content Generation for WordPress v3.2.21
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / js / knowledge-processing.js

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

2,399 lines 102.0 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 currentText = $countSpan.text();
1091 var match = currentText.match(/\((\d+)\)/);
1092 if (match) {
1093 var newCount = Math.max(0, parseInt(match[1]) - 1);
1094 updateEntryCount(newCount);
1095 }
1096 }
1097 });
1098
1099 $('<div class="notice notice-success is-dismissible"><p>Entry deleted successfully from Pinecone.</p></div>')
1100 .insertAfter('.mxchat-hero')
1101 .delay(3000)
1102 .fadeOut();
1103 } else {
1104 $button.prop('disabled', false);
1105 $button.find('.dashicons').removeClass('dashicons-update-alt').addClass('dashicons-trash');
1106 $row.removeClass('mxchat-row-deleting');
1107 alert('Error: ' + response.data);
1108 }
1109 },
1110 error: function() {
1111 $button.prop('disabled', false);
1112 $button.find('.dashicons').removeClass('dashicons-update-alt').addClass('dashicons-trash');
1113 $row.removeClass('mxchat-row-deleting');
1114 alert('Network error occurred');
1115 }
1116 });
1117 });
1118
1119 // ========================================
1120 // WORDPRESS DATABASE DELETE HANDLER (AJAX)
1121 // ========================================
1122
1123 $(document).on('click', '.delete-button-wordpress', function(e) {
1124 e.preventDefault();
1125
1126 if (!confirm('Are you sure you want to delete this entry?')) {
1127 return;
1128 }
1129
1130 var $button = $(this);
1131 var $row = $button.closest('tr');
1132 var entryId = $button.data('entry-id');
1133 var nonce = $button.data('nonce');
1134
1135 $button.prop('disabled', true);
1136 $button.find('.dashicons').removeClass('dashicons-trash').addClass('dashicons-update-alt spin');
1137 $row.addClass('mxchat-row-deleting');
1138
1139 $.ajax({
1140 url: ajaxurl,
1141 type: 'POST',
1142 data: {
1143 action: 'mxchat_delete_wordpress_prompt',
1144 nonce: nonce,
1145 entry_id: entryId
1146 },
1147 success: function(response) {
1148 if (response.success) {
1149 $row.fadeOut(500, function() {
1150 $(this).remove();
1151 // Update entry count displays (header and sidebar)
1152 var $countSpan = $('#mxchat-entry-count');
1153 if ($countSpan.length) {
1154 var currentText = $countSpan.text();
1155 var match = currentText.match(/\((\d+)\)/);
1156 if (match) {
1157 var newCount = Math.max(0, parseInt(match[1]) - 1);
1158 updateEntryCount(newCount);
1159 }
1160 }
1161 });
1162
1163 $('<div class="notice notice-success is-dismissible"><p>Entry deleted successfully.</p></div>')
1164 .insertAfter('.mxchat-hero')
1165 .delay(3000)
1166 .fadeOut();
1167 } else {
1168 $button.prop('disabled', false);
1169 $button.find('.dashicons').removeClass('dashicons-update-alt spin').addClass('dashicons-trash');
1170 $row.removeClass('mxchat-row-deleting');
1171 alert('Error: ' + response.data);
1172 }
1173 },
1174 error: function() {
1175 $button.prop('disabled', false);
1176 $button.find('.dashicons').removeClass('dashicons-update-alt spin').addClass('dashicons-trash');
1177 $row.removeClass('mxchat-row-deleting');
1178 alert('Network error occurred');
1179 }
1180 });
1181 });
1182
1183 // ========================================
1184 // BULK SELECTION FOR KNOWLEDGE ENTRIES
1185 // ========================================
1186
1187 var selectedKnowledgeEntries = new Set();
1188
1189 // Update selection UI
1190 function updateKnowledgeSelectionUI() {
1191 var count = selectedKnowledgeEntries.size;
1192 var $countEl = $('#mxchat-selected-entry-count');
1193 var $deleteBtn = $('#mxchat-delete-selected-entries');
1194 var $deleteAllForm = $('#mxchat-delete-all-form');
1195
1196 if (count > 0) {
1197 // Show Delete Selected button, hide Delete All form
1198 $deleteAllForm.hide();
1199 $deleteBtn.show();
1200 $countEl.text('(' + count + ')');
1201 } else {
1202 // Show Delete All form, hide Delete Selected button
1203 $deleteAllForm.show();
1204 $deleteBtn.hide();
1205 $countEl.text('');
1206 }
1207
1208 // Update select all checkbox state
1209 var totalItems = $('.mxchat-entry-checkbox').length;
1210 var checkedItems = $('.mxchat-entry-checkbox:checked').length;
1211 $('.mxchat-entry-checkbox-all').prop('checked', totalItems > 0 && checkedItems === totalItems);
1212 $('.mxchat-entry-checkbox-all').prop('indeterminate', checkedItems > 0 && checkedItems < totalItems);
1213 }
1214
1215 // Select all checkbox handler
1216 $(document).on('change', '.mxchat-entry-checkbox-all', function() {
1217 var isChecked = $(this).is(':checked');
1218
1219 // Sync all select-all checkboxes
1220 $('.mxchat-entry-checkbox-all').prop('checked', isChecked);
1221
1222 $('.mxchat-entry-checkbox').prop('checked', isChecked);
1223
1224 if (isChecked) {
1225 $('.mxchat-entry-checkbox').each(function() {
1226 var entryData = {
1227 id: $(this).data('entry-id'),
1228 source: $(this).data('source'),
1229 sourceUrl: $(this).data('source-url'),
1230 isGroup: $(this).data('is-group'),
1231 chunkCount: $(this).data('chunk-count') || 1
1232 };
1233 selectedKnowledgeEntries.add(JSON.stringify(entryData));
1234 $(this).closest('tr').addClass('selected');
1235 });
1236 } else {
1237 selectedKnowledgeEntries.clear();
1238 $('tr.selected').removeClass('selected');
1239 }
1240
1241 updateKnowledgeSelectionUI();
1242 });
1243
1244 // Individual checkbox handler
1245 $(document).on('change', '.mxchat-entry-checkbox', function() {
1246 var $checkbox = $(this);
1247 var $row = $checkbox.closest('tr');
1248 var entryData = {
1249 id: $checkbox.data('entry-id'),
1250 source: $checkbox.data('source'),
1251 sourceUrl: $checkbox.data('source-url'),
1252 isGroup: $checkbox.data('is-group'),
1253 chunkCount: $checkbox.data('chunk-count') || 1
1254 };
1255 var entryKey = JSON.stringify(entryData);
1256
1257 if ($checkbox.is(':checked')) {
1258 selectedKnowledgeEntries.add(entryKey);
1259 $row.addClass('selected');
1260 } else {
1261 selectedKnowledgeEntries.delete(entryKey);
1262 $row.removeClass('selected');
1263 }
1264
1265 updateKnowledgeSelectionUI();
1266 });
1267
1268 // Bulk delete button handler
1269 $(document).on('click', '#mxchat-delete-selected-entries', function() {
1270 var count = selectedKnowledgeEntries.size;
1271 if (count === 0) return;
1272
1273 if (!confirm('Are you sure you want to delete ' + count + ' selected entries? This action cannot be undone.')) {
1274 return;
1275 }
1276
1277 var $button = $(this);
1278 var nonce = $button.data('nonce');
1279 var botId = $button.data('bot-id');
1280
1281 // Parse selected entries
1282 var entries = Array.from(selectedKnowledgeEntries).map(function(entryStr) {
1283 return JSON.parse(entryStr);
1284 });
1285
1286 // Show loading state
1287 $button.prop('disabled', true);
1288 $button.find('.mxchat-bulk-delete-text').text('Deleting...');
1289 $button.find('.dashicons').removeClass('dashicons-trash').addClass('dashicons-update spin');
1290
1291 // Mark rows as deleting
1292 entries.forEach(function(entry) {
1293 $('#prompt-' + entry.id).addClass('mxchat-row-deleting');
1294 });
1295
1296 $.ajax({
1297 url: ajaxurl,
1298 type: 'POST',
1299 timeout: 120000, // 120 seconds — matches server-side set_time_limit
1300 data: {
1301 action: 'mxchat_bulk_delete_knowledge',
1302 entries: entries,
1303 bot_id: botId,
1304 nonce: nonce
1305 },
1306 success: function(response) {
1307 if (response.success) {
1308 var data = response.data;
1309
1310 // Remove successful rows
1311 if (data.success_ids && data.success_ids.length > 0) {
1312 data.success_ids.forEach(function(id) {
1313 var $row = $('#prompt-' + id);
1314 // Also remove child chunk rows if it's a group
1315 var groupId = $row.data('group-id');
1316 if (groupId) {
1317 $('.mxchat-chunk-row.' + groupId).fadeOut(300, function() {
1318 $(this).remove();
1319 });
1320 }
1321 $row.fadeOut(300, function() {
1322 $(this).remove();
1323 });
1324 });
1325 }
1326
1327 // Handle failed entries
1328 if (data.failed_ids && data.failed_ids.length > 0) {
1329 data.failed_ids.forEach(function(id) {
1330 $('#prompt-' + id).removeClass('mxchat-row-deleting').addClass('mxchat-row-error');
1331 });
1332 }
1333
1334 // Show result message
1335 var successCount = data.success_ids ? data.success_ids.length : 0;
1336 var failedCount = data.failed_ids ? data.failed_ids.length : 0;
1337 var message = 'Deleted ' + successCount + ' entries.';
1338 if (failedCount > 0) {
1339 message += ' ' + failedCount + ' entries failed to delete.';
1340 }
1341
1342 $('<div class="notice notice-success is-dismissible"><p>' + message + '</p></div>')
1343 .insertAfter('.mxchat-hero')
1344 .delay(5000)
1345 .fadeOut();
1346
1347 // Clear selection
1348 selectedKnowledgeEntries.clear();
1349 updateKnowledgeSelectionUI();
1350
1351 // Update entry count displays (header and sidebar)
1352 if (successCount > 0) {
1353 var $countSpan = $('#mxchat-entry-count');
1354 if ($countSpan.length) {
1355 var currentText = $countSpan.text();
1356 var match = currentText.match(/\((\d+)\)/);
1357 if (match) {
1358 var newCount = Math.max(0, parseInt(match[1]) - successCount);
1359 updateEntryCount(newCount);
1360 }
1361 }
1362 }
1363
1364 } else {
1365 alert('Error: ' + (response.data || 'Unknown error'));
1366 $('tr.mxchat-row-deleting').removeClass('mxchat-row-deleting');
1367 }
1368 },
1369 error: function(jqXHR, textStatus) {
1370 var message = 'An error occurred while deleting entries.';
1371 if (textStatus === 'timeout') {
1372 message = 'The deletion request timed out. Please refresh the page to check which entries were deleted, then try again for any remaining.';
1373 } else if (textStatus === 'error' && jqXHR.status === 0) {
1374 message = 'Network error: The server took too long to respond. Please refresh and try deleting fewer entries at a time.';
1375 } else if (jqXHR.responseJSON && jqXHR.responseJSON.data) {
1376 message = 'Error: ' + jqXHR.responseJSON.data;
1377 }
1378 alert(message);
1379 $('tr.mxchat-row-deleting').removeClass('mxchat-row-deleting');
1380 },
1381 complete: function() {
1382 $button.prop('disabled', selectedKnowledgeEntries.size === 0);
1383 $button.find('.mxchat-bulk-delete-text').text('Delete Selected');
1384 $button.find('.dashicons').removeClass('dashicons-update spin').addClass('dashicons-trash');
1385 }
1386 });
1387 });
1388
1389 // Clear selection when page changes
1390 $(document).on('click', '.mxchat-page-link', function() {
1391 selectedKnowledgeEntries.clear();
1392 updateKnowledgeSelectionUI();
1393 });
1394
1395 // ========================================
1396 // AJAX PAGINATION FOR KNOWLEDGE BASE ENTRIES
1397 // ========================================
1398
1399 // Handle pagination link clicks
1400 $(document).on('click', '.mxchat-page-link', function(e) {
1401 e.preventDefault();
1402
1403 var $link = $(this);
1404 var page = $link.data('page');
1405 var $paginationWrapper = $('#mxchat-kb-pagination');
1406 var $tbody = $('#mxchat-entries-tbody');
1407
1408 if (!page || $link.hasClass('loading')) {
1409 return;
1410 }
1411
1412 // Show loading state
1413 $link.addClass('loading');
1414 $tbody.css('opacity', '0.5');
1415
1416 // Add loading indicator to pagination
1417 var $loadingIndicator = $('<span class="mxchat-pagination-loading"><span class="dashicons dashicons-update spin"></span></span>');
1418 $paginationWrapper.find('.mxchat-ajax-pagination').append($loadingIndicator);
1419
1420 // Get search and filter values from pagination wrapper
1421 var searchQuery = $paginationWrapper.data('search') || '';
1422 var contentType = $paginationWrapper.data('content-type') || '';
1423
1424 $.ajax({
1425 url: ajaxurl,
1426 type: 'POST',
1427 data: {
1428 action: 'mxchat_paginate_entries',
1429 nonce: mxchatAdmin.entries_nonce,
1430 bot_id: mxchatAdmin.bot_id || 'default',
1431 page: page,
1432 search: searchQuery,
1433 content_type: contentType
1434 },
1435 success: function(response) {
1436 $link.removeClass('loading');
1437 $tbody.css('opacity', '1');
1438 $loadingIndicator.remove();
1439
1440 if (response.success && response.data) {
1441 // Update the table body with new HTML
1442 $tbody.html(response.data.html);
1443
1444 // Update the pagination - find the inner container
1445 if (response.data.pagination_html) {
1446 // Replace the inner pagination div content
1447 $paginationWrapper.html(response.data.pagination_html);
1448 }
1449
1450 // Update data attributes on wrapper (preserve search/filter for next pagination)
1451 $paginationWrapper.attr('data-current-page', response.data.page);
1452 if (response.data.total_pages) {
1453 $paginationWrapper.attr('data-total-pages', response.data.total_pages);
1454 }
1455 // Preserve search and content_type on the wrapper from the inner pagination div
1456 var $innerPagination = $paginationWrapper.find('.mxchat-ajax-pagination');
1457 if ($innerPagination.length) {
1458 $paginationWrapper.attr('data-search', $innerPagination.data('search') || '');
1459 $paginationWrapper.attr('data-content-type', $innerPagination.data('content-type') || '');
1460 }
1461
1462 // Scroll to top of the table smoothly
1463 $('html, body').animate({
1464 scrollTop: $('#knowledge-base').offset().top - 50
1465 }, 300);
1466
1467 // Update URL hash to stay on knowledge-base tab
1468 if (window.history && window.history.replaceState) {
1469 window.history.replaceState(null, '', window.location.pathname + window.location.search + '#knowledge-base');
1470 }
1471
1472 // Show success feedback
1473 showNotification('success', 'Page ' + response.data.page + ' loaded');
1474 } else {
1475 showNotification('error', 'Failed to load page: ' + (response.data?.message || 'Unknown error'));
1476 }
1477 },
1478 error: function(xhr, status, error) {
1479 $link.removeClass('loading');
1480 $tbody.css('opacity', '1');
1481 $loadingIndicator.remove();
1482 showNotification('error', 'Network error while loading page');
1483 }
1484 });
1485 });
1486
1487 // ========================================
1488 // PINECONE REFRESH ENTRIES BUTTON
1489 // ========================================
1490
1491 $('#mxchat-refresh-pinecone-entries').on('click', function() {
1492 var $button = $(this);
1493 var $icon = $button.find('.dashicons');
1494 var $tbody = $('#mxchat-entries-tbody');
1495 var $paginationWrapper = $('#mxchat-kb-pagination');
1496
1497 // Show loading state
1498 $button.prop('disabled', true);
1499 $icon.addClass('spin');
1500 $tbody.css('opacity', '0.5');
1501
1502 $.ajax({
1503 url: ajaxurl,
1504 type: 'POST',
1505 data: {
1506 action: 'mxchat_refresh_pinecone_entries',
1507 nonce: mxchatAdmin.entries_nonce,
1508 bot_id: mxchatAdmin.bot_id || 'default',
1509 page: 1
1510 },
1511 success: function(response) {
1512 $button.prop('disabled', false);
1513 $icon.removeClass('spin');
1514 $tbody.css('opacity', '1');
1515
1516 if (response.success && response.data) {
1517 // Update the table body with new HTML
1518 $tbody.html(response.data.html);
1519
1520 // Update the pagination (same pattern as refreshKnowledgeBaseTable)
1521 if ($paginationWrapper.length) {
1522 if (response.data.pagination_html) {
1523 $paginationWrapper.html(response.data.pagination_html);
1524 $paginationWrapper.attr('style', 'padding: 16px; border-top: 1px solid var(--mxch-card-border); text-align: center;');
1525 } else {
1526 $paginationWrapper.html('');
1527 $paginationWrapper.attr('style', '');
1528 }
1529 }
1530
1531 // Update the count display
1532 if (response.data.total_count !== undefined) {
1533 updateEntryCount(response.data.total_count);
1534 }
1535
1536 // Show success feedback
1537 showNotification('success', 'Entries refreshed successfully!');
1538 } else {
1539 showNotification('error', 'Failed to refresh entries: ' + (response.data?.message || 'Unknown error'));
1540 }
1541 },
1542 error: function(xhr, status, error) {
1543 $button.prop('disabled', false);
1544 $icon.removeClass('spin');
1545 $tbody.css('opacity', '1');
1546 showNotification('error', 'Network error while refreshing entries');
1547 }
1548 });
1549 });
1550
1551 // ========================================
1552 // ACCORDION FUNCTIONALITY
1553 // ========================================
1554
1555 // Handle expand/collapse toggle
1556 $(document).on('click', '.mxchat-expand-toggle', function(e) {
1557 e.preventDefault();
1558 e.stopPropagation();
1559
1560 const $button = $(this);
1561 const $wrapper = $button.closest('.mxchat-accordion-wrapper');
1562 const $preview = $wrapper.find('.mxchat-content-preview');
1563 const $fullContent = $wrapper.find('.mxchat-content-full');
1564
1565 // Toggle expanded state
1566 if ($fullContent.is(':visible')) {
1567 // Collapse
1568 $fullContent.slideUp(300);
1569 $button.removeClass('expanded');
1570 } else {
1571 // Expand
1572 $fullContent.slideDown(300);
1573 $button.addClass('expanded');
1574 }
1575 });
1576
1577 // Click anywhere on preview to toggle (expand or collapse)
1578 $(document).on('click', '.mxchat-content-preview', function(e) {
1579 // Only trigger if not clicking the button directly
1580 if (!$(e.target).closest('.mxchat-expand-toggle').length) {
1581 const $preview = $(this);
1582 const $wrapper = $preview.closest('.mxchat-accordion-wrapper');
1583 const $button = $preview.find('.mxchat-expand-toggle');
1584
1585 // Only trigger if there's a button (meaning content is long enough to expand)
1586 if ($button.length) {
1587 $button.trigger('click');
1588 }
1589 }
1590 });
1591
1592 // ========================================
1593 // CHUNK GROUP TOGGLE FUNCTIONALITY
1594 // ========================================
1595
1596 // Handle chunk group expand/collapse toggle
1597 $(document).on('click', '.mxchat-chunk-toggle', function(e) {
1598 e.preventDefault();
1599 e.stopPropagation();
1600
1601 const $button = $(this);
1602 const groupId = $button.data('group-id');
1603 const $chunkRows = $('.mxchat-chunk-row.' + groupId);
1604
1605 // Toggle expanded state
1606 if ($button.hasClass('expanded')) {
1607 // Collapse
1608 $chunkRows.slideUp(300);
1609 $button.removeClass('expanded');
1610 } else {
1611 // Expand
1612 $chunkRows.slideDown(300);
1613 $button.addClass('expanded');
1614 }
1615 });
1616
1617 // Handle delete button for chunk groups
1618 $(document).on('click', '.delete-button-group', function(e) {
1619 e.preventDefault();
1620 e.stopPropagation();
1621
1622 const $button = $(this);
1623 const sourceUrl = $button.data('source-url');
1624 const chunkCount = $button.data('chunk-count');
1625 const dataSource = $button.data('data-source');
1626 const botId = $button.data('bot-id');
1627 const nonce = $button.data('nonce');
1628
1629 // Confirm deletion
1630 if (!confirm('Are you sure you want to delete all ' + chunkCount + ' chunks for this URL? This action cannot be undone.')) {
1631 return;
1632 }
1633
1634 // Show loading state
1635 $button.prop('disabled', true);
1636 $button.find('.dashicons').removeClass('dashicons-trash').addClass('dashicons-update spin');
1637
1638 // Make AJAX request to delete all chunks for this URL
1639 $.ajax({
1640 url: ajaxurl,
1641 type: 'POST',
1642 data: {
1643 action: 'mxchat_delete_chunks_by_url',
1644 source_url: sourceUrl,
1645 data_source: dataSource,
1646 bot_id: botId,
1647 nonce: nonce
1648 },
1649 success: function(response) {
1650 if (response.success) {
1651 // Remove the group header row and all chunk rows
1652 const $headerRow = $button.closest('tr');
1653 const groupId = $headerRow.data('group-id');
1654 $('.mxchat-chunk-row.' + groupId).fadeOut(300, function() {
1655 $(this).remove();
1656 });
1657 $headerRow.fadeOut(300, function() {
1658 $(this).remove();
1659 });
1660 } else {
1661 var errorMsg = response.data || 'Unknown error';
1662 if (typeof response.data === 'object' && response.data.message) {
1663 errorMsg = response.data.message;
1664 }
1665 alert('Error deleting chunks: ' + errorMsg);
1666 $button.prop('disabled', false);
1667 $button.find('.dashicons').removeClass('dashicons-update spin').addClass('dashicons-trash');
1668 }
1669 },
1670 error: function(xhr, status, error) {
1671 console.error('AJAX error:', xhr.responseText);
1672 alert('Error deleting chunks: ' + (error || 'Server error'));
1673 $button.prop('disabled', false);
1674 $button.find('.dashicons').removeClass('dashicons-update spin').addClass('dashicons-trash');
1675 }
1676 });
1677 });
1678
1679 // ========================================
1680 // REAL-TIME KNOWLEDGE ENTRIES UPDATE
1681 // ========================================
1682
1683 let lastEntryId = 0;
1684 let entriesPollingInterval = null;
1685 let isEntriesPolling = false;
1686
1687 // Initialize: get the highest ID from the current table
1688 function initializeLastEntryId() {
1689 const $tbody = $('#mxchat-entries-tbody');
1690 if ($tbody.length === 0) return;
1691
1692 // Get the highest ID from the table
1693 $tbody.find('tr').each(function() {
1694 const idText = $(this).find('td:first').text().trim();
1695 const id = parseInt(idText);
1696 if (!isNaN(id) && id > lastEntryId) {
1697 lastEntryId = id;
1698 }
1699 });
1700 }
1701
1702 // Poll for new entries during processing
1703 function startEntriesPolling() {
1704 if (entriesPollingInterval) return; // Already polling
1705
1706 isEntriesPolling = true;
1707
1708 // Fetch immediately, then start interval
1709 fetchNewEntries();
1710
1711 entriesPollingInterval = setInterval(function() {
1712 fetchNewEntries();
1713 }, 3000); // Poll every 3 seconds
1714 }
1715
1716 function stopEntriesPolling() {
1717 if (entriesPollingInterval) {
1718 clearInterval(entriesPollingInterval);
1719 entriesPollingInterval = null;
1720 }
1721 isEntriesPolling = false;
1722 }
1723
1724 // Track Pinecone count for change detection
1725 var lastPineconeCount = 0;
1726 var pineconeRefreshPending = false;
1727
1728 // Fetch new entries from server
1729 function fetchNewEntries() {
1730 if (!mxchatAdmin.entries_nonce) {
1731 return;
1732 }
1733
1734 $.ajax({
1735 url: ajaxurl,
1736 type: 'POST',
1737 data: {
1738 action: 'mxchat_get_recent_entries',
1739 nonce: mxchatAdmin.entries_nonce,
1740 last_id: lastEntryId,
1741 bot_id: mxchatAdmin.bot_id || 'default',
1742 limit: 20
1743 },
1744 success: function(response) {
1745 if (response.success && response.data) {
1746 // Update count
1747 if (response.data.total_count !== undefined) {
1748 updateEntryCount(response.data.total_count);
1749 }
1750
1751 // Handle Pinecone data source differently
1752 if (response.data.data_source === 'pinecone') {
1753 var newCount = response.data.total_count || 0;
1754
1755 // If count changed and we haven't scheduled a refresh yet
1756 if (newCount !== lastPineconeCount && !pineconeRefreshPending) {
1757 lastPineconeCount = newCount;
1758
1759 // Schedule a table refresh after processing completes
1760 // Show a "refresh to see entries" message
1761 var $tbody = $('#mxchat-entries-tbody');
1762 var $refreshNotice = $tbody.find('.mxchat-pinecone-refresh-notice');
1763
1764 if ($refreshNotice.length === 0 && newCount > 0) {
1765 var noticeHtml = '<tr class="mxchat-pinecone-refresh-notice">' +
1766 '<td colspan="4" style="padding: 20px; text-align: center; background: #f0f7ff; border-bottom: 1px solid var(--mxch-card-border);">' +
1767 '<span class="dashicons dashicons-update" style="color: #7873f5; margin-right: 8px;"></span>' +
1768 '<strong>' + newCount + ' entries in Pinecone.</strong> ' +
1769 '<a href="#" class="mxchat-refresh-table-link" style="color: #7873f5; text-decoration: underline;">Refresh to see new entries</a>' +
1770 '</td></tr>';
1771 $tbody.prepend(noticeHtml);
1772
1773 // Handle refresh link click
1774 $tbody.find('.mxchat-refresh-table-link').on('click', function(e) {
1775 e.preventDefault();
1776 location.reload();
1777 });
1778 } else if ($refreshNotice.length > 0) {
1779 // Update the count in existing notice
1780 $refreshNotice.find('strong').text(newCount + ' entries in Pinecone.');
1781 }
1782 }
1783 } else {
1784 // WordPress DB - Track new entries and show refresh notice
1785 // Don't add rows individually during processing as they need to be grouped by source_url
1786 if (response.data.entries && response.data.entries.length > 0) {
1787 // Update last ID to track progress
1788 if (response.data.max_id > lastEntryId) {
1789 lastEntryId = response.data.max_id;
1790 }
1791
1792 // Show/update refresh notice (similar to Pinecone handling)
1793 var $tbody = $('#mxchat-entries-tbody');
1794 var $refreshNotice = $tbody.find('.mxchat-wordpress-refresh-notice');
1795 var newCount = response.data.total_count || 0;
1796
1797 if ($refreshNotice.length === 0 && newCount > 0) {
1798 var noticeHtml = '<tr class="mxchat-wordpress-refresh-notice mxchat-new-entry">' +
1799 '<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);">' +
1800 '<span class="dashicons dashicons-update spin" style="color: #7873f5; margin-right: 8px;"></span>' +
1801 '<strong style="color: var(--mxch-text-primary);">Processing... <span class="mxchat-processing-count">' + newCount + '</span> entries</strong>' +
1802 '</td></tr>';
1803 $tbody.prepend(noticeHtml);
1804 } else if ($refreshNotice.length > 0) {
1805 // Update the count in existing notice
1806 $refreshNotice.find('.mxchat-processing-count').text(newCount);
1807 }
1808 } else {
1809 // Update last ID even if no new entries
1810 if (response.data.max_id > lastEntryId) {
1811 lastEntryId = response.data.max_id;
1812 }
1813 }
1814 }
1815 }
1816 },
1817 error: function(xhr, status, error) {
1818 console.error('MxChat: Error fetching new entries:', error, xhr.responseText);
1819 }
1820 });
1821 }
1822
1823 // Update the entry count display
1824 function updateEntryCount(count) {
1825 // Update main table count
1826 const $countSpan = $('#mxchat-entry-count');
1827 if ($countSpan.length) {
1828 $countSpan.text('(' + count + ')');
1829
1830 // Flash animation to indicate update
1831 $countSpan.addClass('mxchat-count-updated');
1832 setTimeout(function() {
1833 $countSpan.removeClass('mxchat-count-updated');
1834 }, 1000);
1835 }
1836
1837 // Update sidebar badge count
1838 const $sidebarCount = $('#mxchat-sidebar-count');
1839 if ($sidebarCount.length) {
1840 $sidebarCount.text(count);
1841
1842 // Flash animation for sidebar too
1843 $sidebarCount.addClass('mxchat-count-updated');
1844 setTimeout(function() {
1845 $sidebarCount.removeClass('mxchat-count-updated');
1846 }, 1000);
1847 }
1848 }
1849
1850 // Refresh the knowledge base table via AJAX pagination
1851 // This ensures entries are properly grouped by source_url
1852 function refreshKnowledgeBaseTable() {
1853 var $paginationWrapper = $('#mxchat-kb-pagination');
1854 var $tbody = $('#mxchat-entries-tbody');
1855
1856 if ($tbody.length === 0) {
1857 return;
1858 }
1859
1860 // Remove any processing notice
1861 $tbody.find('.mxchat-wordpress-refresh-notice, .mxchat-pinecone-refresh-notice').remove();
1862
1863 // Show loading state
1864 $tbody.css('opacity', '0.5');
1865
1866 $.ajax({
1867 url: ajaxurl,
1868 type: 'POST',
1869 data: {
1870 action: 'mxchat_paginate_entries',
1871 nonce: mxchatAdmin.entries_nonce,
1872 bot_id: mxchatAdmin.bot_id || 'default',
1873 page: 1 // Always go to first page to see newest entries
1874 },
1875 success: function(response) {
1876 $tbody.css('opacity', '1');
1877
1878 if (response.success && response.data) {
1879 // Update the table body with properly grouped HTML
1880 $tbody.html(response.data.html);
1881
1882 // Update the pagination
1883 if ($paginationWrapper.length) {
1884 if (response.data.pagination_html) {
1885 // Add pagination content and styling
1886 $paginationWrapper.html(response.data.pagination_html);
1887 $paginationWrapper.attr('style', 'padding: 16px; border-top: 1px solid var(--mxch-card-border); text-align: center;');
1888 } else {
1889 // No pagination needed - clear and hide
1890 $paginationWrapper.html('');
1891 $paginationWrapper.attr('style', '');
1892 }
1893 }
1894
1895 // Update count display
1896 if (response.data.total_count !== undefined) {
1897 updateEntryCount(response.data.total_count);
1898 }
1899 }
1900 },
1901 error: function(xhr, status, error) {
1902 $tbody.css('opacity', '1');
1903 console.error('MxChat: Error refreshing table:', error);
1904 }
1905 });
1906 }
1907
1908 // Expose refreshKnowledgeBaseTable for external access (content-selector.js)
1909 window.refreshKnowledgeBaseTable = refreshKnowledgeBaseTable;
1910
1911 // Add new entries to the table (kept for backwards compatibility but not used during processing)
1912 function addNewEntriesToTable(entries) {
1913 const $tbody = $('#mxchat-entries-tbody');
1914 if ($tbody.length === 0) return;
1915
1916 // Remove "no entries" message if present
1917 const $noEntries = $tbody.find('td[colspan="4"]').closest('tr');
1918 if ($noEntries.length) {
1919 $noEntries.remove();
1920 }
1921
1922 // Add entries in reverse order (oldest first, so newest ends up at top)
1923 entries.reverse().forEach(function(entry) {
1924 // Check if entry already exists
1925 if ($tbody.find('tr[data-entry-id="' + entry.id + '"]').length > 0) {
1926 return;
1927 }
1928
1929 const sourceHtml = entry.has_link
1930 ? '<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>'
1931 : '<span style="color: var(--mxch-text-muted);">Manual</span>';
1932
1933 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;
1934
1935 // Check if content needs expand button (content longer than preview)
1936 const needsExpand = entry.content_length > entry.preview_length;
1937
1938 // Build accordion-style content cell (matching initial page load structure)
1939 let contentHtml = '<div class="mxchat-accordion-wrapper">' +
1940 '<div class="mxchat-content-preview">' +
1941 '<span class="preview-text">' + entry.preview + '</span>';
1942
1943 if (needsExpand) {
1944 contentHtml += '<button class="mxchat-expand-toggle" type="button">' +
1945 '<span class="dashicons dashicons-arrow-down-alt2"></span>' +
1946 '</button>';
1947 }
1948
1949 contentHtml += '</div>' +
1950 '<div class="mxchat-content-full" style="display: none;">' +
1951 '<div class="content-view">' + entry.full_content + '</div>' +
1952 '</div>' +
1953 '</div>';
1954
1955 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;">' +
1956 '<td style="padding: 12px 16px; font-size: 13px;">' + entry.id + '</td>' +
1957 '<td class="mxchat-content-cell" style="padding: 12px 16px; font-size: 13px;">' + contentHtml + '</td>' +
1958 '<td class="mxchat-url-cell" style="padding: 12px 16px; font-size: 13px;">' + sourceHtml + '</td>' +
1959 '<td style="padding: 12px 16px; white-space: nowrap;">' +
1960 '<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"' +
1961 ' data-source-url="' + (entry.source_url || '') + '"' +
1962 ' data-entry-id="' + entry.id + '"' +
1963 ' data-data-source="wordpress"' +
1964 ' data-bot-id="' + (entry.bot_id || 'default') + '"' +
1965 ' data-nonce="' + (entry.edit_nonce || '') + '"' +
1966 ' title="Edit content">' +
1967 '<span class="dashicons dashicons-edit" style="font-size: 14px;"></span>' +
1968 '</button>' +
1969 '<a href="' + deleteUrl + '" class="mxch-btn mxch-btn-ghost mxch-btn-sm" style="color: var(--mxch-error);" onclick="return confirm(\'Delete this entry?\');">' +
1970 '<span class="dashicons dashicons-trash" style="font-size: 14px;"></span>' +
1971 '</a>' +
1972 '</td>' +
1973 '</tr>');
1974
1975 // Add highlight class and prepend to tbody
1976 $row.addClass('mxchat-new-entry');
1977 $tbody.prepend($row);
1978 $row.slideDown(300);
1979
1980 // Remove highlight after animation
1981 setTimeout(function() {
1982 $row.removeClass('mxchat-new-entry');
1983 }, 2000);
1984 });
1985 }
1986
1987 // Initialize entry ID tracking
1988 initializeLastEntryId();
1989
1990 // Check on page load if there's already active processing (e.g., page was refreshed during processing)
1991 if ($('.mxchat-status-card').length > 0) {
1992 // Check if processing is active via AJAX
1993 $.ajax({
1994 url: ajaxurl,
1995 type: 'POST',
1996 data: {
1997 action: 'mxchat_get_status_updates',
1998 nonce: mxchatAdmin.status_nonce
1999 },
2000 success: function(response) {
2001 if (response.is_processing) {
2002 startEntriesPolling();
2003 }
2004 }
2005 });
2006 }
2007
2008 // Expose functions for external access
2009 window.mxchatEntriesPolling = {
2010 start: startEntriesPolling,
2011 stop: stopEntriesPolling,
2012 fetch: fetchNewEntries
2013 };
2014
2015 // ========================================
2016 // SITEMAP DETECTION FUNCTIONALITY
2017 // ========================================
2018
2019 let sitemapDetectionInitialized = false;
2020
2021 /**
2022 * Initialize sitemap detection when Sitemap Import is clicked
2023 */
2024 function initSitemapDetection() {
2025 if (sitemapDetectionInitialized) return;
2026
2027 const loadingEl = document.getElementById('mxchat-sitemaps-loading');
2028 const detectedEl = document.getElementById('mxchat-detected-sitemaps');
2029 const noSitemapsEl = document.getElementById('mxchat-no-sitemaps');
2030 const listEl = document.getElementById('mxchat-sitemaps-list');
2031 const refreshBtn = document.getElementById('mxchat-refresh-sitemaps');
2032 const nonceEl = document.getElementById('mxchat-detect-sitemaps-nonce');
2033
2034 if (!loadingEl || !nonceEl) return;
2035
2036 sitemapDetectionInitialized = true;
2037
2038 function detectSitemaps() {
2039 // Show loading
2040 loadingEl.style.display = 'block';
2041 if (detectedEl) detectedEl.style.display = 'none';
2042 if (noSitemapsEl) noSitemapsEl.style.display = 'none';
2043
2044 // Disable refresh button
2045 if (refreshBtn) {
2046 refreshBtn.disabled = true;
2047 var refreshIcon = refreshBtn.querySelector('.dashicons');
2048 if (refreshIcon) refreshIcon.classList.add('spin');
2049 }
2050
2051 $.ajax({
2052 url: ajaxurl,
2053 type: 'POST',
2054 data: {
2055 action: 'mxchat_detect_sitemaps',
2056 nonce: nonceEl.value
2057 },
2058 timeout: 60000, // 60 second timeout for slow servers
2059 success: function(data) {
2060 loadingEl.style.display = 'none';
2061
2062 // Re-enable refresh button
2063 if (refreshBtn) {
2064 refreshBtn.disabled = false;
2065 var refreshIcon = refreshBtn.querySelector('.dashicons');
2066 if (refreshIcon) refreshIcon.classList.remove('spin');
2067 }
2068
2069 if (data.success && data.data && data.data.sitemaps && data.data.sitemaps.length > 0) {
2070 renderSitemaps(data.data.sitemaps);
2071 if (detectedEl) detectedEl.style.display = 'block';
2072 } else {
2073 if (noSitemapsEl) {
2074 noSitemapsEl.style.display = 'block';
2075 $(noSitemapsEl).data('was-shown', true);
2076 }
2077 }
2078 },
2079 error: function(xhr, status, error) {
2080 loadingEl.style.display = 'none';
2081 if (noSitemapsEl) {
2082 noSitemapsEl.style.display = 'block';
2083 $(noSitemapsEl).data('was-shown', true);
2084 }
2085 if (refreshBtn) {
2086 refreshBtn.disabled = false;
2087 var refreshIcon = refreshBtn.querySelector('.dashicons');
2088 if (refreshIcon) refreshIcon.classList.remove('spin');
2089 }
2090 }
2091 });
2092 }
2093
2094 function renderSitemaps(sitemaps) {
2095 if (!listEl) return;
2096
2097 var html = '';
2098 var botIdEl = document.getElementById('mxchat-sitemap-bot-id');
2099 var botId = botIdEl ? botIdEl.value : '';
2100
2101 sitemaps.forEach(function(sitemap) {
2102 if (sitemap.type === 'index' && sitemap.sub_sitemaps && sitemap.sub_sitemaps.length > 0) {
2103 // Render sitemap index with sub-sitemaps
2104 html += '<div class="mxchat-sitemap-group">';
2105 html += '<div class="mxchat-sitemap-group-header">';
2106 html += '<div style="display: flex; align-items: center; gap: 10px;">';
2107 html += '<span class="dashicons dashicons-arrow-right-alt2" style="transition: transform 0.2s;"></span>';
2108 html += '<span class="dashicons dashicons-list-view" style="color: #7873f5;"></span>';
2109 html += '<div>';
2110 html += '<strong style="font-size: 13px;">Sitemap Index</strong>';
2111 html += '<span style="color: #666; font-size: 12px; margin-left: 8px;">' + sitemap.source + '</span>';
2112 html += '</div>';
2113 html += '</div>';
2114 html += '<span style="background: rgba(120, 115, 245, 0.1); color: #7873f5; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600;">';
2115 html += sitemap.sub_sitemaps.length + ' sitemaps';
2116 html += '</span>';
2117 html += '</div>';
2118 html += '<div class="mxchat-sitemap-sub-list">';
2119 sitemap.sub_sitemaps.forEach(function(sub) {
2120 html += renderSitemapRow(sub, botId, true);
2121 });
2122 html += '</div>';
2123 html += '</div>';
2124 } else if (sitemap.type !== 'index') {
2125 // Render standalone sitemap
2126 html += renderSitemapRow(sitemap, botId, false);
2127 }
2128 });
2129
2130 listEl.innerHTML = html;
2131
2132 // Add click handlers for group toggles
2133 $(listEl).find('.mxchat-sitemap-group-header').on('click', function() {
2134 var $group = $(this).parent();
2135 var $subList = $group.find('.mxchat-sitemap-sub-list');
2136 var $arrow = $(this).find('.dashicons-arrow-right-alt2');
2137
2138 $group.toggleClass('expanded');
2139
2140 if ($group.hasClass('expanded')) {
2141 $subList.slideDown(200);
2142 $arrow.css('transform', 'rotate(90deg)');
2143 } else {
2144 $subList.slideUp(200);
2145 $arrow.css('transform', 'rotate(0deg)');
2146 }
2147 });
2148
2149 // Add click handlers for process buttons
2150 $(listEl).find('.mxchat-process-sitemap-btn').on('click', function() {
2151 var url = $(this).data('url');
2152 var type = $(this).data('sitemap-type');
2153 processSitemap(url, type, this);
2154 });
2155 }
2156
2157 function renderSitemapRow(sitemap, botId, isSubItem) {
2158 var typeLabels = {
2159 'content': 'Content',
2160 'taxonomy': 'Taxonomy',
2161 'author': 'Authors'
2162 };
2163 var typeLabel = typeLabels[sitemap.type] || sitemap.type;
2164 var displayName = sitemap.name || sitemap.url.split('/').pop();
2165 var urlCount = sitemap.url_count || 0;
2166 var paddingLeft = isSubItem ? '40px' : '16px';
2167
2168 var html = '<div class="mxchat-sitemap-row" style="padding-left: ' + paddingLeft + ';">';
2169 html += '<div style="display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0;">';
2170 html += '<span class="dashicons dashicons-media-text" style="color: #666; flex-shrink: 0;"></span>';
2171 html += '<div style="min-width: 0; flex: 1;">';
2172 html += '<div style="font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="' + sitemap.url + '">';
2173 html += displayName;
2174 html += '</div>';
2175 html += '<div style="font-size: 11px; color: #666;">';
2176 html += '<span style="background: #f0f0f0; padding: 1px 6px; border-radius: 3px; margin-right: 8px;">' + typeLabel + '</span>';
2177 if (urlCount > 0) {
2178 html += urlCount + ' URLs';
2179 }
2180 html += '</div>';
2181 html += '</div>';
2182 html += '</div>';
2183 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 + '">';
2184 html += '<span class="dashicons dashicons-download" style="font-size: 14px; margin-top: 3px;"></span> Process';
2185 html += '</button>';
2186 html += '</div>';
2187
2188 return html;
2189 }
2190
2191 function processSitemap(url, type, buttonEl) {
2192 var $button = $(buttonEl);
2193 var originalHtml = $button.html();
2194
2195 // Update button to show loading
2196 $button.prop('disabled', true);
2197 $button.html('<span class="dashicons dashicons-update spin" style="font-size: 14px; margin-top: 3px;"></span> Processing...');
2198
2199 // Fill in the sitemap URL form and submit
2200 var $form = $('#mxchat-url-form');
2201 var $urlInput = $('#sitemap_url');
2202 var $importType = $('#import_type');
2203
2204 if ($urlInput.length) {
2205 $urlInput.val(url);
2206 }
2207
2208 if ($importType.length) {
2209 $importType.val('sitemap');
2210 }
2211
2212 // Add a hidden submit field if not present (required by the PHP handler)
2213 if ($form.find('input[name="submit_sitemap"]').length === 0) {
2214 $form.append('<input type="hidden" name="submit_sitemap" value="1">');
2215 }
2216
2217 // Submit the form
2218 $form.submit();
2219 }
2220
2221
2222 // Refresh button handler
2223 if (refreshBtn) {
2224 $(refreshBtn).on('click', detectSitemaps);
2225 }
2226
2227 // Start detection
2228 detectSitemaps();
2229 }
2230
2231 // Expose initSitemapDetection globally so it can be called from the import options handler
2232 window.mxchatInitSitemapDetection = initSitemapDetection;
2233
2234 // ========================================
2235 // ADMIN NOTICE DISMISS FUNCTIONALITY
2236 // ========================================
2237
2238 // Initialize dismissible notices - add dismiss button if missing
2239 function initDismissibleNotices() {
2240 $('.notice.is-dismissible').each(function() {
2241 var $notice = $(this);
2242
2243 // Skip if already has a dismiss button
2244 if ($notice.find('.notice-dismiss').length > 0) {
2245 return;
2246 }
2247
2248 // Add dismiss button
2249 var $dismissButton = $('<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>');
2250 $notice.append($dismissButton);
2251 });
2252 }
2253
2254 // Initialize on page load
2255 initDismissibleNotices();
2256
2257 // Use event delegation for dismiss button clicks - works for existing and dynamically added notices
2258 $(document).on('click', '.notice.is-dismissible .notice-dismiss', function(e) {
2259 e.preventDefault();
2260 e.stopPropagation();
2261
2262 var $notice = $(this).closest('.notice');
2263 $notice.fadeTo(100, 0, function() {
2264 $notice.slideUp(100, function() {
2265 $notice.remove();
2266 });
2267 });
2268 });
2269
2270 // Re-initialize when new notices are added dynamically (e.g., via AJAX)
2271 $(document).on('DOMNodeInserted', function(e) {
2272 if ($(e.target).hasClass('notice') && $(e.target).hasClass('is-dismissible')) {
2273 setTimeout(initDismissibleNotices, 10);
2274 }
2275 });
2276
2277 // ========================================
2278 // CUSTOM META-KEY DISCOVERY PICKER (plan-mxchat-20260709-fe8e4e)
2279 // Mirror the ACF picker's discover-and-click UX for non-ACF post meta: scan
2280 // published content for meta keys, render clickable chips, and append chosen
2281 // keys into the existing whitelist textarea (its autosave then persists them).
2282 // ========================================
2283 (function() {
2284 var $scanBtn = $('#mxchat-scan-meta-btn');
2285 if (!$scanBtn.length) { return; }
2286
2287 var $results = $('#mxchat-meta-scan-results');
2288 var $textarea = $('#mxchat_custom_meta_whitelist');
2289 var $internal = $('#mxchat-scan-internal');
2290 var $label = $scanBtn.find('.mxchat-scan-label');
2291 var defaultLabel = $label.text();
2292
2293 function currentKeys() {
2294 return ($textarea.val() || '')
2295 .split('\n')
2296 .map(function(k) { return k.trim(); })
2297 .filter(function(k) { return k.length; });
2298 }
2299
2300 function addKey(key) {
2301 var keys = currentKeys();
2302 if (keys.indexOf(key) !== -1) { return false; }
2303 keys.push(key);
2304 $textarea.val(keys.join('\n'));
2305 // Fire the existing autosave (bound on 'change' in mxchat-admin.js).
2306 $textarea.trigger('change');
2307 return true;
2308 }
2309
2310 function escapeHtml(s) {
2311 return String(s).replace(/[&<>"']/g, function(c) {
2312 return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
2313 });
2314 }
2315
2316 function humanize(key) {
2317 var h = key.replace(/^_+/, '').replace(/[_-]+/g, ' ').trim();
2318 return h.length ? h.charAt(0).toUpperCase() + h.slice(1) : key;
2319 }
2320
2321 function renderChips(keys) {
2322 if (!keys || !keys.length) {
2323 $results.html('<p class="mxchat-meta-scan-empty">' +
2324 'No custom meta keys found on your published content. Try enabling internal keys, or add keys manually below.' +
2325 '</p>');
2326 return;
2327 }
2328 var existing = currentKeys();
2329 var html = '<div class="mxchat-meta-chip-grid">';
2330 keys.forEach(function(item) {
2331 var already = existing.indexOf(item.key) !== -1;
2332 html += '<button type="button" class="mxchat-meta-chip' + (already ? ' is-added' : '') +
2333 '" data-key="' + escapeHtml(item.key) + '"' + (already ? ' aria-pressed="true"' : '') + '>' +
2334 '<span class="mxchat-meta-chip-main">' +
2335 '<span class="mxchat-meta-chip-name">' + escapeHtml(humanize(item.key)) + '</span>' +
2336 '<span class="mxchat-meta-chip-key">' + escapeHtml(item.key) + '</span>' +
2337 '</span>' +
2338 '<span class="mxchat-meta-chip-meta">on ' + item.count + ' post' + (item.count === 1 ? '' : 's') +
2339 (item.sample ? ' · e.g. ' + escapeHtml(item.sample) : '') + '</span>' +
2340 '<span class="mxchat-meta-chip-tick">' + (already ? '✓ Added' : '+ Add') + '</span>' +
2341 '</button>';
2342 });
2343 html += '</div>';
2344 $results.html(html);
2345 }
2346
2347 $scanBtn.on('click', function() {
2348 var nonce = $scanBtn.data('nonce') ||
2349 (window.mxchatAdmin && mxchatAdmin.settings_nonce) || '';
2350 var ajaxUrl = (window.mxchatAdmin && (mxchatAdmin.ajax_url || mxchatAdmin.ajaxurl)) ||
2351 window.ajaxurl || '/wp-admin/admin-ajax.php';
2352
2353 $scanBtn.prop('disabled', true);
2354 $label.text('Scanning…');
2355 $results.html('<p class="mxchat-meta-scan-loading">Scanning your content for meta keys…</p>');
2356
2357 $.post(ajaxUrl, {
2358 action: 'mxchat_scan_custom_meta_keys',
2359 _ajax_nonce: nonce,
2360 include_internal: $internal.is(':checked') ? '1' : '0'
2361 }).done(function(resp) {
2362 if (resp && resp.success && resp.data && resp.data.keys) {
2363 renderChips(resp.data.keys);
2364 } else {
2365 var msg = (resp && resp.data && resp.data.message) ? resp.data.message : 'Scan failed. Please try again.';
2366 $results.html('<p class="mxchat-meta-scan-empty">' + escapeHtml(msg) + '</p>');
2367 }
2368 }).fail(function() {
2369 $results.html('<p class="mxchat-meta-scan-empty">Scan request failed. Please try again.</p>');
2370 }).always(function() {
2371 $scanBtn.prop('disabled', false);
2372 $label.text(defaultLabel);
2373 });
2374 });
2375
2376 // Chip click -> add the raw key to the whitelist textarea (dedup) + autosave.
2377 $results.on('click', '.mxchat-meta-chip', function() {
2378 var $chip = $(this);
2379 if ($chip.hasClass('is-added')) { return; }
2380 var key = String($chip.data('key'));
2381 if (addKey(key)) {
2382 $chip.addClass('is-added').attr('aria-pressed', 'true');
2383 $chip.find('.mxchat-meta-chip-tick').text('✓ Added');
2384 }
2385 });
2386 })();
2387
2388 // ========================================
2389 // VIDEO CARDS — threshold follows the master switch (plan f52492)
2390 // ========================================
2391 // Presentation only. The server gate does NOT consult the threshold when
2392 // the switch is off, so hiding the field never changes behavior — it just
2393 // stops the page offering a number that is currently inert. Mirrors the
2394 // YouTube import form's own show/hide idiom on this same page. Autosave is
2395 // untouched: both fields keep their own change handler in mxchat-admin.js.
2396 $('#mxchat_video_embed_enabled').on('change', function() {
2397 $('#mxchat-video-embed-threshold-field').toggle($(this).is(':checked'));
2398 });
2399 });