PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.25
MxChat – AI Chatbot & Content Generation for WordPress v3.2.25
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 +467 -3 3.2.17 → 3.2.25 View file →
@@ -50,9 +50,9 @@
50 50 $('.mxchat-import-box').removeClass('active');
51 51 $box.addClass('active');
52 52
53 53 // Hide all input areas
54 - $('#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();
55 55
56 56 // Hide sitemap-specific sections (but NOT for sitemap option - let detection logic handle it)
57 57 if (option !== 'sitemap') {
58 58 $('#mxchat-detected-sitemaps, #mxchat-no-sitemaps, #mxchat-sitemaps-loading').hide();
@@ -108,8 +108,16 @@
108 108 // Add or update bot_id hidden field for PDF upload form
109 109 updateBotIdInForm('#mxchat-pdf-upload-form');
110 110 break;
111 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 +
112 120 case 'youtube':
113 121 // Show YouTube import area
114 122 $('#mxchat-youtube-input-area').show();
115 123
@@ -259,11 +267,11 @@
259 267 <span class="${badgeClass}">${badgeText}</span>
260 268 ${isProcessed ? '<span class="mxchat-kb-last-updated">Last updated: ' + item.processed_date + '</span>' : ''}
261 269 </div>
262 270 <div class="mxchat-kb-content-meta">
263 - <span class="mxchat-kb-content-type">${item.type}</span>
271 + <span class="mxchat-kb-content-type">${item.type_label || item.type}</span>
264 272 <span class="mxchat-kb-content-date">${item.date}</span>
265 - <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>
266 274 </div>
267 275 <div class="mxchat-kb-content-excerpt">${item.excerpt}</div>
268 276 </div>
269 277 </div>
@@ -438,10 +446,22 @@
438 446 // Handle filter changes
439 447 $typeFilter.add($statusFilter).add($processedFilter).on('change', function() {
440 448 currentPage = 1; // Reset to first page on filter change
441 449 selectedItems.clear(); // Clear selection when filter changes
450 + updateMediaNote();
442 451 loadContent();
443 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();
444 464
445 465 // Process selected content
446 466 $processButton.on('click', function() {
447 467 if (selectedItems.size === 0) {
@@ -781,11 +801,455 @@
781 801 return;
782 802 }
783 803
784 804 initPineconeToggle();
805 + initPineconeIndexType();
785 806 initPineconeConnectionTest();
786 807 checkPineconeCompatibility();
787 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 + }
788 1252
789 1253 function initPineconeToggle() {
790 1254 // Remove any existing handlers to prevent duplicates
791 1255 var $toggleInput = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');