PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.23
MxChat – AI Chatbot & Content Generation for WordPress v3.2.23
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.23, at js/content-selector.js

1,435 lines 67.7 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 $('#mxchat_pinecone_docs_switch_anyway').off('change.pineconeDocs').on('change.pineconeDocs', refreshGate);
879 $('#mxchat_pinecone_host').off('input.pineconeDocs change.pineconeDocs').on('input.pineconeDocs change.pineconeDocs', refreshGate);
880
881 function post(data) {
882 data.nonce = nonce;
883 return $.post(ajaxUrl, data);
884 }
885
886 // ---- Index type change: a document index needs its own name, so when
887 // the field still holds the vector index's name (or is empty) suggest a
888 // free "<name>-docs" from the project; Vector restores the original.
889 var suggestedName = '';
890 function nameIsOurs() {
891 var name = $.trim($('#mxchat_pinecone_index').val() || '');
892 return name === '' || name === storedIndex || name === originalIndexName || name === suggestedName;
893 }
894 // When the host in the field is a checked document index, the name is
895 // that index's own; otherwise a free "<name>-docs" for the create button.
896 function suggestDocsName() {
897 var name = $.trim($('#mxchat_pinecone_index').val() || '');
898 var apiKey = $('#mxchat_pinecone_api_key').val();
899 if (liveType === 'document' || !apiKey || currentType() !== 'document') { return; }
900 if (!nameIsOurs()) { return; } // the owner typed their own
901 var host = (hostValue() !== '' && hostValue() === verifiedHost) ? hostValue() : '';
902 post({ action: 'mxchat_pinecone_docs_suggest_name', base: name || storedIndex, host: host, api_key: apiKey }).done(function (response) {
903 if (response && response.success && response.data && response.data.name && currentType() === 'document' && nameIsOurs()) {
904 suggestedName = response.data.name;
905 $('#mxchat_pinecone_index').val(suggestedName);
906 }
907 });
908 }
909 $type.off('change.pineconeDocs').on('change.pineconeDocs', function () {
910 if (currentType() === 'document') {
911 suggestDocsName();
912 } else if (suggestedName !== '' && $.trim($('#mxchat_pinecone_index').val() || '') === suggestedName) {
913 $('#mxchat_pinecone_index').val(originalIndexName);
914 }
915 refreshGate();
916 });
917 // Pasting the checked document host resolves the name to that index's own.
918 $('#mxchat_pinecone_host').on('change.pineconeDocsName', function () {
919 if (currentType() === 'document' && hostValue() !== '' && hostValue() === verifiedHost) { suggestDocsName(); }
920 });
921 refreshGate();
922 function failMessage(response, fallback) {
923 if (response && response.data) {
924 if (typeof response.data === 'string') { return response.data; }
925 if (response.data.message) { return response.data.message; }
926 }
927 return fallback;
928 }
929
930 // ---- Create index for me
931 $('#mxchat-pinecone-docs-create').off('click.pineconeDocs').on('click.pineconeDocs', function () {
932 var $btn = $(this);
933 var name = $.trim($('#mxchat_pinecone_index').val() || '');
934 if (!name) {
935 notice('error', 'Enter an Index Name first — the document index is created under that name.');
936 return;
937 }
938 $btn.prop('disabled', true).text('Creating…');
939 $result.hide();
940 post({
941 action: 'mxchat_pinecone_docs_create_index',
942 index_name: name,
943 api_key: $('#mxchat_pinecone_api_key').val(),
944 cloud: $('#mxchat_pinecone_docs_cloud').val(),
945 region: $('#mxchat_pinecone_docs_region').val(),
946 language: $('#mxchat_pinecone_docs_language').val()
947 }).done(function (response) {
948 if (response && response.success) {
949 $('#mxchat_pinecone_host').val(response.data.host);
950 $('#mxchat_pinecone_index').val(response.data.name);
951 if (response.data.vector_host && !$.trim($('#mxchat_pinecone_vector_host').val() || '')) {
952 $('#mxchat_pinecone_vector_host').val(response.data.vector_host);
953 }
954 verifiedHost = String(response.data.host || '').toLowerCase();
955 setStatus(true, 'Document index checked: ' + response.data.host + ' (' + response.data.dimension + ' dimensions).');
956 notice(response.data.ready ? 'success' : 'info', response.data.message);
957 refreshGate();
958 } else {
959 // A taken name comes back with a free suggestion — fill it in so the next click works.
960 if (response && response.data && response.data.suggested) {
961 suggestedName = response.data.suggested;
962 $('#mxchat_pinecone_index').val(response.data.suggested);
963 }
964 notice('error', failMessage(response, 'Could not create the index.'));
965 }
966 }).fail(function () {
967 notice('error', 'Could not reach the server to create the index.');
968 }).always(function () {
969 $btn.prop('disabled', false).text('Create index for me');
970 });
971 });
972
973 // ---- Check index
974 $('#mxchat-pinecone-docs-check').off('click.pineconeDocs').on('click.pineconeDocs', function () {
975 var $btn = $(this);
976 var host = hostValue();
977 if (!host) {
978 notice('error', 'Enter the Pinecone Host of the document index first.');
979 return;
980 }
981 $btn.prop('disabled', true).text('Checking…');
982 $result.hide();
983 post({
984 action: 'mxchat_pinecone_docs_check_index',
985 host: host,
986 api_key: $('#mxchat_pinecone_api_key').val()
987 }).done(function (response) {
988 if (response && response.success) {
989 verifiedHost = String(response.data.host || host).toLowerCase();
990 if (response.data.name) { $('#mxchat_pinecone_index').val(response.data.name); }
991 if (response.data.vector_host && !$.trim($('#mxchat_pinecone_vector_host').val() || '')) {
992 $('#mxchat_pinecone_vector_host').val(response.data.vector_host);
993 }
994 setStatus(true, 'Document index checked: ' + response.data.host + ' (' + response.data.dimension + ' dimensions).');
995 notice('success', response.data.message);
996 } else {
997 setStatus(false, 'Not checked: this host cannot be used as a document index.');
998 notice('error', failMessage(response, 'Check failed.'));
999 }
1000 refreshGate();
1001 }).fail(function () {
1002 notice('error', 'Could not reach the server to check the index.');
1003 }).always(function () {
1004 $btn.prop('disabled', false).text('Check index');
1005 });
1006 });
1007
1008 // ---- Migrate (copy, page by page, resumable). 3e83e4: the copy runs
1009 // against the document host in the form while the chatbot keeps
1010 // answering from the vector index; a dropped request is retried with
1011 // backoff, and a second click continues from the recorded position.
1012 var $migrateBtn = $('#mxchat-pinecone-docs-migrate');
1013 // e50d2e: "Copy every namespace" — offered once the start response
1014 // reports records in other namespaces; the copy then walks every
1015 // namespace of the source in one run, each into its own namespace on
1016 // the document side. Disabled while a copy runs in this tab (the
1017 // server extends a running copy on the next Migrate click, never
1018 // underneath a step that is in flight).
1019 var $allNs = $('#mxchat_pinecone_docs_all_namespaces');
1020 var $allNsRow = $('#mxchat-pinecone-docs-all-namespaces');
1021 var stepRetries = 0;
1022 var STEP_RETRY_DELAYS = [1000, 2000, 4000, 8000, 16000];
1023 function remainingOf(st) {
1024 var total = parseInt(st.total, 10) || 0, copied = parseInt(st.copied, 10) || 0;
1025 return Math.max(0, total - copied);
1026 }
1027 function otherNamespacesText(st) {
1028 var other = st.other_namespaces || {};
1029 var parts = [];
1030 $.each(other, function (name, count) { parts.push(name + ' (' + fmt(count) + ')'); });
1031 return parts.join(', ');
1032 }
1033 function otherNamespacesSummary(st) {
1034 var records = 0, n = 0;
1035 $.each(st.other_namespaces || {}, function (name, count) { records += parseInt(count, 10) || 0; n++; });
1036 return n ? { records: records, count: n } : null;
1037 }
1038 function namespaceEntries(st) {
1039 var out = [];
1040 $.each(st.per_namespace || {}, function (key, e) { out.push($.extend({ key: key }, e)); });
1041 return out;
1042 }
1043 function isMulti(st) { return !!st.all_namespaces || namespaceEntries(st).length > 1; }
1044 function namespaceLine(e, sep) {
1045 return e.key + ' ' + fmt(e.copied) + ' of ' + fmt(e.total) + (e.target_count ? (sep + 'the document index reports ' + fmt(e.target_count) + ')') : '');
1046 }
1047 function renderMigration(st) {
1048 migrationState = (st && st.status) ? st : null;
1049 var $progress = $('#mxchat-pinecone-docs-migrate-progress');
1050 if (!st || !st.status) {
1051 $progress.hide();
1052 $('#mxchat-pinecone-docs-migrate-counts').text('');
1053 $('#mxchat-pinecone-docs-delete-old').hide();
1054 $allNsRow.hide();
1055 refreshGate();
1056 return;
1057 }
1058 var total = parseInt(st.total, 10) || 0;
1059 var copied = parseInt(st.copied, 10) || 0;
1060 var pct = total > 0 ? Math.min(100, Math.round(copied * 100 / total)) : (st.status === 'done' ? 100 : 0);
1061 var multi = isMulti(st);
1062 var entries = namespaceEntries(st);
1063 var others = otherNamespacesSummary(st);
1064 var label, suffix;
1065 if (st.status === 'done') {
1066 suffix = ' — done' + (st.target_count ? ('; the document index reports ' + fmt(st.target_count) + ' records') : '');
1067 } else if (st.status === 'source_deleted') {
1068 suffix = ' — old index deleted';
1069 } else if (st.status === 'running') {
1070 suffix = st.error ? (' — stopped: ' + st.error) : (' — copying, ' + fmt(remainingOf(st)) + ' remaining…');
1071 } else {
1072 suffix = '';
1073 }
1074 if (multi && st.status === 'running') {
1075 var idx = 0, cur = null;
1076 $.each(entries, function (i, e) { if (e.key === (st.current === '' ? '__default__' : st.current)) { idx = i + 1; cur = e; } });
1077 label = (cur ? ('Namespace ' + idx + ' of ' + entries.length + ' (' + cur.key + ') — ' + fmt(cur.copied) + ' of ' + fmt(cur.total) + ' records copied; ') : '') +
1078 fmt(copied) + ' of ' + fmt(total) + ' overall (' + pct + '%)' + (st.error ? (' — stopped: ' + st.error) : (', ' + fmt(remainingOf(st)) + ' remaining…'));
1079 } else if (multi) {
1080 label = fmt(copied) + ' of ' + fmt(total) + ' records copied across ' + entries.length + ' namespaces (' + pct + '%)' + suffix;
1081 } else {
1082 label = fmt(copied) + ' of ' + fmt(total) + ' records copied (' + pct + '%)' + suffix;
1083 }
1084 $progress.show().find('.mxch-progress-bar-fill').css('width', pct + '%');
1085 $progress.find('.mxch-progress-label').text(label);
1086 var counts;
1087 if (multi) {
1088 var lines = $.map(entries, function (e) { return namespaceLine(e, ' ('); });
1089 counts = (st.source_name ? ('Source: ' + st.source_name + ' (' + fmt(total) + ' records in ' + entries.length + ' namespaces). ') : '') + lines.join(' · ') + '.';
1090 } else {
1091 counts = st.source_name ? ('Source: ' + st.source_name + ' (' + fmt(total) + ' records' + (st.namespace ? (' in namespace ' + st.namespace) : '') + ')') : '';
1092 if (others) {
1093 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.';
1094 }
1095 }
1096 $('#mxchat-pinecone-docs-migrate-counts').text(counts);
1097 // The toggle appears once the source is known to have other namespaces, and stays on for a copy of every namespace.
1098 $allNsRow.toggle(multi || !!others);
1099 if (st.all_namespaces) { $allNs.prop('checked', true); }
1100 $allNs.prop('disabled', $migrateBtn.prop('disabled'));
1101 // Delete old index is never offered while the copy is known to leave namespaces behind (the server refuses too).
1102 $('#mxchat-pinecone-docs-delete-old').toggle(st.status === 'done' && total > 0 && copied >= total && liveType === 'document' && !others);
1103 refreshGate();
1104 }
1105 function migrateFinished(st) {
1106 $migrateBtn.prop('disabled', false).text('Migrate');
1107 $allNs.prop('disabled', false);
1108 var total = parseInt(st.total, 10) || 0, copied = parseInt(st.copied, 10) || 0;
1109 var reported = st.target_count ? (' The document index reports ' + fmt(st.target_count) + ' records.') : '';
1110 var stillVector = liveType === 'document' ? '' : ' Your chatbot is still on the vector index — save the Pinecone settings to switch it over.';
1111 var entries = namespaceEntries(st);
1112 var others = otherNamespacesSummary(st);
1113 if (isMulti(st)) {
1114 var lines = $.map(entries, function (e) { return namespaceLine(e, ' ('); });
1115 if (copied >= total) {
1116 notice('success', 'Copy finished: ' + fmt(copied) + ' of ' + fmt(total) + ' records across ' + entries.length + ' namespaces are in the document index — ' + lines.join(', ') + '.' + stillVector);
1117 } else {
1118 var short = $.map(entries, function (e) { return (parseInt(e.copied, 10) || 0) < (parseInt(e.total, 10) || 0) ? namespaceLine(e, ' (') : null; });
1119 notice('warning', 'The copy reached the end of every namespace after ' + fmt(copied) + ' records, but ' + fmt(total) + ' were expected. Short: ' + short.join(', ') + '.' + reported +
1120 (parseInt(st.skipped, 10) ? (' ' + fmt(st.skipped) + ' records had no vector and were skipped.') : ''));
1121 }
1122 return;
1123 }
1124 if (copied >= total) {
1125 notice(others ? 'warning' : 'success', 'Copy finished: ' + fmt(copied) + ' of ' + fmt(total) + ' records are in the document index.' + reported +
1126 (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);
1127 } else {
1128 notice('warning', 'The copy reached the end of the source namespace after ' + fmt(copied) + ' records, but ' + fmt(total) + ' were expected.' + reported +
1129 (others ? (' Records in other namespaces were not copied: ' + otherNamespacesText(st) + '.') : '') +
1130 (parseInt(st.skipped, 10) ? (' ' + fmt(st.skipped) + ' records had no vector and were skipped.') : ''));
1131 }
1132 }
1133 function migrateStep() {
1134 post({ action: 'mxchat_pinecone_docs_migrate', mode: 'step', target_host: hostValue(), api_key: $('#mxchat_pinecone_api_key').val() }).done(function (response) {
1135 stepRetries = 0;
1136 if (response && response.success) {
1137 renderMigration(response.data.state);
1138 if (response.data.state && response.data.state.status === 'running') {
1139 setTimeout(migrateStep, 250);
1140 } else {
1141 migrateFinished(response.data.state || {});
1142 }
1143 } else {
1144 var st = (response && response.data && response.data.state) ? response.data.state : null;
1145 if (st) { renderMigration(st); }
1146 notice('error', failMessage(response, 'The copy stopped.') + (st ? (' Click Migrate to continue from record ' + fmt(st.copied) + '.') : ' Click Migrate to resume.'));
1147 $migrateBtn.prop('disabled', false).text('Migrate');
1148 $allNs.prop('disabled', false);
1149 }
1150 }).fail(function (xhr) {
1151 // The server did not answer (timeout, 5xx, rate limit): wait and try the same step again.
1152 if (stepRetries < STEP_RETRY_DELAYS.length) {
1153 var delay = STEP_RETRY_DELAYS[stepRetries++];
1154 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.');
1155 setTimeout(migrateStep, delay);
1156 return;
1157 }
1158 stepRetries = 0;
1159 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.');
1160 $migrateBtn.prop('disabled', false).text('Migrate');
1161 $allNs.prop('disabled', false);
1162 });
1163 }
1164 $migrateBtn.off('click.pineconeDocs').on('click.pineconeDocs', function () {
1165 var source = $.trim($('#mxchat_pinecone_vector_host').val() || '');
1166 if (!source) {
1167 notice('error', 'Enter the host of the vector index to copy from.');
1168 return;
1169 }
1170 if (!hostValue()) {
1171 notice('error', 'Enter the host of the document index to copy into, or create one with Create index for me.');
1172 return;
1173 }
1174 $migrateBtn.prop('disabled', true).text('Copying…');
1175 $allNs.prop('disabled', true);
1176 $result.hide();
1177 stepRetries = 0;
1178 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) {
1179 if (response && response.success) {
1180 var st = response.data.state || {};
1181 if (st.target_host && !verifiedHost) { verifiedHost = String(st.target_host).toLowerCase(); setStatus(true, 'Document index checked: ' + st.target_host + '.'); }
1182 renderMigration(st);
1183 if (st.status === 'running') {
1184 migrateStep();
1185 } else {
1186 migrateFinished(st);
1187 }
1188 } else {
1189 notice('error', failMessage(response, 'The copy could not start.'));
1190 $migrateBtn.prop('disabled', false).text('Migrate');
1191 $allNs.prop('disabled', false);
1192 }
1193 }).fail(function () {
1194 notice('error', 'Could not reach the server to start the copy.');
1195 $migrateBtn.prop('disabled', false).text('Migrate');
1196 $allNs.prop('disabled', false);
1197 });
1198 });
1199 renderMigration(state.migration || null);
1200
1201 // ---- Delete old index (typed confirmation)
1202 $('#mxchat-pinecone-docs-delete-old-btn').off('click.pineconeDocs').on('click.pineconeDocs', function () {
1203 var $btn = $(this);
1204 var confirmName = $.trim($('#mxchat_pinecone_docs_delete_confirm').val() || '');
1205 if (!confirmName) {
1206 notice('error', 'Type the old index name to confirm.');
1207 return;
1208 }
1209 $btn.prop('disabled', true).text('Deleting…');
1210 post({ action: 'mxchat_pinecone_docs_delete_old_index', confirm_name: confirmName }).done(function (response) {
1211 if (response && response.success) {
1212 notice('success', response.data.message);
1213 $('#mxchat_pinecone_vector_host').val('');
1214 $('#mxchat_pinecone_docs_delete_confirm').val('');
1215 renderMigration(response.data.state);
1216 } else {
1217 notice('error', failMessage(response, 'The old index was not deleted.'));
1218 }
1219 }).fail(function () {
1220 notice('error', 'Could not reach the server.');
1221 }).always(function () {
1222 $btn.prop('disabled', false).text('Delete old index');
1223 });
1224 });
1225 }
1226
1227 function initPineconeToggle() {
1228 // Remove any existing handlers to prevent duplicates
1229 var $toggleInput = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');
1230 $toggleInput.off('change.pineconeToggle');
1231
1232 // Ensure the success notice exists in the settings div (add if not present)
1233 // Check for both the JS-added class and any existing PHP-rendered success notice
1234 var settingsDiv = $('.mxchat-pinecone-settings');
1235 if (settingsDiv.length > 0 && settingsDiv.find('.mxch-notice-success').length === 0) {
1236 var successNotice = $('<div class="mxch-notice mxch-notice-success mxchat-pinecone-enabled-notice" style="margin-bottom: 20px; display: none;">' +
1237 '<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>' +
1238 '<span>Pinecone is enabled. All new knowledge base content will be stored in Pinecone.</span>' +
1239 '</div>');
1240 settingsDiv.prepend(successNotice);
1241 }
1242
1243 // Add the toggle handler for UI only (auto-save will handle the actual saving)
1244 $toggleInput.on('change.pineconeToggle', function() {
1245 var $checkbox = $(this);
1246 var isChecked = $checkbox.is(':checked');
1247 var settingsDiv = $('.mxchat-pinecone-settings');
1248 var enabledNotice = settingsDiv.find('.mxchat-pinecone-enabled-notice, .mxch-notice-success');
1249
1250 // Update the UI immediately
1251 if (isChecked) {
1252 settingsDiv.slideDown(300);
1253 enabledNotice.slideDown(300);
1254 } else {
1255 enabledNotice.slideUp(300);
1256 settingsDiv.slideUp(300);
1257 }
1258
1259 // Update sidebar badge for Pinecone
1260 updateSidebarBadge('pinecone', isChecked);
1261 });
1262
1263 // Set initial state based on current checkbox value
1264 var currentToggle = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');
1265 if (currentToggle.length > 0) {
1266 var settingsDiv = $('.mxchat-pinecone-settings');
1267 var enabledNotice = settingsDiv.find('.mxchat-pinecone-enabled-notice, .mxch-notice-success');
1268 if (currentToggle.is(':checked')) {
1269 settingsDiv.show();
1270 enabledNotice.show();
1271 } else {
1272 settingsDiv.hide();
1273 enabledNotice.hide();
1274 }
1275 }
1276 }
1277
1278 function initPineconeConnectionTest() {
1279 $('#test-pinecone-connection').off('click.pinecone');
1280
1281 $('#test-pinecone-connection').on('click.pinecone', function() {
1282 var button = $(this);
1283 var resultDiv = $('#connection-test-result');
1284
1285 var apiKey = $('#mxchat_pinecone_api_key').val();
1286 var host = $('#mxchat_pinecone_host').val();
1287 var index = $('#mxchat_pinecone_index').val();
1288
1289 if (!apiKey || !host || !index) {
1290 resultDiv.html('<div class="notice notice-error"><p>Please fill in all required fields first.</p></div>').show();
1291 return;
1292 }
1293
1294 button.prop('disabled', true).text('Testing...');
1295 resultDiv.hide();
1296
1297 var ajaxUrl = (typeof mxchatPromptsAdmin !== 'undefined') ? mxchatPromptsAdmin.ajax_url : ajaxurl;
1298 var nonce = (typeof mxchatPromptsAdmin !== 'undefined') ? mxchatPromptsAdmin.prompts_setting_nonce :
1299 (typeof mxchatAdmin !== 'undefined') ? mxchatAdmin.setting_nonce : '';
1300
1301 $.ajax({
1302 url: ajaxUrl,
1303 type: 'POST',
1304 data: {
1305 action: 'mxchat_test_pinecone_connection',
1306 _ajax_nonce: nonce,
1307 api_key: apiKey,
1308 host: host,
1309 index_name: index
1310 },
1311 success: function(response) {
1312 if (response.success) {
1313 resultDiv.html('<div class="notice notice-success"><p><span class="dashicons dashicons-yes-alt"></span> ' + response.data.message + '</p></div>');
1314 } else {
1315 resultDiv.html('<div class="notice notice-error"><p><span class="dashicons dashicons-warning"></span> ' + response.data.message + '</p></div>');
1316 }
1317 resultDiv.show();
1318 },
1319 error: function() {
1320 resultDiv.html('<div class="notice notice-error"><p>Connection test failed. Please check your settings.</p></div>').show();
1321 },
1322 complete: function() {
1323 button.prop('disabled', false).text('Test Connection');
1324 }
1325 });
1326 });
1327 }
1328
1329 function checkPineconeCompatibility() {
1330 if ($('.mxchat-pinecone-compatibility-notice').length > 0) {
1331 return;
1332 }
1333
1334 var hasOldAddon = $('body').hasClass('mxchat-pinecone-addon-active') ||
1335 $('.pcm-card').length > 0;
1336
1337 if (hasOldAddon) {
1338 var compatibilityNotice = $(`
1339 <div class="notice notice-info mxchat-pinecone-compatibility-notice">
1340 <p><strong>Pinecone Integration Notice:</strong> We've detected you have the Pinecone add-on installed.
1341 Pinecone functionality is now built into the core plugin. You can safely deactivate the separate
1342 Pinecone add-on after confirming your settings are migrated below.</p>
1343 </div>
1344 `);
1345
1346 $('#mxchat-kb-tab-pinecone .mxchat-card').prepend(compatibilityNotice);
1347
1348 migratePineconeSettings();
1349 }
1350 }
1351
1352 function migratePineconeSettings() {
1353 if (typeof ajaxurl !== 'undefined') {
1354 $.ajax({
1355 url: ajaxurl,
1356 type: 'POST',
1357 data: {
1358 action: 'mxchat_migrate_pinecone_settings',
1359 _ajax_nonce: (typeof mxchatAdmin !== 'undefined') ? mxchatAdmin.setting_nonce : ''
1360 },
1361 success: function(response) {
1362 if (response.success && response.data.migrated) {
1363 location.reload();
1364 }
1365 },
1366 error: function() {
1367 //console.log('Pinecone settings migration not available');
1368 }
1369 });
1370 }
1371 }
1372
1373 // ============================================
1374 // OpenAI Vector Store functionality
1375 // ============================================
1376 initVectorStoreFeatures = function() {
1377 if ($('#openai-vectorstore').length === 0) {
1378 return;
1379 }
1380
1381 initVectorStoreToggle();
1382 };
1383
1384 function initVectorStoreToggle() {
1385 // Remove any existing handlers to prevent duplicates
1386 var $toggleInput = $('input[name="mxchat_openai_vectorstore_options[mxchat_use_openai_vectorstore]"]');
1387 $toggleInput.off('change.vectorstoreToggle');
1388
1389 // Ensure the success notice exists in the settings div (add if not present)
1390 // Check for both the JS-added class and any existing PHP-rendered success notice
1391 var settingsDiv = $('.mxchat-vectorstore-settings');
1392 if (settingsDiv.length > 0 && settingsDiv.find('.mxch-notice-success').length === 0) {
1393 var successNotice = $('<div class="mxch-notice mxch-notice-success mxchat-vectorstore-enabled-notice" style="margin-bottom: 20px; display: none;">' +
1394 '<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>' +
1395 '<span>OpenAI Vector Store is enabled. Queries will search your Vector Store for relevant content.</span>' +
1396 '</div>');
1397 settingsDiv.prepend(successNotice);
1398 }
1399
1400 // Add the toggle handler for UI only (form submit will handle the actual saving)
1401 $toggleInput.on('change.vectorstoreToggle', function() {
1402 var $checkbox = $(this);
1403 var isChecked = $checkbox.is(':checked');
1404 var settingsDiv = $('.mxchat-vectorstore-settings');
1405 var enabledNotice = settingsDiv.find('.mxchat-vectorstore-enabled-notice, .mxch-notice-success');
1406
1407 // Update the UI immediately
1408 if (isChecked) {
1409 settingsDiv.slideDown(300);
1410 enabledNotice.slideDown(300);
1411 } else {
1412 enabledNotice.slideUp(300);
1413 settingsDiv.slideUp(300);
1414 }
1415
1416 // Update sidebar badge for OpenAI Vector Store
1417 updateSidebarBadge('openai-vectorstore', isChecked);
1418 });
1419
1420 // Set initial state based on current checkbox value
1421 var currentToggle = $('input[name="mxchat_openai_vectorstore_options[mxchat_use_openai_vectorstore]"]');
1422 if (currentToggle.length > 0) {
1423 var settingsDiv = $('.mxchat-vectorstore-settings');
1424 var enabledNotice = settingsDiv.find('.mxchat-vectorstore-enabled-notice, .mxch-notice-success');
1425 if (currentToggle.is(':checked')) {
1426 settingsDiv.show();
1427 enabledNotice.show();
1428 } else {
1429 settingsDiv.hide();
1430 enabledNotice.hide();
1431 }
1432 }
1433 }
1434
1435 })(jQuery);