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 / content-selector.js

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

1,461 lines 69.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 jQuery(document).ready(function($) {
2 // Modal elements
3 const $modal = $('#mxchat-kb-content-selector-modal');
4 const $openButton = $('#mxchat-open-content-selector');
5 const $closeButtons = $('.mxchat-kb-modal-close');
6 const $contentList = $('.mxchat-kb-content-list');
7 const $loading = $('.mxchat-kb-loading');
8 const $pagination = $('.mxchat-kb-pagination');
9 const $processButton = $('#mxchat-kb-process-selected');
10 const $selectAll = $('#mxchat-kb-select-all');
11 const $selectionCount = $('.mxchat-kb-selection-count');
12 // ACF→PDF extraction is an install-level setting (Knowledge → ACF Fields);
13 // the server reads the stored option — nothing to collect in this modal.
14
15 // Filter elements
16 const $searchInput = $('#mxchat-kb-content-search');
17 const $typeFilter = $('#mxchat-kb-content-type-filter');
18 const $statusFilter = $('#mxchat-kb-content-status-filter');
19 const $processedFilter = $('#mxchat-kb-processed-filter');
20
21 // Current state - using let for variables that change
22 let currentPage = 1;
23 let totalPages = 1;
24 let selectedItems = new Set();
25 let allItems = [];
26
27 // Open modal when WordPress import button is clicked
28 $openButton.on('click', function() {
29 $modal.show();
30 // Reset to first page when opening the modal
31 currentPage = 1;
32 loadContent();
33 });
34
35 // Close modal
36 $closeButtons.on('click', function() {
37 $modal.hide();
38 });
39 // Handle import option box clicks (for non-WordPress options)
40 $('.mxchat-import-box').on('click', function() {
41 const $box = $(this);
42 const option = $box.data('option');
43
44 // Skip if this is the WordPress option (it has its own handler)
45 if (option === 'wordpress') {
46 return;
47 }
48
49 // Update active state
50 $('.mxchat-import-box').removeClass('active');
51 $box.addClass('active');
52
53 // Hide all input areas
54 $('#mxchat-url-input-area, #mxchat-content-input-area, #mxchat-pdf-upload-area, #mxchat-document-upload-area, #mxchat-youtube-input-area').hide();
55
56 // Hide sitemap-specific sections (but NOT for sitemap option - let detection logic handle it)
57 if (option !== 'sitemap') {
58 $('#mxchat-detected-sitemaps, #mxchat-no-sitemaps, #mxchat-sitemaps-loading').hide();
59 }
60
61 // Handle different import options
62 switch (option) {
63 case 'pdf-url':
64 case 'sitemap':
65 case 'url':
66 // Show URL input area with appropriate placeholder
67 $('#mxchat-url-input-area').show();
68 $('#sitemap_url').attr('placeholder', $box.data('placeholder'));
69 $('#import_type').val(option === 'pdf-url' ? 'pdf' : option);
70
71 // UPDATED: Add or update bot_id hidden field for URL forms
72 updateBotIdInForm('#mxchat-url-form');
73
74 // Update the description text based on the import type
75 let descriptionText = '';
76 if (option === 'pdf-url') {
77 descriptionText = 'Import a PDF document by entering its URL above. PDFs are processed via cron job. If processing does not start, you can manually process batch 5 pages at a time.';
78 } else if (option === 'sitemap') {
79 descriptionText = 'Enter a content-specific sub-sitemap URL, not the sitemap index. Sitemaps are processed via cron job. If processing does not start, you can manually process batch 5 pages at a time.';
80 // Re-show sitemap sections if they were previously loaded
81 const $sitemapsList = $('#mxchat-sitemaps-list');
82 if ($sitemapsList.children().length > 0) {
83 // Sitemaps were already loaded, just show the container
84 $('#mxchat-detected-sitemaps').show();
85 } else if ($('#mxchat-no-sitemaps').data('was-shown')) {
86 // No sitemaps message was shown before
87 $('#mxchat-no-sitemaps').show();
88 }
89 // Note: If neither condition is true, initSitemapDetection will show loading state
90 } else if (option === 'url') {
91 descriptionText = 'Import content from any webpage by entering its URL.';
92 }
93 $('#url-description-text').text(descriptionText);
94 break;
95
96 case 'content':
97 // Show content input area
98 $('#mxchat-content-input-area').show();
99
100 // UPDATED: Add or update bot_id hidden field for content forms
101 updateBotIdInForm('#mxchat-content-form');
102 break;
103
104 case 'pdf-upload':
105 // Show PDF file upload area
106 $('#mxchat-pdf-upload-area').show();
107
108 // Add or update bot_id hidden field for PDF upload form
109 updateBotIdInForm('#mxchat-pdf-upload-form');
110 break;
111
112 case 'document-upload':
113 // Show document (.docx/.txt/.md) upload area
114 $('#mxchat-document-upload-area').show();
115
116 // Add or update bot_id hidden field for the document upload form
117 updateBotIdInForm('#mxchat-document-upload-form');
118 break;
119
120 case 'youtube':
121 // Show YouTube import area
122 $('#mxchat-youtube-input-area').show();
123
124 // Add or update bot_id hidden field for YouTube form
125 updateBotIdInForm('#mxchat-youtube-form');
126 break;
127 }
128 });
129
130 // YouTube import: toggle the manual-description box with the mode radios, and
131 // only require the textarea when "Write my own" is selected.
132 $(document).on('change', 'input[name="youtube_description_mode"]', function() {
133 const manual = $('input[name="youtube_description_mode"]:checked').val() === 'manual';
134 $('#mxchat-youtube-description-field').toggle(manual);
135 $('#mxchat-youtube-description').prop('required', manual);
136 });
137
138 // If the server bounced back with prefill state (auto-import found no
139 // transcript), reopen the YouTube form in manual mode ready to augment.
140 if ($('#mxchat-youtube-form').data('yt-prefill')) {
141 $('.mxchat-import-box[data-option="youtube"]').trigger('click');
142 $('input[name="youtube_description_mode"]').trigger('change');
143 $('#mxchat-youtube-description').trigger('focus');
144 }
145
146 // Helper function to add/update bot_id hidden field in forms
147 function updateBotIdInForm(formSelector) {
148 const $form = $(formSelector);
149 if ($form.length === 0) return;
150
151 // Get current bot_id from the bot selector dropdown
152 const currentBotId = $('#mxchat-bot-selector').val();
153
154 // Only add bot_id field if multi-bot is active and bot is not 'default'
155 if (currentBotId && currentBotId !== 'default') {
156 // Remove existing bot_id field if it exists
157 $form.find('input[name="bot_id"]').remove();
158
159 // Add new bot_id field
160 $form.append('<input type="hidden" name="bot_id" value="' + currentBotId + '">');
161
162 console.log('Updated bot_id in form ' + formSelector + ' to: ' + currentBotId);
163 } else {
164 // Remove bot_id field if bot is default
165 $form.find('input[name="bot_id"]').remove();
166 }
167 }
168
169 // Load content via AJAX
170 function loadContent() {
171 $loading.show();
172 $contentList.find('.mxchat-kb-content-item').remove();
173
174 const data = {
175 action: 'mxchat_get_content_list',
176 nonce: mxchatSelector.nonce,
177 page: currentPage,
178 per_page: 100,
179 search: $searchInput.val(),
180 post_type: $typeFilter.val(),
181 post_status: $statusFilter.val(),
182 processed_filter: $processedFilter.val()
183 };
184
185 //console.log('Loading content for page', currentPage, 'with filters:', data);
186
187 $.ajax({
188 url: mxchatSelector.ajaxurl,
189 data: data,
190 method: 'GET',
191 dataType: 'json',
192 success: function(response) {
193 $loading.hide();
194
195 if (response.success && response.data.items && response.data.items.length > 0) {
196 // Store the items directly
197 let items = response.data.items;
198
199 if (items.length > 0) {
200 renderContentItems(items);
201 renderPagination(parseInt(response.data.current_page), parseInt(response.data.total_pages));
202
203 // Update state
204 allItems = items;
205 totalPages = parseInt(response.data.total_pages);
206 currentPage = parseInt(response.data.current_page);
207
208 // Update select all checkbox based on current selection
209 updateSelectAllState();
210 } else {
211 displayNoResults($processedFilter.val());
212 }
213 } else {
214 displayNoResults($processedFilter.val());
215 }
216 },
217 error: function(xhr, status, error) {
218 $loading.hide();
219 console.error('AJAX Error:', status, error);
220 $contentList.html('<div class="mxchat-kb-error">Error loading content. Please try again.</div>');
221 // Clear pagination on error
222 $pagination.empty();
223 }
224 });
225 }
226
227 // Helper function to display appropriate "no results" message
228 function displayNoResults(processedStatus) {
229 let message = 'No content found matching your criteria.';
230
231 if (processedStatus === 'processed') {
232 message = 'No content found in knowledge base.';
233 } else if (processedStatus === 'unprocessed') {
234 message = 'All content is already in knowledge base.';
235 }
236
237 $contentList.html('<div class="mxchat-kb-no-results">' + message + '</div>');
238 // Clear pagination when no results
239 $pagination.empty();
240 }
241
242 // Render content items
243 function renderContentItems(items) {
244 let html = '';
245
246 items.forEach(function(item) {
247 const isSelected = selectedItems.has(item.id);
248 const isProcessed = item.already_processed;
249 const chunkCount = item.chunk_count || 0;
250
251 // Updated badge text - include chunk count if > 1
252 let badgeText = 'Not In Knowledge Base';
253 if (isProcessed) {
254 badgeText = chunkCount > 1 ? `In Knowledge Base (${chunkCount} chunks)` : 'In Knowledge Base';
255 }
256 const badgeClass = isProcessed ? 'mxchat-kb-processed-badge' : 'mxchat-kb-unprocessed-badge';
257
258
259 html += `
260 <div class="mxchat-kb-content-item ${isProcessed ? 'processed' : ''}" data-id="${item.id}">
261 <div class="mxchat-kb-content-checkbox">
262 <input type="checkbox" id="content-${item.id}" ${isSelected ? 'checked' : ''}>
263 </div>
264 <div class="mxchat-kb-content-details">
265 <div class="mxchat-kb-content-title">
266 <a href="${item.permalink}" target="_blank">${item.title}</a>
267 <span class="${badgeClass}">${badgeText}</span>
268 ${isProcessed ? '<span class="mxchat-kb-last-updated">Last updated: ' + item.processed_date + '</span>' : ''}
269 </div>
270 <div class="mxchat-kb-content-meta">
271 <span class="mxchat-kb-content-type">${item.type_label || item.type}</span>
272 <span class="mxchat-kb-content-date">${item.date}</span>
273 <span class="mxchat-kb-content-words">${item.size_label ? item.size_label : item.word_count + ' words'}</span>
274 </div>
275 <div class="mxchat-kb-content-excerpt">${item.excerpt}</div>
276 </div>
277 </div>
278 `;
279 });
280
281 $contentList.html(html);
282
283 // Add event listeners for checkboxes using delegation for better performance
284 $contentList.off('change', 'input[type="checkbox"]').on('change', 'input[type="checkbox"]', function() {
285 const $checkbox = $(this);
286 const itemId = parseInt($checkbox.closest('.mxchat-kb-content-item').data('id'));
287
288 if ($checkbox.is(':checked')) {
289 selectedItems.add(itemId);
290 } else {
291 selectedItems.delete(itemId);
292 }
293
294 updateSelection();
295 });
296 }
297
298 // Render pagination - FIXED VERSION
299 function renderPagination(currentPage, totalPages) {
300 // Clear existing pagination first
301 $pagination.empty();
302
303 // Don't render pagination if only one page
304 if (totalPages <= 1) {
305 return;
306 }
307
308 let html = '<div class="mxchat-kb-pagination-links">';
309
310 // Previous button
311 if (currentPage > 1) {
312 html += '<a href="#" class="mxchat-kb-page-link prev" data-page="' + (currentPage - 1) + '">&laquo; Previous</a>';
313 }
314
315 // Page numbers
316 const startPage = Math.max(1, currentPage - 2);
317 const endPage = Math.min(totalPages, startPage + 4);
318
319 for (let i = startPage; i <= endPage; i++) {
320 if (i === currentPage) {
321 html += '<span class="mxchat-kb-page-current">' + i + '</span>';
322 } else {
323 html += '<a href="#" class="mxchat-kb-page-link" data-page="' + i + '">' + i + '</a>';
324 }
325 }
326
327 // Next button
328 if (currentPage < totalPages) {
329 html += '<a href="#" class="mxchat-kb-page-link next" data-page="' + (currentPage + 1) + '">Next &raquo;</a>';
330 }
331
332 html += '</div>';
333
334 $pagination.html(html);
335 }
336
337 // Handle pagination clicks directly on the document
338 $(document).on('click', '.mxchat-kb-page-link', function(e) {
339 e.preventDefault();
340 const newPage = parseInt($(this).data('page'));
341 //console.log('Pagination clicked: changing from page', currentPage, 'to', newPage);
342
343 // Only reload if the page actually changed
344 if (currentPage !== newPage) {
345 currentPage = newPage;
346 loadContent();
347 }
348 });
349
350 // Update selection counts and button state
351 function updateSelection() {
352 const selectedCount = selectedItems.size;
353 $selectionCount.text(selectedCount + ' ' + (selectedCount === 1 ? 'selected' : 'selected'));
354 $('.mxchat-kb-selected-count').text('(' + selectedCount + ')');
355
356 // Show/hide clear all selections link
357 let $clearAllLink = $('.mxchat-kb-clear-all-selections');
358 if (selectedCount > 0) {
359 if ($clearAllLink.length === 0) {
360 $clearAllLink = $('<a href="#" class="mxchat-kb-clear-all-selections" style="margin-left: 10px; font-size: 12px; color: var(--mxch-error, #dc2626);">Clear all</a>');
361 $selectionCount.after($clearAllLink);
362 $clearAllLink.on('click', function(e) {
363 e.preventDefault();
364 selectedItems.clear();
365 $('.mxchat-kb-content-item input[type="checkbox"]').prop('checked', false);
366 updateSelection();
367 });
368 }
369 $clearAllLink.show();
370 } else {
371 $clearAllLink.hide();
372 }
373
374 // Determine if any selected items are already processed
375 const hasProcessedItems = Array.from(selectedItems).some(id => {
376 const item = allItems.find(item => item.id === id);
377 return item && item.already_processed;
378 });
379
380 if (selectedCount > 0) {
381 $processButton.prop('disabled', false);
382
383 // Update button text based on selection
384 if (hasProcessedItems && selectedCount === 1) {
385 $processButton.text('Update Selected Content (1)').addClass('update-mode');
386 } else if (hasProcessedItems && selectedCount > 1) {
387 $processButton.text('Process/Update Selected (' + selectedCount + ')').addClass('mixed-mode');
388 } else {
389 $processButton.text('Process Selected Content (' + selectedCount + ')').removeClass('update-mode mixed-mode');
390 }
391 } else {
392 $processButton.prop('disabled', true);
393 $processButton.text('Process Selected Content').removeClass('update-mode mixed-mode');
394 $('.mxchat-kb-selected-count').text('(0)');
395 }
396
397 updateSelectAllState();
398 }
399
400 // Update "Select All" checkbox state
401 function updateSelectAllState() {
402 const availableItems = allItems.length;
403 const selectedAvailableItems = allItems.filter(item => selectedItems.has(item.id)).length;
404
405 if (availableItems === 0) {
406 $selectAll.prop('checked', false);
407 $selectAll.prop('disabled', true);
408 } else if (selectedAvailableItems === availableItems) {
409 $selectAll.prop('checked', true);
410 } else {
411 $selectAll.prop('checked', false);
412 }
413 }
414
415 // Handle Select All checkbox
416 $selectAll.on('change', function() {
417 const isChecked = $(this).is(':checked');
418
419 $contentList.find('.mxchat-kb-content-item input[type="checkbox"]').each(function() {
420 const $checkbox = $(this);
421 const $item = $checkbox.closest('.mxchat-kb-content-item');
422 const itemId = parseInt($item.data('id'));
423
424 $checkbox.prop('checked', isChecked);
425
426 if (isChecked) {
427 selectedItems.add(itemId);
428 } else {
429 selectedItems.delete(itemId);
430 }
431 });
432
433 updateSelection();
434 });
435
436 // Handle search input
437 let searchTimer;
438 $searchInput.on('keyup', function() {
439 clearTimeout(searchTimer);
440 searchTimer = setTimeout(function() {
441 currentPage = 1; // Reset to first page on new search
442 loadContent();
443 }, 500);
444 });
445
446 // Handle filter changes
447 $typeFilter.add($statusFilter).add($processedFilter).on('change', function() {
448 currentPage = 1; // Reset to first page on filter change
449 selectedItems.clear(); // Clear selection when filter changes
450 updateMediaNote();
451 loadContent();
452 });
453
454 // The Media note explains what that list deliberately leaves out; it is
455 // meaningless next to any other type, so it only exists while Media is picked.
456 function updateMediaNote() {
457 const $note = $('#mxchat-kb-media-note');
458 if (!$note.length) {
459 return;
460 }
461 $note.prop('hidden', $typeFilter.val() !== 'attachment');
462 }
463 updateMediaNote();
464
465 // Process selected content
466 $processButton.on('click', function() {
467 if (selectedItems.size === 0) {
468 return;
469 }
470
471 const $button = $(this);
472 $button.prop('disabled', true);
473
474 // Update button text based on mode
475 if ($button.hasClass('update-mode')) {
476 $button.text('Updating...');
477 } else if ($button.hasClass('mixed-mode')) {
478 $button.text('Processing/Updating...');
479 } else {
480 $button.text('Processing...');
481 }
482
483 // Convert selected items to array
484 const selectedPostIds = Array.from(selectedItems);
485 const totalToProcess = selectedPostIds.length;
486 let processed = 0;
487 let updated = 0;
488 let failed = 0;
489 const results = {
490 success: [],
491 updated: [],
492 failed: []
493 };
494
495 // UPDATED: Get current bot_id for WordPress content processing
496 const currentBotId = $('#mxchat-bot-selector').val();
497
498 // Flag to track if processing should be aborted
499 let abortProcessing = false;
500 let currentXHR = null;
501
502 // Create a modal to show progress with stop button
503 const $progressModal = $('<div class="mxchat-kb-processing-overlay">' +
504 '<div class="mxchat-kb-processing-content">' +
505 '<h3>Processing Content</h3>' +
506 '<p class="mxchat-kb-processing-status">Processing 1 of ' + totalToProcess + '...</p>' +
507 '<div class="mxchat-kb-progress-bar"><div class="mxchat-kb-progress-fill" style="width: 0%"></div></div>' +
508 '<p class="mxchat-kb-current-item"></p>' +
509 '<button type="button" class="mxchat-kb-stop-processing mxch-btn mxch-btn-secondary" style="margin-top: 15px;">' +
510 '<span class="dashicons dashicons-controls-pause" style="margin-right: 5px;"></span>Stop Processing</button>' +
511 '</div>' +
512 '</div>');
513
514 $('body').append($progressModal);
515
516 // Handle stop button click
517 $progressModal.find('.mxchat-kb-stop-processing').on('click', function() {
518 abortProcessing = true;
519 $(this).prop('disabled', true).html('<span class="dashicons dashicons-update spin" style="margin-right: 5px;"></span>Stopping...');
520 if (currentXHR) {
521 currentXHR.abort();
522 }
523 });
524
525 // Process posts one by one
526 function processNext(index) {
527 // Check if processing was aborted
528 if (abortProcessing) {
529 finishProcessing(true); // Pass true to indicate abort
530 return;
531 }
532
533 if (index >= selectedPostIds.length) {
534 // All done
535 finishProcessing();
536 return;
537 }
538
539 const postId = selectedPostIds[index];
540 const percent = Math.round((index / totalToProcess) * 100);
541 const item = allItems.find(item => item.id === postId);
542 const isUpdate = item && item.already_processed;
543
544 // Update progress UI
545 $progressModal.find('.mxchat-kb-processing-status')
546 .text((isUpdate ? 'Updating' : 'Processing') + ' ' + (index + 1) + ' of ' + totalToProcess + '...');
547 $progressModal.find('.mxchat-kb-progress-fill').css('width', percent + '%');
548
549 // UPDATED: Prepare AJAX data with bot_id
550 const ajaxData = {
551 action: 'mxchat_process_selected_content',
552 nonce: mxchatSelector.nonce,
553 post_ids: [postId],
554 is_update: isUpdate
555 };
556
557 // Add bot_id if multi-bot is active and not default
558 if (currentBotId && currentBotId !== 'default') {
559 ajaxData.bot_id = currentBotId;
560 }
561
562 // Make AJAX request for this post (store reference for potential abort)
563 currentXHR = $.ajax({
564 url: mxchatSelector.ajaxurl,
565 method: 'POST',
566 data: ajaxData,
567 dataType: 'json',
568 success: function(response) {
569 if (response.success) {
570 if (isUpdate) {
571 updated++;
572 results.updated.push({
573 id: postId,
574 title: response.data.title || ('ID: ' + postId)
575 });
576 } else {
577 processed++;
578 results.success.push({
579 id: postId,
580 title: response.data.title || ('ID: ' + postId)
581 });
582 }
583
584 let successText = 'Successfully ' + (isUpdate ? 'updated' : 'processed') + ': ' + response.data.title;
585 const pdfCount = parseInt(response.data.pdf_extracted_count, 10) || 0;
586 if (pdfCount > 0) {
587 const suffixTpl = (mxchatSelector.i18n && mxchatSelector.i18n.pdfExtractedSuffix) || ' (%d PDF(s) extracted)';
588 successText += suffixTpl.replace('%d', pdfCount);
589 }
590 $progressModal.find('.mxchat-kb-current-item').text(successText);
591 } else {
592 failed++;
593 results.failed.push({
594 id: postId,
595 error: response.data || 'Unknown error'
596 });
597
598 $progressModal.find('.mxchat-kb-current-item')
599 .text('Failed to ' + (isUpdate ? 'update' : 'process') + ' ID: ' + postId);
600 }
601
602 // Process next post
603 setTimeout(function() {
604 processNext(index + 1);
605 }, 500); // Small delay between requests
606 },
607 error: function(xhr, status, error) {
608 failed++;
609 results.failed.push({
610 id: postId,
611 error: error || 'Server error'
612 });
613
614 $progressModal.find('.mxchat-kb-current-item')
615 .text('Error ' + (isUpdate ? 'updating' : 'processing') + ' ID: ' + postId);
616
617 // Process next post
618 setTimeout(function() {
619 processNext(index + 1);
620 }, 500);
621 }
622 });
623 }
624
625 // Function to finish processing and show results
626 function finishProcessing(wasAborted) {
627 // Remove progress modal
628 $progressModal.remove();
629
630 // Determine notification type based on results
631 let notificationClass = 'success';
632 if (wasAborted) {
633 notificationClass = processed > 0 || updated > 0 ? 'warning' : 'info';
634 } else if (failed > 0) {
635 notificationClass = processed > 0 || updated > 0 ? 'warning' : 'error';
636 }
637
638 // Create summary message
639 let resultHTML = '<div class="mxchat-kb-notification ' + notificationClass + '">' +
640 '<h4>';
641
642 if (wasAborted) {
643 resultHTML += 'Processing stopped. ';
644 if (processed > 0 || updated > 0) {
645 resultHTML += 'Completed ' + (processed + updated) + ' of ' + totalToProcess + ' items before stopping';
646 } else {
647 resultHTML += 'No items were processed before stopping';
648 }
649 } else if (processed > 0 && updated > 0) {
650 resultHTML += 'Processed ' + processed + ' new items and updated ' + updated + ' existing items';
651 } else if (processed > 0) {
652 resultHTML += 'Processed ' + processed + ' items successfully';
653 } else if (updated > 0) {
654 resultHTML += 'Updated ' + updated + ' items successfully';
655 } else {
656 resultHTML += 'No items were processed successfully';
657 }
658
659 if (failed > 0 && !wasAborted) {
660 resultHTML += ' with ' + failed + ' failures';
661 }
662
663 resultHTML += '</h4>';
664
665 // Add details if there were failures
666 if (failed > 0) {
667 resultHTML += '<div class="mxchat-kb-results-details">';
668 resultHTML += '<h5>Failed Items:</h5><ul>';
669
670 results.failed.forEach(function(item) {
671 resultHTML += '<li><strong>ID: ' + item.id + '</strong>: ' + item.error + '</li>';
672 });
673
674 resultHTML += '</ul></div>';
675 }
676
677 resultHTML += '</div>';
678
679 // Show results in modal
680 $modal.find('.mxchat-kb-modal-content').prepend($(resultHTML));
681
682 // Clear selection
683 selectedItems.clear();
684 updateSelection();
685
686 // Enable button
687 $button.prop('disabled', false)
688 .text('Process Selected Content')
689 .removeClass('update-mode mixed-mode');
690 $('.mxchat-kb-selected-count').text('(0)');
691
692 // Only reload if there were successful operations
693 if (processed > 0 || updated > 0) {
694 // Refresh the knowledge base table with properly grouped entries
695 if (typeof window.refreshKnowledgeBaseTable === 'function') {
696 window.refreshKnowledgeBaseTable();
697 }
698
699 // Reload content list to update "already processed" status
700 setTimeout(function() {
701 loadContent();
702 }, 1000);
703 }
704 }
705
706 // Start processing the first post
707 processNext(0);
708 });
709
710 // Initialize - Set WordPress as the active option by default
711 $('.mxchat-import-box[data-option="wordpress"]').addClass('active');
712 });
713
714 // Navigation functionality for Knowledge Base page
715 jQuery(document).ready(function($) {
716
717 // Hook into the new navigation system using .mxch-nav-link
718 $(document).on('click', '.mxch-nav-link[data-target], .mxch-mobile-nav-link[data-target]', function() {
719 var target = $(this).data('target');
720
721 // Initialize Pinecone functionality when Pinecone section is activated
722 if (target === 'pinecone') {
723 setTimeout(function() {
724 if (typeof initPineconeFeatures === 'function') {
725 initPineconeFeatures();
726 }
727 }, 100);
728 }
729
730 // Initialize OpenAI Vector Store functionality when Vector Store section is activated
731 if (target === 'openai-vectorstore') {
732 setTimeout(function() {
733 if (typeof initVectorStoreFeatures === 'function') {
734 initVectorStoreFeatures();
735 }
736 }, 100);
737 }
738
739 // Check if Pinecone was changed and we're going to import section
740 if (target === 'import' && sessionStorage.getItem('mxchat_pinecone_changed') === 'true') {
741 sessionStorage.removeItem('mxchat_pinecone_changed');
742
743 // Show refresh notice
744 var $knowledgeCard = $('#import .mxch-card').eq(1);
745 if ($knowledgeCard.length > 0 && $knowledgeCard.find('.notice-warning').length === 0) {
746 var refreshNotice = $('<div class="notice notice-warning" style="margin: 15px 0; padding: 10px 15px;">' +
747 '<p style="margin: 0;">' +
748 '<span class="dashicons dashicons-info" style="color: #f0ad4e; margin-right: 5px;"></span>' +
749 'Database settings have changed. ' +
750 '<a href="#" onclick="location.reload(); return false;" style="font-weight: bold;">Click here to refresh</a> to see the updated knowledge base.' +
751 '</p></div>');
752
753 $knowledgeCard.prepend(refreshNotice);
754 }
755 }
756 });
757
758 // Also check on page load if we're already on one of these sections
759 setTimeout(function() {
760 if ($('#pinecone').is(':visible') || $('#pinecone.active').length > 0) {
761 if (typeof initPineconeFeatures === 'function') {
762 initPineconeFeatures();
763 }
764 }
765
766 if ($('#openai-vectorstore').is(':visible') || $('#openai-vectorstore.active').length > 0) {
767 if (typeof initVectorStoreFeatures === 'function') {
768 initVectorStoreFeatures();
769 }
770 }
771 }, 200);
772 });
773
774 // Pinecone and Vector Store functionality - global functions
775 var initPineconeFeatures, initVectorStoreFeatures;
776
777 (function($) {
778
779 // Helper function to update sidebar badge when integration is toggled
780 function updateSidebarBadge(section, isActive) {
781 // Find the nav item for this section (both desktop and mobile)
782 var $desktopNavItem = $('.mxch-nav-item[data-section="' + section + '"] .mxch-nav-link');
783 var $mobileNavItem = $('.mxch-mobile-nav-link[data-target="' + section + '"]');
784
785 // Remove existing badge if any
786 $desktopNavItem.find('.mxch-active-badge').remove();
787 $mobileNavItem.find('.mxch-active-badge').remove();
788
789 // Add badge if active
790 if (isActive) {
791 var badgeHtml = '<span class="mxch-nav-link-badge mxch-active-badge">Active</span>';
792 $desktopNavItem.append(badgeHtml);
793 $mobileNavItem.append(badgeHtml);
794 }
795 }
796
797 // Pinecone functionality
798 initPineconeFeatures = function() {
799 // Check for either old or new section ID
800 if ($('#pinecone').length === 0 && $('#mxchat-kb-tab-pinecone').length === 0) {
801 return;
802 }
803
804 initPineconeToggle();
805 initPineconeIndexType();
806 initPineconeConnectionTest();
807 checkPineconeCompatibility();
808 };
809
810 // Index type (plan 362c31): the document-index controls on the Pinecone
811 // card — Create index for me, Check index, Migrate, Delete old index —
812 // plus the save gate (a document host must be checked before it saves).
813 function initPineconeIndexType() {
814 var $type = $('input.mxchat-pinecone-index-type');
815 if ($type.length === 0) {
816 return;
817 }
818 var $docs = $('.mxchat-pinecone-docs-settings');
819 var $form = $type.closest('form');
820 var $submit = $form.find('input[type="submit"], button[type="submit"]');
821 var $result = $('#mxchat-pinecone-docs-result');
822 var stateEl = document.getElementById('mxchat-pinecone-docs-state');
823 var state = {};
824 try { state = stateEl ? JSON.parse(stateEl.textContent || '{}') : {}; } catch (e) { state = {}; }
825 var verifiedHost = String(state.verified_host || '').toLowerCase();
826 // 3e83e4: the LIVE chatbot's index type + host as saved, the migration
827 // state as last known, and the index name the page loaded with (so a
828 // suggested "-docs" name can be undone by switching back to Vector).
829 var liveType = state.index_type === 'document' ? 'document' : 'vector';
830 var storedHost = String(state.stored_host || '').toLowerCase();
831 var storedIndex = String(state.stored_index || '');
832 var originalIndexName = $.trim($('#mxchat_pinecone_index').val() || '');
833 var migrationState = (state.migration && state.migration.status) ? state.migration : null;
834 var ajaxUrl = (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.ajax_url) ? mxchatAdmin.ajax_url : ajaxurl;
835 var nonce = (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.settings_nonce) ? mxchatAdmin.settings_nonce :
836 ((typeof mxchatPromptsAdmin !== 'undefined' && mxchatPromptsAdmin.prompts_setting_nonce) ? mxchatPromptsAdmin.prompts_setting_nonce : '');
837
838 function escapeText(text) { return $('<div>').text(String(text == null ? '' : text)).html(); }
839 function notice(kind, text) {
840 $result.html('<div class="mxch-notice mxch-notice-' + kind + '" style="margin: 12px 0;"><span>' + escapeText(text) + '</span></div>').show();
841 }
842 function setStatus(ok, text) {
843 $('#mxchat-pinecone-docs-status').removeClass('mxch-notice-success mxch-notice-warning').addClass(ok ? 'mxch-notice-success' : 'mxch-notice-warning');
844 $('#mxchat-pinecone-docs-status-text').text(text);
845 }
846 function currentType() { return $type.filter(':checked').val() || 'vector'; }
847 function hostValue() {
848 return $.trim($('#mxchat_pinecone_host').val() || '').replace(/^https?:\/\//i, '').replace(/\/+$/, '').toLowerCase();
849 }
850 function fmt(n) { n = parseInt(n, 10) || 0; return n.toLocaleString(); }
851 // Does saving Document right now point the chatbot at an index whose copy is unfinished?
852 function switchBlockedBy() {
853 if (currentType() !== 'document' || liveType === 'document' || !migrationState) { return null; }
854 if (migrationState.status !== 'running') { return null; }
855 if (String(migrationState.target_host || '').toLowerCase() !== hostValue()) { return null; }
856 return migrationState;
857 }
858 function refreshGate() {
859 var isDoc = currentType() === 'document';
860 $docs.toggle(isDoc);
861 var blocked = isDoc && (hostValue() === '' || hostValue() !== verifiedHost);
862 $('#mxchat-pinecone-docs-gate').toggle(blocked);
863 var running = blocked ? null : switchBlockedBy();
864 var $switchGate = $('#mxchat-pinecone-docs-switch-gate');
865 if (running) {
866 $('#mxchat-pinecone-docs-switch-gate-text').text(
867 'The copy into ' + (running.target_name || running.target_host) + ' is still running (' + fmt(running.copied) + ' of ' + fmt(running.total) +
868 ' records). Let it finish before switching the chatbot to the document index.'
869 );
870 $switchGate.show();
871 blocked = !$('#mxchat_pinecone_docs_switch_anyway').is(':checked');
872 } else {
873 $switchGate.hide();
874 $('#mxchat_pinecone_docs_switch_anyway').prop('checked', false);
875 }
876 $submit.prop('disabled', blocked);
877 }
878 // 47c61f: the status notice follows the host in the FIELD, the same
879 // comparison the Save gate makes. The server paints it from the saved
880 // host, which is the classic index for the whole length of a copy, so a
881 // checked document host typed here used to sit under "Not checked yet".
882 // Kept out of refreshGate(): a failed Check sets its own text first and
883 // then calls refreshGate(), and that text must survive until the host changes.
884 function refreshStatus() {
885 var typed = hostValue();
886 if (typed !== '' && typed === verifiedHost) {
887 var dim = parseInt(state.expected_dimension, 10) || 0;
888 setStatus(true, String(state.status_checked || 'Document index checked: %1$s (%2$d dimensions).').replace('%1$s', typed).replace('%2$d', dim));
889 } else {
890 setStatus(false, String(state.status_unchecked || 'Not checked yet. Create a document index below, or enter its host above and click Check index. The index type only saves once the host has been checked.'));
891 }
892 }
893 $('#mxchat_pinecone_docs_switch_anyway').off('change.pineconeDocs').on('change.pineconeDocs', refreshGate);
894 $('#mxchat_pinecone_host').off('input.pineconeDocs change.pineconeDocs').on('input.pineconeDocs change.pineconeDocs', function () { refreshStatus(); refreshGate(); });
895
896 function post(data) {
897 data.nonce = nonce;
898 return $.post(ajaxUrl, data);
899 }
900
901 // ---- Index type change: a document index needs its own name, so when
902 // the field still holds the vector index's name (or is empty) suggest a
903 // free "<name>-docs" from the project; Vector restores the original.
904 var suggestedName = '';
905 function nameIsOurs() {
906 var name = $.trim($('#mxchat_pinecone_index').val() || '');
907 return name === '' || name === storedIndex || name === originalIndexName || name === suggestedName;
908 }
909 // When the host in the field is a checked document index, the name is
910 // that index's own; otherwise a free "<name>-docs" for the create button.
911 function suggestDocsName() {
912 var name = $.trim($('#mxchat_pinecone_index').val() || '');
913 var apiKey = $('#mxchat_pinecone_api_key').val();
914 if (liveType === 'document' || !apiKey || currentType() !== 'document') { return; }
915 if (!nameIsOurs()) { return; } // the owner typed their own
916 var host = (hostValue() !== '' && hostValue() === verifiedHost) ? hostValue() : '';
917 post({ action: 'mxchat_pinecone_docs_suggest_name', base: name || storedIndex, host: host, api_key: apiKey }).done(function (response) {
918 if (response && response.success && response.data && response.data.name && currentType() === 'document' && nameIsOurs()) {
919 suggestedName = response.data.name;
920 $('#mxchat_pinecone_index').val(suggestedName);
921 }
922 });
923 }
924 $type.off('change.pineconeDocs').on('change.pineconeDocs', function () {
925 if (currentType() === 'document') {
926 suggestDocsName();
927 } else if (suggestedName !== '' && $.trim($('#mxchat_pinecone_index').val() || '') === suggestedName) {
928 $('#mxchat_pinecone_index').val(originalIndexName);
929 }
930 refreshGate();
931 });
932 // Pasting the checked document host resolves the name to that index's own.
933 $('#mxchat_pinecone_host').on('change.pineconeDocsName', function () {
934 if (currentType() === 'document' && hostValue() !== '' && hostValue() === verifiedHost) { suggestDocsName(); }
935 });
936 refreshGate();
937 function failMessage(response, fallback) {
938 if (response && response.data) {
939 if (typeof response.data === 'string') { return response.data; }
940 if (response.data.message) { return response.data.message; }
941 }
942 return fallback;
943 }
944
945 // ---- Create index for me
946 $('#mxchat-pinecone-docs-create').off('click.pineconeDocs').on('click.pineconeDocs', function () {
947 var $btn = $(this);
948 var name = $.trim($('#mxchat_pinecone_index').val() || '');
949 if (!name) {
950 notice('error', 'Enter an Index Name first — the document index is created under that name.');
951 return;
952 }
953 $btn.prop('disabled', true).text('Creating…');
954 $result.hide();
955 post({
956 action: 'mxchat_pinecone_docs_create_index',
957 index_name: name,
958 api_key: $('#mxchat_pinecone_api_key').val(),
959 cloud: $('#mxchat_pinecone_docs_cloud').val(),
960 region: $('#mxchat_pinecone_docs_region').val(),
961 language: $('#mxchat_pinecone_docs_language').val()
962 }).done(function (response) {
963 if (response && response.success) {
964 $('#mxchat_pinecone_host').val(response.data.host);
965 $('#mxchat_pinecone_index').val(response.data.name);
966 if (response.data.vector_host && !$.trim($('#mxchat_pinecone_vector_host').val() || '')) {
967 $('#mxchat_pinecone_vector_host').val(response.data.vector_host);
968 }
969 verifiedHost = String(response.data.host || '').toLowerCase();
970 setStatus(true, 'Document index checked: ' + response.data.host + ' (' + response.data.dimension + ' dimensions).');
971 notice(response.data.ready ? 'success' : 'info', response.data.message);
972 refreshGate();
973 } else {
974 // A taken name comes back with a free suggestion — fill it in so the next click works.
975 if (response && response.data && response.data.suggested) {
976 suggestedName = response.data.suggested;
977 $('#mxchat_pinecone_index').val(response.data.suggested);
978 }
979 notice('error', failMessage(response, 'Could not create the index.'));
980 }
981 }).fail(function () {
982 notice('error', 'Could not reach the server to create the index.');
983 }).always(function () {
984 $btn.prop('disabled', false).text('Create index for me');
985 });
986 });
987
988 // ---- Check index
989 $('#mxchat-pinecone-docs-check').off('click.pineconeDocs').on('click.pineconeDocs', function () {
990 var $btn = $(this);
991 var host = hostValue();
992 if (!host) {
993 notice('error', 'Enter the Pinecone Host of the document index first.');
994 return;
995 }
996 $btn.prop('disabled', true).text('Checking…');
997 $result.hide();
998 post({
999 action: 'mxchat_pinecone_docs_check_index',
1000 host: host,
1001 api_key: $('#mxchat_pinecone_api_key').val()
1002 }).done(function (response) {
1003 if (response && response.success) {
1004 verifiedHost = String(response.data.host || host).toLowerCase();
1005 if (response.data.name) { $('#mxchat_pinecone_index').val(response.data.name); }
1006 if (response.data.vector_host && !$.trim($('#mxchat_pinecone_vector_host').val() || '')) {
1007 $('#mxchat_pinecone_vector_host').val(response.data.vector_host);
1008 }
1009 setStatus(true, 'Document index checked: ' + response.data.host + ' (' + response.data.dimension + ' dimensions).');
1010 notice('success', response.data.message);
1011 } else {
1012 setStatus(false, 'Not checked: this host cannot be used as a document index.');
1013 notice('error', failMessage(response, 'Check failed.'));
1014 }
1015 refreshGate();
1016 }).fail(function () {
1017 notice('error', 'Could not reach the server to check the index.');
1018 }).always(function () {
1019 $btn.prop('disabled', false).text('Check index');
1020 });
1021 });
1022
1023 // ---- Migrate (copy, page by page, resumable). 3e83e4: the copy runs
1024 // against the document host in the form while the chatbot keeps
1025 // answering from the vector index; a dropped request is retried with
1026 // backoff, and a second click continues from the recorded position.
1027 var $migrateBtn = $('#mxchat-pinecone-docs-migrate');
1028 // e50d2e: "Copy every namespace" — offered once the start response
1029 // reports records in other namespaces; the copy then walks every
1030 // namespace of the source in one run, each into its own namespace on
1031 // the document side. Disabled while a copy runs in this tab (the
1032 // server extends a running copy on the next Migrate click, never
1033 // underneath a step that is in flight).
1034 var $allNs = $('#mxchat_pinecone_docs_all_namespaces');
1035 var $allNsRow = $('#mxchat-pinecone-docs-all-namespaces');
1036 var stepRetries = 0;
1037 var STEP_RETRY_DELAYS = [1000, 2000, 4000, 8000, 16000];
1038 function remainingOf(st) {
1039 var total = parseInt(st.total, 10) || 0, copied = parseInt(st.copied, 10) || 0;
1040 return Math.max(0, total - copied);
1041 }
1042 function otherNamespacesText(st) {
1043 var other = st.other_namespaces || {};
1044 // 47c61f: default namespace first, then by name. The server writes them
1045 // in that order now; this covers states written before it did, and
1046 // namespaces named like numbers, which a JSON object reorders.
1047 var names = [];
1048 $.each(other, function (name) { names.push(String(name)); });
1049 names.sort(function (a, b) {
1050 if (a === b) { return 0; }
1051 if (a === '__default__') { return -1; }
1052 if (b === '__default__') { return 1; }
1053 return a < b ? -1 : 1;
1054 });
1055 var parts = [];
1056 $.each(names, function (i, name) { parts.push(name + ' (' + fmt(other[name]) + ')'); });
1057 return parts.join(', ');
1058 }
1059 function otherNamespacesSummary(st) {
1060 var records = 0, n = 0;
1061 $.each(st.other_namespaces || {}, function (name, count) { records += parseInt(count, 10) || 0; n++; });
1062 return n ? { records: records, count: n } : null;
1063 }
1064 function namespaceEntries(st) {
1065 var out = [];
1066 $.each(st.per_namespace || {}, function (key, e) { out.push($.extend({ key: key }, e)); });
1067 return out;
1068 }
1069 function isMulti(st) { return !!st.all_namespaces || namespaceEntries(st).length > 1; }
1070 function namespaceLine(e, sep) {
1071 return e.key + ' ' + fmt(e.copied) + ' of ' + fmt(e.total) + (e.target_count ? (sep + 'the document index reports ' + fmt(e.target_count) + ')') : '');
1072 }
1073 function renderMigration(st) {
1074 migrationState = (st && st.status) ? st : null;
1075 var $progress = $('#mxchat-pinecone-docs-migrate-progress');
1076 if (!st || !st.status) {
1077 $progress.hide();
1078 $('#mxchat-pinecone-docs-migrate-counts').text('');
1079 $('#mxchat-pinecone-docs-delete-old').hide();
1080 $allNsRow.hide();
1081 refreshGate();
1082 return;
1083 }
1084 var total = parseInt(st.total, 10) || 0;
1085 var copied = parseInt(st.copied, 10) || 0;
1086 var pct = total > 0 ? Math.min(100, Math.round(copied * 100 / total)) : (st.status === 'done' ? 100 : 0);
1087 var multi = isMulti(st);
1088 var entries = namespaceEntries(st);
1089 var others = otherNamespacesSummary(st);
1090 var label, suffix;
1091 if (st.status === 'done') {
1092 suffix = ' — done' + (st.target_count ? ('; the document index reports ' + fmt(st.target_count) + ' records') : '');
1093 } else if (st.status === 'source_deleted') {
1094 suffix = ' — old index deleted';
1095 } else if (st.status === 'running') {
1096 suffix = st.error ? (' — stopped: ' + st.error) : (' — copying, ' + fmt(remainingOf(st)) + ' remaining…');
1097 } else {
1098 suffix = '';
1099 }
1100 if (multi && st.status === 'running') {
1101 var idx = 0, cur = null;
1102 $.each(entries, function (i, e) { if (e.key === (st.current === '' ? '__default__' : st.current)) { idx = i + 1; cur = e; } });
1103 label = (cur ? ('Namespace ' + idx + ' of ' + entries.length + ' (' + cur.key + ') — ' + fmt(cur.copied) + ' of ' + fmt(cur.total) + ' records copied; ') : '') +
1104 fmt(copied) + ' of ' + fmt(total) + ' overall (' + pct + '%)' + (st.error ? (' — stopped: ' + st.error) : (', ' + fmt(remainingOf(st)) + ' remaining…'));
1105 } else if (multi) {
1106 label = fmt(copied) + ' of ' + fmt(total) + ' records copied across ' + entries.length + ' namespaces (' + pct + '%)' + suffix;
1107 } else {
1108 label = fmt(copied) + ' of ' + fmt(total) + ' records copied (' + pct + '%)' + suffix;
1109 }
1110 $progress.show().find('.mxch-progress-bar-fill').css('width', pct + '%');
1111 $progress.find('.mxch-progress-label').text(label);
1112 var counts;
1113 if (multi) {
1114 var lines = $.map(entries, function (e) { return namespaceLine(e, ' ('); });
1115 counts = (st.source_name ? ('Source: ' + st.source_name + ' (' + fmt(total) + ' records in ' + entries.length + ' namespaces). ') : '') + lines.join(' · ') + '.';
1116 } else {
1117 counts = st.source_name ? ('Source: ' + st.source_name + ' (' + fmt(total) + ' records' + (st.namespace ? (' in namespace ' + st.namespace) : '') + ')') : '';
1118 if (others) {
1119 counts += (counts ? '. ' : '') + 'Not copied — ' + fmt(others.records) + ' records in ' + others.count + ' other namespace' + (others.count === 1 ? '' : 's') + ': ' + otherNamespacesText(st) + '. Tick Copy every namespace and click Migrate to include them.';
1120 }
1121 }
1122 $('#mxchat-pinecone-docs-migrate-counts').text(counts);
1123 // The toggle appears once the source is known to have other namespaces, and stays on for a copy of every namespace.
1124 $allNsRow.toggle(multi || !!others);
1125 if (st.all_namespaces) { $allNs.prop('checked', true); }
1126 $allNs.prop('disabled', $migrateBtn.prop('disabled'));
1127 // Delete old index is never offered while the copy is known to leave namespaces behind (the server refuses too).
1128 $('#mxchat-pinecone-docs-delete-old').toggle(st.status === 'done' && total > 0 && copied >= total && liveType === 'document' && !others);
1129 refreshGate();
1130 }
1131 function migrateFinished(st) {
1132 $migrateBtn.prop('disabled', false).text('Migrate');
1133 $allNs.prop('disabled', false);
1134 var total = parseInt(st.total, 10) || 0, copied = parseInt(st.copied, 10) || 0;
1135 var reported = st.target_count ? (' The document index reports ' + fmt(st.target_count) + ' records.') : '';
1136 var stillVector = liveType === 'document' ? '' : ' Your chatbot is still on the vector index — save the Pinecone settings to switch it over.';
1137 var entries = namespaceEntries(st);
1138 var others = otherNamespacesSummary(st);
1139 if (isMulti(st)) {
1140 var lines = $.map(entries, function (e) { return namespaceLine(e, ' ('); });
1141 if (copied >= total) {
1142 notice('success', 'Copy finished: ' + fmt(copied) + ' of ' + fmt(total) + ' records across ' + entries.length + ' namespaces are in the document index — ' + lines.join(', ') + '.' + stillVector);
1143 } else {
1144 var short = $.map(entries, function (e) { return (parseInt(e.copied, 10) || 0) < (parseInt(e.total, 10) || 0) ? namespaceLine(e, ' (') : null; });
1145 notice('warning', 'The copy reached the end of every namespace after ' + fmt(copied) + ' records, but ' + fmt(total) + ' were expected. Short: ' + short.join(', ') + '.' + reported +
1146 (parseInt(st.skipped, 10) ? (' ' + fmt(st.skipped) + ' records had no vector and were skipped.') : ''));
1147 }
1148 return;
1149 }
1150 if (copied >= total) {
1151 notice(others ? 'warning' : 'success', 'Copy finished: ' + fmt(copied) + ' of ' + fmt(total) + ' records are in the document index.' + reported +
1152 (others ? (' ' + fmt(others.records) + ' records in ' + others.count + ' other namespace' + (others.count === 1 ? ' were' : 's were') + ' not copied: ' + otherNamespacesText(st) + '. Tick Copy every namespace and click Migrate to include them.') : '') + stillVector);
1153 } else {
1154 notice('warning', 'The copy reached the end of the source namespace after ' + fmt(copied) + ' records, but ' + fmt(total) + ' were expected.' + reported +
1155 (others ? (' Records in other namespaces were not copied: ' + otherNamespacesText(st) + '.') : '') +
1156 (parseInt(st.skipped, 10) ? (' ' + fmt(st.skipped) + ' records had no vector and were skipped.') : ''));
1157 }
1158 }
1159 function migrateStep() {
1160 post({ action: 'mxchat_pinecone_docs_migrate', mode: 'step', target_host: hostValue(), api_key: $('#mxchat_pinecone_api_key').val() }).done(function (response) {
1161 stepRetries = 0;
1162 if (response && response.success) {
1163 renderMigration(response.data.state);
1164 if (response.data.state && response.data.state.status === 'running') {
1165 setTimeout(migrateStep, 250);
1166 } else {
1167 migrateFinished(response.data.state || {});
1168 }
1169 } else {
1170 var st = (response && response.data && response.data.state) ? response.data.state : null;
1171 if (st) { renderMigration(st); }
1172 notice('error', failMessage(response, 'The copy stopped.') + (st ? (' Click Migrate to continue from record ' + fmt(st.copied) + '.') : ' Click Migrate to resume.'));
1173 $migrateBtn.prop('disabled', false).text('Migrate');
1174 $allNs.prop('disabled', false);
1175 }
1176 }).fail(function (xhr) {
1177 // The server did not answer (timeout, 5xx, rate limit): wait and try the same step again.
1178 if (stepRetries < STEP_RETRY_DELAYS.length) {
1179 var delay = STEP_RETRY_DELAYS[stepRetries++];
1180 notice('info', 'No answer from the server (HTTP ' + (xhr && xhr.status ? xhr.status : 0) + '). Retrying in ' + Math.round(delay / 1000) + ' s… the copy continues from where it stopped.');
1181 setTimeout(migrateStep, delay);
1182 return;
1183 }
1184 stepRetries = 0;
1185 notice('error', 'The server stopped answering (HTTP ' + (xhr && xhr.status ? xhr.status : 0) + ') after several tries. Nothing is lost — click Migrate to continue from the last record copied.');
1186 $migrateBtn.prop('disabled', false).text('Migrate');
1187 $allNs.prop('disabled', false);
1188 });
1189 }
1190 $migrateBtn.off('click.pineconeDocs').on('click.pineconeDocs', function () {
1191 var source = $.trim($('#mxchat_pinecone_vector_host').val() || '');
1192 if (!source) {
1193 notice('error', 'Enter the host of the vector index to copy from.');
1194 return;
1195 }
1196 if (!hostValue()) {
1197 notice('error', 'Enter the host of the document index to copy into, or create one with Create index for me.');
1198 return;
1199 }
1200 $migrateBtn.prop('disabled', true).text('Copying…');
1201 $allNs.prop('disabled', true);
1202 $result.hide();
1203 stepRetries = 0;
1204 post({ action: 'mxchat_pinecone_docs_migrate', mode: 'start', source_host: source, target_host: hostValue(), api_key: $('#mxchat_pinecone_api_key').val(), all_namespaces: $allNs.is(':checked') ? 1 : 0 }).done(function (response) {
1205 if (response && response.success) {
1206 var st = response.data.state || {};
1207 if (st.target_host && !verifiedHost) { verifiedHost = String(st.target_host).toLowerCase(); setStatus(true, 'Document index checked: ' + st.target_host + '.'); }
1208 renderMigration(st);
1209 if (st.status === 'running') {
1210 migrateStep();
1211 } else {
1212 migrateFinished(st);
1213 }
1214 } else {
1215 notice('error', failMessage(response, 'The copy could not start.'));
1216 $migrateBtn.prop('disabled', false).text('Migrate');
1217 $allNs.prop('disabled', false);
1218 }
1219 }).fail(function () {
1220 notice('error', 'Could not reach the server to start the copy.');
1221 $migrateBtn.prop('disabled', false).text('Migrate');
1222 $allNs.prop('disabled', false);
1223 });
1224 });
1225 renderMigration(state.migration || null);
1226
1227 // ---- Delete old index (typed confirmation)
1228 $('#mxchat-pinecone-docs-delete-old-btn').off('click.pineconeDocs').on('click.pineconeDocs', function () {
1229 var $btn = $(this);
1230 var confirmName = $.trim($('#mxchat_pinecone_docs_delete_confirm').val() || '');
1231 if (!confirmName) {
1232 notice('error', 'Type the old index name to confirm.');
1233 return;
1234 }
1235 $btn.prop('disabled', true).text('Deleting…');
1236 post({ action: 'mxchat_pinecone_docs_delete_old_index', confirm_name: confirmName }).done(function (response) {
1237 if (response && response.success) {
1238 notice('success', response.data.message);
1239 $('#mxchat_pinecone_vector_host').val('');
1240 $('#mxchat_pinecone_docs_delete_confirm').val('');
1241 renderMigration(response.data.state);
1242 } else {
1243 notice('error', failMessage(response, 'The old index was not deleted.'));
1244 }
1245 }).fail(function () {
1246 notice('error', 'Could not reach the server.');
1247 }).always(function () {
1248 $btn.prop('disabled', false).text('Delete old index');
1249 });
1250 });
1251 }
1252
1253 function initPineconeToggle() {
1254 // Remove any existing handlers to prevent duplicates
1255 var $toggleInput = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');
1256 $toggleInput.off('change.pineconeToggle');
1257
1258 // Ensure the success notice exists in the settings div (add if not present)
1259 // Check for both the JS-added class and any existing PHP-rendered success notice
1260 var settingsDiv = $('.mxchat-pinecone-settings');
1261 if (settingsDiv.length > 0 && settingsDiv.find('.mxch-notice-success').length === 0) {
1262 var successNotice = $('<div class="mxch-notice mxch-notice-success mxchat-pinecone-enabled-notice" style="margin-bottom: 20px; display: none;">' +
1263 '<svg class="mxch-notice-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>' +
1264 '<span>Pinecone is enabled. All new knowledge base content will be stored in Pinecone.</span>' +
1265 '</div>');
1266 settingsDiv.prepend(successNotice);
1267 }
1268
1269 // Add the toggle handler for UI only (auto-save will handle the actual saving)
1270 $toggleInput.on('change.pineconeToggle', function() {
1271 var $checkbox = $(this);
1272 var isChecked = $checkbox.is(':checked');
1273 var settingsDiv = $('.mxchat-pinecone-settings');
1274 var enabledNotice = settingsDiv.find('.mxchat-pinecone-enabled-notice, .mxch-notice-success');
1275
1276 // Update the UI immediately
1277 if (isChecked) {
1278 settingsDiv.slideDown(300);
1279 enabledNotice.slideDown(300);
1280 } else {
1281 enabledNotice.slideUp(300);
1282 settingsDiv.slideUp(300);
1283 }
1284
1285 // Update sidebar badge for Pinecone
1286 updateSidebarBadge('pinecone', isChecked);
1287 });
1288
1289 // Set initial state based on current checkbox value
1290 var currentToggle = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');
1291 if (currentToggle.length > 0) {
1292 var settingsDiv = $('.mxchat-pinecone-settings');
1293 var enabledNotice = settingsDiv.find('.mxchat-pinecone-enabled-notice, .mxch-notice-success');
1294 if (currentToggle.is(':checked')) {
1295 settingsDiv.show();
1296 enabledNotice.show();
1297 } else {
1298 settingsDiv.hide();
1299 enabledNotice.hide();
1300 }
1301 }
1302 }
1303
1304 function initPineconeConnectionTest() {
1305 $('#test-pinecone-connection').off('click.pinecone');
1306
1307 $('#test-pinecone-connection').on('click.pinecone', function() {
1308 var button = $(this);
1309 var resultDiv = $('#connection-test-result');
1310
1311 var apiKey = $('#mxchat_pinecone_api_key').val();
1312 var host = $('#mxchat_pinecone_host').val();
1313 var index = $('#mxchat_pinecone_index').val();
1314
1315 if (!apiKey || !host || !index) {
1316 resultDiv.html('<div class="notice notice-error"><p>Please fill in all required fields first.</p></div>').show();
1317 return;
1318 }
1319
1320 button.prop('disabled', true).text('Testing...');
1321 resultDiv.hide();
1322
1323 var ajaxUrl = (typeof mxchatPromptsAdmin !== 'undefined') ? mxchatPromptsAdmin.ajax_url : ajaxurl;
1324 var nonce = (typeof mxchatPromptsAdmin !== 'undefined') ? mxchatPromptsAdmin.prompts_setting_nonce :
1325 (typeof mxchatAdmin !== 'undefined') ? mxchatAdmin.setting_nonce : '';
1326
1327 $.ajax({
1328 url: ajaxUrl,
1329 type: 'POST',
1330 data: {
1331 action: 'mxchat_test_pinecone_connection',
1332 _ajax_nonce: nonce,
1333 api_key: apiKey,
1334 host: host,
1335 index_name: index
1336 },
1337 success: function(response) {
1338 if (response.success) {
1339 resultDiv.html('<div class="notice notice-success"><p><span class="dashicons dashicons-yes-alt"></span> ' + response.data.message + '</p></div>');
1340 } else {
1341 resultDiv.html('<div class="notice notice-error"><p><span class="dashicons dashicons-warning"></span> ' + response.data.message + '</p></div>');
1342 }
1343 resultDiv.show();
1344 },
1345 error: function() {
1346 resultDiv.html('<div class="notice notice-error"><p>Connection test failed. Please check your settings.</p></div>').show();
1347 },
1348 complete: function() {
1349 button.prop('disabled', false).text('Test Connection');
1350 }
1351 });
1352 });
1353 }
1354
1355 function checkPineconeCompatibility() {
1356 if ($('.mxchat-pinecone-compatibility-notice').length > 0) {
1357 return;
1358 }
1359
1360 var hasOldAddon = $('body').hasClass('mxchat-pinecone-addon-active') ||
1361 $('.pcm-card').length > 0;
1362
1363 if (hasOldAddon) {
1364 var compatibilityNotice = $(`
1365 <div class="notice notice-info mxchat-pinecone-compatibility-notice">
1366 <p><strong>Pinecone Integration Notice:</strong> We've detected you have the Pinecone add-on installed.
1367 Pinecone functionality is now built into the core plugin. You can safely deactivate the separate
1368 Pinecone add-on after confirming your settings are migrated below.</p>
1369 </div>
1370 `);
1371
1372 $('#mxchat-kb-tab-pinecone .mxchat-card').prepend(compatibilityNotice);
1373
1374 migratePineconeSettings();
1375 }
1376 }
1377
1378 function migratePineconeSettings() {
1379 if (typeof ajaxurl !== 'undefined') {
1380 $.ajax({
1381 url: ajaxurl,
1382 type: 'POST',
1383 data: {
1384 action: 'mxchat_migrate_pinecone_settings',
1385 _ajax_nonce: (typeof mxchatAdmin !== 'undefined') ? mxchatAdmin.setting_nonce : ''
1386 },
1387 success: function(response) {
1388 if (response.success && response.data.migrated) {
1389 location.reload();
1390 }
1391 },
1392 error: function() {
1393 //console.log('Pinecone settings migration not available');
1394 }
1395 });
1396 }
1397 }
1398
1399 // ============================================
1400 // OpenAI Vector Store functionality
1401 // ============================================
1402 initVectorStoreFeatures = function() {
1403 if ($('#openai-vectorstore').length === 0) {
1404 return;
1405 }
1406
1407 initVectorStoreToggle();
1408 };
1409
1410 function initVectorStoreToggle() {
1411 // Remove any existing handlers to prevent duplicates
1412 var $toggleInput = $('input[name="mxchat_openai_vectorstore_options[mxchat_use_openai_vectorstore]"]');
1413 $toggleInput.off('change.vectorstoreToggle');
1414
1415 // Ensure the success notice exists in the settings div (add if not present)
1416 // Check for both the JS-added class and any existing PHP-rendered success notice
1417 var settingsDiv = $('.mxchat-vectorstore-settings');
1418 if (settingsDiv.length > 0 && settingsDiv.find('.mxch-notice-success').length === 0) {
1419 var successNotice = $('<div class="mxch-notice mxch-notice-success mxchat-vectorstore-enabled-notice" style="margin-bottom: 20px; display: none;">' +
1420 '<svg class="mxch-notice-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>' +
1421 '<span>OpenAI Vector Store is enabled. Queries will search your Vector Store for relevant content.</span>' +
1422 '</div>');
1423 settingsDiv.prepend(successNotice);
1424 }
1425
1426 // Add the toggle handler for UI only (form submit will handle the actual saving)
1427 $toggleInput.on('change.vectorstoreToggle', function() {
1428 var $checkbox = $(this);
1429 var isChecked = $checkbox.is(':checked');
1430 var settingsDiv = $('.mxchat-vectorstore-settings');
1431 var enabledNotice = settingsDiv.find('.mxchat-vectorstore-enabled-notice, .mxch-notice-success');
1432
1433 // Update the UI immediately
1434 if (isChecked) {
1435 settingsDiv.slideDown(300);
1436 enabledNotice.slideDown(300);
1437 } else {
1438 enabledNotice.slideUp(300);
1439 settingsDiv.slideUp(300);
1440 }
1441
1442 // Update sidebar badge for OpenAI Vector Store
1443 updateSidebarBadge('openai-vectorstore', isChecked);
1444 });
1445
1446 // Set initial state based on current checkbox value
1447 var currentToggle = $('input[name="mxchat_openai_vectorstore_options[mxchat_use_openai_vectorstore]"]');
1448 if (currentToggle.length > 0) {
1449 var settingsDiv = $('.mxchat-vectorstore-settings');
1450 var enabledNotice = settingsDiv.find('.mxchat-vectorstore-enabled-notice, .mxch-notice-success');
1451 if (currentToggle.is(':checked')) {
1452 settingsDiv.show();
1453 enabledNotice.show();
1454 } else {
1455 settingsDiv.hide();
1456 enabledNotice.hide();
1457 }
1458 }
1459 }
1460
1461 })(jQuery);