| 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) + '">« 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 »</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); |