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 +244 -21 3.2.22 → 3.2.25 View file →
@@ -822,8 +822,16 @@
822 822 var stateEl = document.getElementById('mxchat-pinecone-docs-state');
823 823 var state = {};
824 824 try { state = stateEl ? JSON.parse(stateEl.textContent || '{}') : {}; } catch (e) { state = {}; }
825 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;
826 834 var ajaxUrl = (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.ajax_url) ? mxchatAdmin.ajax_url : ajaxurl;
827 835 var nonce = (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.settings_nonce) ? mxchatAdmin.settings_nonce :
828 836 ((typeof mxchatPromptsAdmin !== 'undefined' && mxchatPromptsAdmin.prompts_setting_nonce) ? mxchatPromptsAdmin.prompts_setting_nonce : '');
829 837
@@ -838,23 +846,95 @@
838 846 function currentType() { return $type.filter(':checked').val() || 'vector'; }
839 847 function hostValue() {
840 848 return $.trim($('#mxchat_pinecone_host').val() || '').replace(/^https?:\/\//i, '').replace(/\/+$/, '').toLowerCase();
841 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 + }
842 858 function refreshGate() {
843 859 var isDoc = currentType() === 'document';
844 860 $docs.toggle(isDoc);
845 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 + }
846 876 $submit.prop('disabled', blocked);
847 - $('#mxchat-pinecone-docs-gate').toggle(blocked);
848 877 }
849 - $type.off('change.pineconeDocs').on('change.pineconeDocs', refreshGate);
850 - $('#mxchat_pinecone_host').off('input.pineconeDocs change.pineconeDocs').on('input.pineconeDocs change.pineconeDocs', refreshGate);
851 - refreshGate();
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(); });
852 895
853 896 function post(data) {
854 897 data.nonce = nonce;
855 898 return $.post(ajaxUrl, data);
856 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();
857 937 function failMessage(response, fallback) {
858 938 if (response && response.data) {
859 939 if (typeof response.data === 'string') { return response.data; }
860 940 if (response.data.message) { return response.data.message; }
@@ -890,8 +970,13 @@
890 970 setStatus(true, 'Document index checked: ' + response.data.host + ' (' + response.data.dimension + ' dimensions).');
891 971 notice(response.data.ready ? 'success' : 'info', response.data.message);
892 972 refreshGate();
893 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 + }
894 979 notice('error', failMessage(response, 'Could not create the index.'));
895 980 }
896 981 }).fail(function () {
897 982 notice('error', 'Could not reach the server to create the index.');
@@ -934,45 +1019,173 @@
934 1019 $btn.prop('disabled', false).text('Check index');
935 1020 });
936 1021 });
937 1022
938 - // ---- Migrate (copy, batch by batch, resumable)
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.
939 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 + }
940 1073 function renderMigration(st) {
1074 + migrationState = (st && st.status) ? st : null;
941 1075 var $progress = $('#mxchat-pinecone-docs-migrate-progress');
942 1076 if (!st || !st.status) {
943 1077 $progress.hide();
944 1078 $('#mxchat-pinecone-docs-migrate-counts').text('');
945 1079 $('#mxchat-pinecone-docs-delete-old').hide();
1080 + $allNsRow.hide();
1081 + refreshGate();
946 1082 return;
947 1083 }
948 1084 var total = parseInt(st.total, 10) || 0;
949 1085 var copied = parseInt(st.copied, 10) || 0;
950 1086 var pct = total > 0 ? Math.min(100, Math.round(copied * 100 / total)) : (st.status === 'done' ? 100 : 0);
951 - var suffix = st.status === 'done' ? ' — done' : (st.status === 'source_deleted' ? ' — old index deleted' : (st.status === 'running' ? ' — copying…' : ''));
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 + }
952 1110 $progress.show().find('.mxch-progress-bar-fill').css('width', pct + '%');
953 - $progress.find('.mxch-progress-label').text(copied + ' of ' + total + ' records copied (' + pct + '%)' + suffix);
954 - $('#mxchat-pinecone-docs-migrate-counts').text(st.source_name ? ('Source: ' + st.source_name + ' (' + total + ' records)') : '');
955 - $('#mxchat-pinecone-docs-delete-old').toggle(st.status === 'done' && total > 0 && copied >= total);
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();
956 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 + }
957 1159 function migrateStep() {
958 - post({ action: 'mxchat_pinecone_docs_migrate', mode: 'step' }).done(function (response) {
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;
959 1162 if (response && response.success) {
960 1163 renderMigration(response.data.state);
961 1164 if (response.data.state && response.data.state.status === 'running') {
962 - setTimeout(migrateStep, 150);
1165 + setTimeout(migrateStep, 250);
963 1166 } else {
964 - $migrateBtn.prop('disabled', false).text('Migrate');
965 - notice('success', 'Copy finished: ' + response.data.state.copied + ' of ' + response.data.state.total + ' records are in the document index.');
1167 + migrateFinished(response.data.state || {});
966 1168 }
967 1169 } else {
968 - if (response && response.data && response.data.state) { renderMigration(response.data.state); }
969 - notice('error', failMessage(response, 'The copy stopped.') + ' Click Migrate to resume.');
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.'));
970 1173 $migrateBtn.prop('disabled', false).text('Migrate');
1174 + $allNs.prop('disabled', false);
971 1175 }
972 - }).fail(function () {
973 - notice('error', 'Could not reach the server. Click Migrate to resume.');
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.');
974 1186 $migrateBtn.prop('disabled', false).text('Migrate');
1187 + $allNs.prop('disabled', false);
975 1188 });
976 1189 }
977 1190 $migrateBtn.off('click.pineconeDocs').on('click.pineconeDocs', function () {
978 1191 var source = $.trim($('#mxchat_pinecone_vector_host').val() || '');
@@ -979,25 +1192,35 @@
979 1192 if (!source) {
980 1193 notice('error', 'Enter the host of the vector index to copy from.');
981 1194 return;
982 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 + }
983 1200 $migrateBtn.prop('disabled', true).text('Copying…');
1201 + $allNs.prop('disabled', true);
984 1202 $result.hide();
985 - post({ action: 'mxchat_pinecone_docs_migrate', mode: 'start', source_host: source }).done(function (response) {
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) {
986 1205 if (response && response.success) {
987 - renderMigration(response.data.state);
988 - if (response.data.state && response.data.state.status === 'running') {
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') {
989 1210 migrateStep();
990 1211 } else {
991 - $migrateBtn.prop('disabled', false).text('Migrate');
1212 + migrateFinished(st);
992 1213 }
993 1214 } else {
994 1215 notice('error', failMessage(response, 'The copy could not start.'));
995 1216 $migrateBtn.prop('disabled', false).text('Migrate');
1217 + $allNs.prop('disabled', false);
996 1218 }
997 1219 }).fail(function () {
998 1220 notice('error', 'Could not reach the server to start the copy.');
999 1221 $migrateBtn.prop('disabled', false).text('Migrate');
1222 + $allNs.prop('disabled', false);
1000 1223 });
1001 1224 });
1002 1225 renderMigration(state.migration || null);
1003 1226