PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.24
MxChat – AI Chatbot & Content Generation for WordPress v3.2.24
3.2.25 3.2.24 3.2.23 3.2.22 3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 All 156 releases
← All changes | js/content-selector.js +502 -7 3.2.1 → 3.2.24 View file →
@@ -8,11 +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 -
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 +
13 15 // Filter elements
14 - const $searchInput = $('#mxchat-kb-content-search');1
16 + const $searchInput = $('#mxchat-kb-content-search');
15 17 const $typeFilter = $('#mxchat-kb-content-type-filter');
16 18 const $statusFilter = $('#mxchat-kb-content-status-filter');
17 19 const $processedFilter = $('#mxchat-kb-processed-filter');
18 20
@@ -48,9 +50,9 @@
48 50 $('.mxchat-import-box').removeClass('active');
49 51 $box.addClass('active');
50 52
51 53 // Hide all input areas
52 - $('#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();
53 55
54 56 // Hide sitemap-specific sections (but NOT for sitemap option - let detection logic handle it)
55 57 if (option !== 'sitemap') {
56 58 $('#mxchat-detected-sitemaps, #mxchat-no-sitemaps, #mxchat-sitemaps-loading').hide();
@@ -105,11 +107,43 @@
105 107
106 108 // Add or update bot_id hidden field for PDF upload form
107 109 updateBotIdInForm('#mxchat-pdf-upload-form');
108 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;
109 127 }
110 128 });
111 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 +
112 146 // Helper function to add/update bot_id hidden field in forms
113 147 function updateBotIdInForm(formSelector) {
114 148 const $form = $(formSelector);
115 149 if ($form.length === 0) return;
@@ -233,11 +267,11 @@
233 267 <span class="${badgeClass}">${badgeText}</span>
234 268 ${isProcessed ? '<span class="mxchat-kb-last-updated">Last updated: ' + item.processed_date + '</span>' : ''}
235 269 </div>
236 270 <div class="mxchat-kb-content-meta">
237 - <span class="mxchat-kb-content-type">${item.type}</span>
271 + <span class="mxchat-kb-content-type">${item.type_label || item.type}</span>
238 272 <span class="mxchat-kb-content-date">${item.date}</span>
239 - <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>
240 274 </div>
241 275 <div class="mxchat-kb-content-excerpt">${item.excerpt}</div>
242 276 </div>
243 277 </div>
@@ -412,10 +446,22 @@
412 446 // Handle filter changes
413 447 $typeFilter.add($statusFilter).add($processedFilter).on('change', function() {
414 448 currentPage = 1; // Reset to first page on filter change
415 449 selectedItems.clear(); // Clear selection when filter changes
450 + updateMediaNote();
416 451 loadContent();
417 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();
418 464
419 465 // Process selected content
420 466 $processButton.on('click', function() {
421 467 if (selectedItems.size === 0) {
@@ -534,10 +580,15 @@
534 580 title: response.data.title || ('ID: ' + postId)
535 581 });
536 582 }
537 583
538 - $progressModal.find('.mxchat-kb-current-item')
539 - .text('Successfully ' + (isUpdate ? 'updated' : 'processed') + ': ' + response.data.title);
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);
540 591 } else {
541 592 failed++;
542 593 results.failed.push({
543 594 id: postId,
@@ -750,11 +801,455 @@
750 801 return;
751 802 }
752 803
753 804 initPineconeToggle();
805 + initPineconeIndexType();
754 806 initPineconeConnectionTest();
755 807 checkPineconeCompatibility();
756 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 + }
757 1252
758 1253 function initPineconeToggle() {
759 1254 // Remove any existing handlers to prevent duplicates
760 1255 var $toggleInput = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');