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 +471 -13 3.2.14 → 3.2.24 View file →
@@ -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, #mxchat-youtube-input-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();
@@ -112,8 +108,16 @@
112 108 // Add or update bot_id hidden field for PDF upload form
113 109 updateBotIdInForm('#mxchat-pdf-upload-form');
114 110 break;
115 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 +
116 120 case 'youtube':
117 121 // Show YouTube import area
118 122 $('#mxchat-youtube-input-area').show();
119 123
@@ -263,11 +267,11 @@
263 267 <span class="${badgeClass}">${badgeText}</span>
264 268 ${isProcessed ? '<span class="mxchat-kb-last-updated">Last updated: ' + item.processed_date + '</span>' : ''}
265 269 </div>
266 270 <div class="mxchat-kb-content-meta">
267 - <span class="mxchat-kb-content-type">${item.type}</span>
271 + <span class="mxchat-kb-content-type">${item.type_label || item.type}</span>
268 272 <span class="mxchat-kb-content-date">${item.date}</span>
269 - <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>
270 274 </div>
271 275 <div class="mxchat-kb-content-excerpt">${item.excerpt}</div>
272 276 </div>
273 277 </div>
@@ -442,10 +446,22 @@
442 446 // Handle filter changes
443 447 $typeFilter.add($statusFilter).add($processedFilter).on('change', function() {
444 448 currentPage = 1; // Reset to first page on filter change
445 449 selectedItems.clear(); // Clear selection when filter changes
450 + updateMediaNote();
446 451 loadContent();
447 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();
448 464
449 465 // Process selected content
450 466 $processButton.on('click', function() {
451 467 if (selectedItems.size === 0) {
@@ -530,15 +546,13 @@
530 546 .text((isUpdate ? 'Updating' : 'Processing') + ' ' + (index + 1) + ' of ' + totalToProcess + '...');
531 547 $progressModal.find('.mxchat-kb-progress-fill').css('width', percent + '%');
532 548
533 549 // UPDATED: Prepare AJAX data with bot_id
534 - const extractAcfPdfs = $acfPdfExtractCheckbox.prop('checked') ? 1 : 0;
535 550 const ajaxData = {
536 551 action: 'mxchat_process_selected_content',
537 552 nonce: mxchatSelector.nonce,
538 553 post_ids: [postId],
539 - is_update: isUpdate,
540 - extract_acf_pdfs: extractAcfPdfs
554 + is_update: isUpdate
541 555 };
542 556
543 557 // Add bot_id if multi-bot is active and not default
544 558 if (currentBotId && currentBotId !== 'default') {
@@ -787,11 +801,455 @@
787 801 return;
788 802 }
789 803
790 804 initPineconeToggle();
805 + initPineconeIndexType();
791 806 initPineconeConnectionTest();
792 807 checkPineconeCompatibility();
793 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 + }
794 1252
795 1253 function initPineconeToggle() {
796 1254 // Remove any existing handlers to prevent duplicates
797 1255 var $toggleInput = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');