| @@ -8,17 +8,13 @@ | ||
| 8 | 8 | const $pagination = $('.mxchat-kb-pagination'); |
| 9 | 9 | const $processButton = $('#mxchat-kb-process-selected'); |
| 10 | 10 | const $selectAll = $('#mxchat-kb-select-all'); |
| 11 | 11 | const $selectionCount = $('.mxchat-kb-selection-count'); |
| 12 | - const $acfPdfExtractCheckbox = $('#mxchat-kb-acf-pdf-extract'); | |
| 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. | |
| 13 | 14 | |
| 14 | - // Restore last-used ACF→PDF extract preference from the server-localized default. | |
| 15 | - if (typeof mxchatSelector !== 'undefined' && mxchatSelector.acfPdfExtractDefault) { | |
| 16 | - $acfPdfExtractCheckbox.prop('checked', true); | |
| 17 | - } | |
| 18 | - | |
| 19 | 15 | // Filter elements |
| 20 | - const $searchInput = $('#mxchat-kb-content-search');1 | |
| 16 | + const $searchInput = $('#mxchat-kb-content-search'); | |
| 21 | 17 | const $typeFilter = $('#mxchat-kb-content-type-filter'); |
| 22 | 18 | const $statusFilter = $('#mxchat-kb-content-status-filter'); |
| 23 | 19 | const $processedFilter = $('#mxchat-kb-processed-filter'); |
| 24 | 20 | |
| @@ -54,9 +50,9 @@ | ||
| 54 | 50 | $('.mxchat-import-box').removeClass('active'); |
| 55 | 51 | $box.addClass('active'); |
| 56 | 52 | |
| 57 | 53 | // Hide all input areas |
| 58 | - $('#mxchat-url-input-area, #mxchat-content-input-area, #mxchat-pdf-upload-area').hide(); | |
| 54 | + $('#mxchat-url-input-area, #mxchat-content-input-area, #mxchat-pdf-upload-area, #mxchat-document-upload-area, #mxchat-youtube-input-area').hide(); | |
| 59 | 55 | |
| 60 | 56 | // Hide sitemap-specific sections (but NOT for sitemap option - let detection logic handle it) |
| 61 | 57 | if (option !== 'sitemap') { |
| 62 | 58 | $('#mxchat-detected-sitemaps, #mxchat-no-sitemaps, #mxchat-sitemaps-loading').hide(); |
| @@ -111,11 +107,43 @@ | ||
| 111 | 107 | |
| 112 | 108 | // Add or update bot_id hidden field for PDF upload form |
| 113 | 109 | updateBotIdInForm('#mxchat-pdf-upload-form'); |
| 114 | 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; | |
| 115 | 127 | } |
| 116 | 128 | }); |
| 117 | 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 | + | |
| 118 | 146 | // Helper function to add/update bot_id hidden field in forms |
| 119 | 147 | function updateBotIdInForm(formSelector) { |
| 120 | 148 | const $form = $(formSelector); |
| 121 | 149 | if ($form.length === 0) return; |
| @@ -239,11 +267,11 @@ | ||
| 239 | 267 | <span class="${badgeClass}">${badgeText}</span> |
| 240 | 268 | ${isProcessed ? '<span class="mxchat-kb-last-updated">Last updated: ' + item.processed_date + '</span>' : ''} |
| 241 | 269 | </div> |
| 242 | 270 | <div class="mxchat-kb-content-meta"> |
| 243 | - <span class="mxchat-kb-content-type">${item.type}</span> | |
| 271 | + <span class="mxchat-kb-content-type">${item.type_label || item.type}</span> | |
| 244 | 272 | <span class="mxchat-kb-content-date">${item.date}</span> |
| 245 | - <span class="mxchat-kb-content-words">${item.word_count} words</span> | |
| 273 | + <span class="mxchat-kb-content-words">${item.size_label ? item.size_label : item.word_count + ' words'}</span> | |
| 246 | 274 | </div> |
| 247 | 275 | <div class="mxchat-kb-content-excerpt">${item.excerpt}</div> |
| 248 | 276 | </div> |
| 249 | 277 | </div> |
| @@ -418,10 +446,22 @@ | ||
| 418 | 446 | // Handle filter changes |
| 419 | 447 | $typeFilter.add($statusFilter).add($processedFilter).on('change', function() { |
| 420 | 448 | currentPage = 1; // Reset to first page on filter change |
| 421 | 449 | selectedItems.clear(); // Clear selection when filter changes |
| 450 | + updateMediaNote(); | |
| 422 | 451 | loadContent(); |
| 423 | 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(); | |
| 424 | 464 | |
| 425 | 465 | // Process selected content |
| 426 | 466 | $processButton.on('click', function() { |
| 427 | 467 | if (selectedItems.size === 0) { |
| @@ -506,15 +546,13 @@ | ||
| 506 | 546 | .text((isUpdate ? 'Updating' : 'Processing') + ' ' + (index + 1) + ' of ' + totalToProcess + '...'); |
| 507 | 547 | $progressModal.find('.mxchat-kb-progress-fill').css('width', percent + '%'); |
| 508 | 548 | |
| 509 | 549 | // UPDATED: Prepare AJAX data with bot_id |
| 510 | - const extractAcfPdfs = $acfPdfExtractCheckbox.prop('checked') ? 1 : 0; | |
| 511 | 550 | const ajaxData = { |
| 512 | 551 | action: 'mxchat_process_selected_content', |
| 513 | 552 | nonce: mxchatSelector.nonce, |
| 514 | 553 | post_ids: [postId], |
| 515 | - is_update: isUpdate, | |
| 516 | - extract_acf_pdfs: extractAcfPdfs | |
| 554 | + is_update: isUpdate | |
| 517 | 555 | }; |
| 518 | 556 | |
| 519 | 557 | // Add bot_id if multi-bot is active and not default |
| 520 | 558 | if (currentBotId && currentBotId !== 'default') { |
| @@ -763,11 +801,429 @@ | ||
| 763 | 801 | return; |
| 764 | 802 | } |
| 765 | 803 | |
| 766 | 804 | initPineconeToggle(); |
| 805 | + initPineconeIndexType(); | |
| 767 | 806 | initPineconeConnectionTest(); |
| 768 | 807 | checkPineconeCompatibility(); |
| 769 | 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 | + } | |
| 770 | 1226 | |
| 771 | 1227 | function initPineconeToggle() { |
| 772 | 1228 | // Remove any existing handlers to prevent duplicates |
| 773 | 1229 | var $toggleInput = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]'); |