PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.7
MxChat – AI Chatbot & Content Generation for WordPress v3.1.7
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 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/mxchat-admin.js +114 -334 3.2.153.1.7 View file →
@@ -10,65 +10,8 @@
10 10 timeout = setTimeout(later, wait);
11 11 };
12 12 }
13 13
14 -// ─── Modal close safety ──────────────────────────────────────────────────
15 -// A click event fires on the common ancestor of its mousedown and mouseup, so
16 -// releasing a text-selection drag past a dialog's edge dispatches the click on
17 -// the overlay. Modals holding editable fields therefore never close on the
18 -// overlay and confirm before discarding; read-only ones require the whole
19 -// gesture to land on the overlay.
20 -
21 -// Serializes a modal's visible fields so edits can be detected on close.
22 -function mxchatModalSnapshot(modal) {
23 - const parts = [];
24 - modal.querySelectorAll('input, textarea, select').forEach((field) => {
25 - if (field.type === 'hidden') return;
26 - parts.push(field.type === 'checkbox' || field.type === 'radio' ? (field.checked ? '1' : '0') : field.value);
27 - });
28 - return JSON.stringify(parts);
29 -}
30 -
31 -// Wraps a modal's closeModal so an explicit close confirms when fields changed.
32 -function mxchatGuardedClose(modal, closeModal) {
33 - const snapshot = mxchatModalSnapshot(modal);
34 - const message = (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.discard_changes_confirm)
35 - ? mxchatAdmin.discard_changes_confirm
36 - : 'Discard your unsaved changes?';
37 - return (e) => {
38 - if (mxchatModalSnapshot(modal) !== snapshot && !window.confirm(message)) return;
39 - closeModal(e);
40 - };
41 -}
42 -
43 -// Esc-to-close for a modal. Binds one named keydown handler and returns the detach
44 -// function the modal's own close path must call. `{ once: true }` cannot be used
45 -// here: it removes the listener on the first keydown of ANY key, so typing a single
46 -// character killed Esc for the rest of the modal's life, and every open that saw no
47 -// keypress left another listener stacked on document.
48 -function mxchatBindEscClose(modal, onEscape) {
49 - function onKeydown(e) {
50 - if (e.key === 'Escape' && modal.classList.contains('active')) {
51 - onEscape(e);
52 - }
53 - }
54 - document.addEventListener('keydown', onKeydown);
55 - return function detachEsc() {
56 - document.removeEventListener('keydown', onKeydown);
57 - };
58 -}
59 -
60 -// Overlay-close for read-only modals, ignoring clicks that began inside.
61 -function mxchatDragSafeOverlayClose(modal, closeModal) {
62 - let downOnOverlay = false;
63 - modal.addEventListener('mousedown', (e) => { downOnOverlay = (e.target === modal); });
64 - modal.addEventListener('click', (e) => {
65 - const overlayGesture = downOnOverlay;
66 - downOnOverlay = false;
67 - if (e.target === modal && overlayGesture) closeModal(e);
68 - });
69 -}
70 -
71 14 // Helper function to open edit modal for intents/actions
72 15 function mxchatOpenEditModal(intentId, phrases) {
73 16 const modal = document.getElementById('mxchat-edit-modal');
74 17 if (!modal) return;
@@ -87,14 +30,9 @@
87 30 modal.classList.add('active');
88 31 });
89 32
90 33 // Set up close handlers
91 - let detachEsc = null;
92 34 const closeModal = () => {
93 - if (detachEsc) {
94 - detachEsc();
95 - detachEsc = null;
96 - }
97 35 modal.classList.remove('active');
98 36 setTimeout(() => {
99 37 modal.style.display = 'none';
100 38 }, 300); // Match the CSS transition time
@@ -99,25 +37,26 @@
99 37 modal.style.display = 'none';
100 38 }, 300); // Match the CSS transition time
101 39 };
102 40
103 - // This modal holds edits: close only on explicit controls, confirming when dirty.
104 - const guardedClose = mxchatGuardedClose(modal, closeModal);
105 -
106 41 // Close button handler
107 42 const closeBtn = modal.querySelector('.mxchat-modal-close');
108 43 if (closeBtn) {
109 - closeBtn.onclick = guardedClose;
44 + closeBtn.onclick = closeModal;
110 45 }
111 46
112 47 // Cancel button handler
113 48 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
114 49 if (cancelBtn) {
115 - cancelBtn.onclick = guardedClose;
50 + cancelBtn.onclick = closeModal;
116 51 }
117 52
118 - // Escape key to close modal
119 - detachEsc = mxchatBindEscClose(modal, guardedClose);
53 + // Click outside modal to close
54 + modal.onclick = (e) => {
55 + if (e.target === modal) {
56 + closeModal();
57 + }
58 + };
120 59
121 60 // Focus the textarea
122 61 phrasesField.focus();
123 62 }
@@ -229,14 +168,9 @@
229 168 modal.classList.add('active');
230 169 });
231 170
232 171 // Set up close handlers
233 - let detachEsc = null;
234 172 const closeModal = () => {
235 - if (detachEsc) {
236 - detachEsc();
237 - detachEsc = null;
238 - }
239 173 modal.classList.remove('active');
240 174 setTimeout(() => {
241 175 modal.style.display = 'none';
242 176 }, 300); // Match the CSS transition time
@@ -241,25 +175,33 @@
241 175 modal.style.display = 'none';
242 176 }, 300); // Match the CSS transition time
243 177 };
244 178
245 - // This modal holds edits: close only on explicit controls, confirming when dirty.
246 - const guardedClose = mxchatGuardedClose(modal, closeModal);
247 -
248 179 // Close button handler
249 180 const closeBtn = modal.querySelector('.mxchat-modal-close');
250 181 if (closeBtn) {
251 - closeBtn.onclick = guardedClose;
182 + closeBtn.onclick = closeModal;
252 183 }
253 184
254 185 // Cancel button handler
255 186 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
256 187 if (cancelBtn) {
257 - cancelBtn.onclick = guardedClose;
188 + cancelBtn.onclick = closeModal;
258 189 }
259 190
191 + // Click outside modal to close
192 + modal.onclick = (e) => {
193 + if (e.target === modal) {
194 + closeModal();
195 + }
196 + };
197 +
260 198 // Escape key to close modal
261 - detachEsc = mxchatBindEscClose(modal, guardedClose);
199 + document.addEventListener('keydown', function(e) {
200 + if (e.key === 'Escape' && modal.classList.contains('active')) {
201 + closeModal();
202 + }
203 + }, { once: true });
262 204
263 205 // Focus the first field
264 206 labelField.focus();
265 207
@@ -535,15 +477,9 @@
535 477 }
536 478 });
537 479 }
538 480
539 - // .mxchat-la-field — the live-agent schedule editors' day inputs (plans
540 - // 8ccaa2 + 99d7a4, one editor per channel). They are nameless by design:
541 - // each editor folds them into its own hidden live_agent_schedule_<channel>
542 - // input and fires ONE change, which this same handler then saves. Without
543 - // the exclusion each keystroke would POST a nameless field the server can
544 - // only reject.
545 - $autosaveSections.find('input, textarea, select').not('#model, #openrouter_selected_model, .mxchat-la-field').on('change', function() {
481 + $autosaveSections.find('input, textarea, select').not('#model, #openrouter_selected_model').on('change', function() {
546 482 const $field = $(this);
547 483 const name = $field.attr('name');
548 484
549 485 // Debounce rate limit fields to prevent UI freezing from rapid changes
@@ -1058,9 +994,8 @@
1058 994 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1059 995 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1060 996 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1061 997 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1062 - <button class="mxchat-model-category-btn" data-category="custom">Custom / Local</button>
1063 998 </div>
1064 999 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1065 1000 </div>
1066 1001 <div class="mxchat-model-selector-modal-footer">
@@ -1075,40 +1010,59 @@
1075 1010 // MOVE THIS OUTSIDE - Make it a property of the window object so it's accessible globally
1076 1011 window.populateModelsGrid = function(filter = '', category = 'all') {
1077 1012 const $grid = $('#mxchat_models_grid');
1078 1013 $grid.empty();
1079 -
1080 - // Catalog refactor (plan-d14e89): when class-mxchat-model-catalog.php
1081 - // is loaded (via wp_localize_script as mxchatChatModelCatalog), use
1082 - // its data so a single edit there flows to this picker grid. The
1083 - // inline fallback below keeps the picker working if for some reason
1084 - // the localize hasn't run (e.g. legacy admin page bootstrap order).
1085 - const models = (typeof mxchatChatModelCatalog === 'object' && mxchatChatModelCatalog) ? mxchatChatModelCatalog : {
1014 +
1015 + const models = {
1086 1016 openrouter: [
1087 1017 { value: 'openrouter', label: 'OpenRouter', description: 'Access 100+ models from multiple providers (add API key to browse)' }
1088 1018 ],
1089 1019 gemini: [
1090 - { value: 'gemini-3.5-flash', label: 'Gemini 3.5 Flash', description: 'Stable — newest Flash generation, recommended default' },
1020 + { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro', description: 'Most intelligent model - multimodal understanding & agentic' },
1021 + { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash', description: 'Most balanced - speed, scale & frontier intelligence' },
1022 + { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', description: 'Advanced thinking - code, math, STEM & long context' },
1023 + { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', description: 'Best price-performance with thinking capabilities' },
1024 + { value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash-Lite', description: 'Ultra fast - optimized for cost & high throughput' },
1025 + { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Deprecated March 2026' },
1026 + { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Deprecated March 2026' },
1027 + { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Deprecated September 2025' },
1028 + { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Deprecated September 2025' },
1091 1029 ],
1092 1030 openai: [
1031 + { value: 'gpt-5.4', label: 'GPT-5.4', description: 'Flagship reasoning and coding model' },
1032 + { value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini', description: 'Fast and affordable with 400K context' },
1033 + { value: 'gpt-5.4-nano', label: 'GPT-5.4 Nano', description: 'Fastest and cheapest for lightweight tasks' },
1034 + { value: 'gpt-5.3-chat-latest', label: 'GPT-5.3 Chat', description: 'Optimized for natural conversations with reduced hallucinations' },
1035 + { value: 'gpt-5.2', label: 'GPT-5.2', description: 'Best general-purpose & agentic model with fast responses' },
1093 1036 { value: 'gpt-5.1-chat-latest', label: 'GPT-5.1 Chat Latest', description: 'Recommended for most use cases' },
1037 + { value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1', description: 'Flagship for coding & agentic tasks with low reasoning (400K context)' },
1038 + { value: 'gpt-5', label: 'GPT-5', description: 'Flagship for coding, reasoning, and agentic tasks across domains' },
1039 + { value: 'gpt-5-mini', label: 'GPT-5 Mini', description: 'Fast and lightweight' },
1040 + { value: 'gpt-5-nano', label: 'GPT-5 Nano', description: 'Fastest and cheapest; ideal for summarization and classification' },
1094 1041 ],
1095 1042 claude: [
1096 - { value: 'claude-fable-5', label: 'Claude Fable 5', description: 'Latest Flagship — newest and most capable Anthropic model' },
1097 - { value: 'claude-opus-5', label: 'Claude Opus 5', description: 'Latest Opus — best for complex agentic and coding work' },
1098 - { value: 'claude-opus-4-8', label: 'Claude Opus 4.8', description: 'Previous Opus generation' },
1099 - { value: 'claude-opus-4-7', label: 'Claude Opus 4.7', description: 'Previous Anthropic flagship model' },
1043 + { value: 'claude-opus-4-6', label: 'Claude Opus 4.6', description: 'Most capable Claude model - recommended' },
1044 + { value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', description: 'Latest Sonnet - excellent balance of speed and capability' },
1045 + { value: 'claude-opus-4-5', label: 'Claude Opus 4.5', description: 'Highly capable for complex tasks' },
1046 + { value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5', description: 'Best for complex agents and coding' },
1047 + { value: 'claude-opus-4-1-20250805', label: 'Claude Opus 4.1', description: 'Exceptional for specialized complex tasks' },
1048 + { value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5', description: 'Fastest and most intelligent Haiku' },
1049 + { value: 'claude-opus-4-20250514', label: 'Claude 4 Opus', description: 'Complex tasks' },
1050 + { value: 'claude-sonnet-4-20250514', label: 'Claude 4 Sonnet', description: 'High performance' },
1100 1051 ],
1101 1052 xai: [
1102 - { value: 'grok-4-0709', label: 'Grok 4', description: 'Latest flagship model' },
1053 + { value: 'grok-4-1-fast-reasoning', label: 'Grok 4.1 Fast (Reasoning)', description: '2M context window and reasoning' },
1054 + { value: 'grok-4-1-fast-non-reasoning', label: 'Grok 4.1 Fast (Non-Reasoning)', description: '2M context window and faster responses' },
1055 + { value: 'grok-4-0709', label: 'Grok 4', description: 'Latest flagship model - unparalleled performance in natural language, math and reasoning' },
1056 + { value: 'grok-3-beta', label: 'Grok-3', description: 'Powerful model with 131K context' },
1057 + { value: 'grok-3-fast-beta', label: 'Grok-3 Fast', description: 'High performance with faster responses' },
1058 + { value: 'grok-3-mini-beta', label: 'Grok-3 Mini', description: 'Affordable model with good performance' },
1059 + { value: 'grok-3-mini-fast-beta', label: 'Grok-3 Mini Fast', description: 'Quick and cost-effective' },
1060 + { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' },
1103 1061 ],
1104 1062 deepseek: [
1105 - { value: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash', description: 'Fast and cost-effective' },
1106 - { value: 'deepseek-v4-pro', label: 'DeepSeek V4 Pro', description: 'Most capable DeepSeek model' },
1063 + { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
1107 1064 ],
1108 - custom: [
1109 - { value: 'custom-provider', label: 'Custom Provider', description: 'OpenAI-compatible local LLM — configure in API Keys tab' },
1110 - ],
1111 1065 };
1112 1066
1113 1067 let allModels = [];
1114 1068 Object.keys(models).forEach(key => {
@@ -1149,9 +1103,8 @@
1149 1103 if (modelValue.startsWith('gemini-')) return '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" class="mxchat-model-icon-gemini"><defs><path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"></path></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"></use></clipPath><path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"></path><path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"></path><path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"></path><path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"></path></svg>';
1150 1104 if (modelValue.startsWith('gpt-')) return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" class="mxchat-model-icon-openai"><path fill="currentColor" d="M297 131a80.6 80.6 0 0 0-93.7-104.2 80.6 80.6 0 0 0-137 29A80.6 80.6 0 0 0 23 189a80.6 80.6 0 0 0 93.7 104.2 80.6 80.6 0 0 0 137-29A80.7 80.7 0 0 0 297.1 131zM176.9 299c-14 .1-27.6-4.8-38.4-13.8l1.9-1 63.7-36.9c3.3-1.8 5.3-5.3 5.2-9v-89.9l27 15.6c.3.1.4.4.5.7v74.4a60 60 0 0 1-60 60zM47.9 244a59.7 59.7 0 0 1-7.1-40.1l1.9 1.1 63.7 36.8c3.2 1.9 7.2 1.9 10.5 0l77.8-45V228c0 .3-.2.6-.4.8L129.9 266a60 60 0 0 1-82-22zM31.2 105c7-12.2 18-21.5 31.2-26.3v75.8c0 3.7 2 7.2 5.2 9l77.8 45-27 15.5a1 1 0 0 1-.9 0L53.1 187a60 60 0 0 1-22-82zm221.2 51.5-77.8-45 27-15.5a1 1 0 0 1 .9 0l64.4 37.1a60 60 0 0 1-9.3 108.2v-75.8c0-3.7-2-7.2-5.2-9zm26.8-40.4-1.9-1.1-63.7-36.8a10.4 10.4 0 0 0-10.5 0L125.4 123V92c0-.3 0-.6.3-.8L190.1 54a60 60 0 0 1 89.1 62.1zm-168.5 55.4-27-15.5a1 1 0 0 1-.4-.7V80.9a60 60 0 0 1 98.3-46.1l-1.9 1L116 72.8a10.3 10.3 0 0 0-5.2 9v89.8zm14.6-31.5 34.7-20 34.6 20v40L160 200l-34.7-20z"></path></svg>';
1151 1105 if (modelValue.startsWith('claude-')) return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 176" fill="none" class="mxchat-model-icon-claude"><path fill="currentColor" d="m147.487 0l70.081 175.78H256L185.919 0zM66.183 106.221l23.98-61.774l23.98 61.774zM70.07 0L0 175.78h39.18l14.33-36.914h73.308l14.328 36.914h39.179L110.255 0z"></path></svg>';
1152 1106 if (modelValue.startsWith('grok-')) return '<svg fill="currentColor" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="mxchat-model-icon-xai"><path d="M6.469 8.776L16.512 23h-4.464L2.005 8.776H6.47zm-.004 7.9l2.233 3.164L6.467 23H2l4.465-6.324zM22 2.582V23h-3.659V7.764L22 2.582zM22 1l-9.952 14.095-2.233-3.163L17.533 1H22z"></path></svg>';
1153 - if (modelValue === 'custom-provider' || modelValue.startsWith('custom-')) return '<span class="dashicons dashicons-admin-site-alt3" style="font-size: 24px; color: #2c8a3d;"></span>';
1154 1107 if (modelValue.startsWith('deepseek-')) return '<svg height="1em" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg" class="mxchat-model-icon-deepseek"><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="currentColor"></path></svg>';
1155 1108 return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
1156 1109 }
1157 1110
@@ -1664,71 +1617,8 @@
1664 1617 });
1665 1618
1666 1619 }
1667 1620
1668 -// 3.2.3: Confirmation dialog shown when the user attempts to switch embedding
1669 -// models after they've already embedded content with a different model. Mixing
1670 -// embeddings from two models silently breaks similarity matching.
1671 -function showEmbeddingSwitchWarning(data, newValue, onChoice) {
1672 - $('#mxchat_embedding_switch_warning').remove();
1673 -
1674 - const dimsBlock = data.dims_differ ? `
1675 - <div class="mxchat-embed-warn-dims">
1676 - <strong>Dimension mismatch:</strong>
1677 - Existing vectors are ${data.active_dims}-dimensional, but ${data.new_label}
1678 - produces ${data.new_dims}-dimensional vectors.
1679 - If you use Pinecone, your index will reject queries entirely until you re-embed.
1680 - </div>` : '';
1681 -
1682 - const $modal = $(`
1683 - <div id="mxchat_embedding_switch_warning" class="mxchat-embed-warn-overlay">
1684 - <div class="mxchat-embed-warn-dialog">
1685 - <div class="mxchat-embed-warn-header">
1686 - <span class="mxchat-embed-warn-icon">⚠️</span>
1687 - <h3>Switching embedding models will break similarity matching</h3>
1688 - </div>
1689 - <div class="mxchat-embed-warn-body">
1690 - <p>
1691 - Your knowledge base and actions are currently embedded with
1692 - <code>${data.active_label}</code>. Switching to
1693 - <code>${data.new_label}</code> means new queries are embedded with
1694 - a different model than the stored vectors — the chatbot will return
1695 - inaccurate results or fail to match anything.
1696 - </p>
1697 - ${dimsBlock}
1698 - <p><strong>To switch safely:</strong></p>
1699 - <ol>
1700 - <li>Go to <em>Knowledge Base</em> and delete all existing entries.</li>
1701 - <li>Go to <em>Actions</em> and delete all existing actions.</li>
1702 - <li>Come back here, switch the model, then re-import your content and re-add your actions.</li>
1703 - </ol>
1704 - </div>
1705 - <div class="mxchat-embed-warn-footer">
1706 - <button type="button" class="button button-secondary" id="mxchat_embed_warn_cancel">Cancel — keep ${data.active_label}</button>
1707 - <button type="button" class="button button-primary mxchat-embed-warn-danger" id="mxchat_embed_warn_continue">Switch anyway (I'll handle it)</button>
1708 - </div>
1709 - </div>
1710 - </div>
1711 - `);
1712 -
1713 - $('body').append($modal);
1714 -
1715 - let resolved = false;
1716 - function resolve(confirmed) {
1717 - if (resolved) return;
1718 - resolved = true;
1719 - $modal.remove();
1720 - if (typeof onChoice === 'function') onChoice(confirmed);
1721 - }
1722 -
1723 - // Click on the overlay background dismisses (cancel)
1724 - $modal.on('click', function(e) {
1725 - if (e.target === $modal[0]) resolve(false);
1726 - });
1727 - $modal.find('#mxchat_embed_warn_cancel').on('click', function() { resolve(false); });
1728 - $modal.find('#mxchat_embed_warn_continue').on('click', function() { resolve(true); });
1729 -}
1730 -
1731 1621 // Embedding model selector - completely separate from chat model selector
1732 1622 function setupMxChatEmbeddingModelSelector() {
1733 1623 const $embeddingModelSelect = $('#embedding_model');
1734 1624
@@ -1909,53 +1799,25 @@
1909 1799 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1910 1800 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1911 1801 });
1912 1802
1913 - // 3.2.3: Commit a model change — same as the original click handler, just
1914 - // factored out so it can be invoked from both the safe path and the
1915 - // post-confirmation path.
1916 - function commitEmbeddingModelChange(modelValue) {
1803 + // Use a direct selector to avoid conflicts with other card elements
1804 + $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1805 + e.stopPropagation(); // Prevent event bubbling
1806 + const modelValue = $(this).data('value');
1807 +
1808 + // Important: Only update this specific select element
1917 1809 $embeddingModelSelect.val(modelValue);
1810 +
1811 + // Manually trigger change only on this element
1918 1812 const changeEvent = new Event('change', { bubbles: true });
1919 1813 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1814 +
1815 + // Update button text
1920 1816 updateButtonText();
1817 +
1818 + // Hide modal
1921 1819 $('#' + embeddingModalId).hide();
1922 - }
1923 -
1924 - // Use a direct selector to avoid conflicts with other card elements
1925 - $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1926 - e.stopPropagation(); // Prevent event bubbling
1927 - const modelValue = $(this).data('value');
1928 - const previousValue = $embeddingModelSelect.val();
1929 -
1930 - // No-op if the user clicked the already-selected card
1931 - if (modelValue === previousValue) {
1932 - $('#' + embeddingModalId).hide();
1933 - return;
1934 - }
1935 -
1936 - // 3.2.3: Preflight — if the user has content embedded with a different
1937 - // model, surface a confirmation dialog before committing the switch.
1938 - $.post(ajaxurl, {
1939 - action: 'mxchat_check_embedding_switch',
1940 - security: (typeof mxchatAdmin !== 'undefined' ? mxchatAdmin.nonce : ''),
1941 - new_model: modelValue
1942 - }).done(function(resp) {
1943 - if (resp && resp.success && resp.data && resp.data.is_mismatch) {
1944 - showEmbeddingSwitchWarning(resp.data, modelValue, function(confirmed) {
1945 - if (confirmed) {
1946 - commitEmbeddingModelChange(modelValue);
1947 - }
1948 - });
1949 - } else {
1950 - commitEmbeddingModelChange(modelValue);
1951 - }
1952 - }).fail(function() {
1953 - // If preflight fails, fall back to the original behavior so a network
1954 - // hiccup doesn't block legitimate model switches.
1955 - commitEmbeddingModelChange(modelValue);
1956 - });
1957 - return;
1958 1820 });
1959 1821
1960 1822 // Close modal when clicking outside - use namespaced events
1961 1823 $(window).on('click.embeddingModelSelector', function(event) {
@@ -2149,55 +2011,8 @@
2149 2011 }
2150 2012 });
2151 2013 }
2152 2014
2153 - // Live agent availability schedules (plans 8ccaa2 + 99d7a4).
2154 - // The editor markup exists once PER CHANNEL (Slack + Telegram tabs), so
2155 - // everything is scoped inside each .mxchat-la-schedule container via classes
2156 - // — no page-global ids. Each editor collects its own day grid into its own
2157 - // hidden live_agent_schedule_<channel> input as JSON and fires one change
2158 - // event, so the existing autosave handler above does the POST.
2159 - $('.mxchat-la-schedule').each(function() {
2160 - const $laSchedule = $(this);
2161 - const $laEnabled = $laSchedule.find('.mxchat-la-enabled');
2162 - const $laHidden = $laSchedule.find('.mxchat-la-hidden');
2163 - const $laDays = $laSchedule.find('.mxchat-la-days');
2164 - const $laStatus = $laSchedule.find('.mxchat-la-status-text');
2165 -
2166 - function laCollect() {
2167 - const days = {};
2168 - $laDays.find('.mxchat-la-day').each(function() {
2169 - const $day = $(this);
2170 - const n = $day.data('day');
2171 - days[n] = {
2172 - enabled: $day.find('.mxchat-la-day-enabled').is(':checked'),
2173 - start: $day.find('.mxchat-la-start').val() || '09:00',
2174 - end: $day.find('.mxchat-la-end').val() || '17:00'
2175 - };
2176 - });
2177 - return { enabled: $laEnabled.is(':checked'), days: days };
2178 - }
2179 -
2180 - function laSync() {
2181 - const schedule = laCollect();
2182 - $laSchedule.toggleClass('is-active', schedule.enabled);
2183 - $laDays.attr('aria-hidden', schedule.enabled ? 'false' : 'true');
2184 - $laStatus.text(
2185 - schedule.enabled
2186 - ? (mxchatAdmin.i18n_scheduled_hours || 'Scheduled hours')
2187 - : (mxchatAdmin.i18n_always_available || 'Always available')
2188 - );
2189 - $laDays.find('.mxchat-la-day').each(function() {
2190 - const $day = $(this);
2191 - $day.toggleClass('is-on', $day.find('.mxchat-la-day-enabled').is(':checked'));
2192 - });
2193 - // Hand this channel's schedule to the shared autosave transport.
2194 - $laHidden.val(JSON.stringify(schedule)).trigger('change');
2195 - }
2196 -
2197 - $laSchedule.find('.mxchat-la-field').on('change', laSync);
2198 - });
2199 -
2200 2015 // Function to adjust the textarea height to content
2201 2016 function adjustTextareaHeight() {
2202 2017 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
2203 2018 this.style.height = this.scrollHeight + 'px'; // Expand to content height
@@ -2413,12 +2228,14 @@
2413 2228 setTimeout(() => {
2414 2229 noticeContainer.remove();
2415 2230 }, 300);
2416 2231 });
2417 -
2418 - // Click outside to close (drag-safe)
2419 - mxchatDragSafeOverlayClose(noticeContainer, function() {
2420 - closeButton.click();
2232 +
2233 + // Click outside to close
2234 + noticeContainer.addEventListener('click', function(e) {
2235 + if (e.target === noticeContainer) {
2236 + closeButton.click();
2237 + }
2421 2238 });
2422 2239
2423 2240 // Show with animation
2424 2241 setTimeout(() => {
@@ -2464,12 +2281,14 @@
2464 2281 setTimeout(() => {
2465 2282 noticeContainer.remove();
2466 2283 }, 300);
2467 2284 });
2468 -
2469 - // Click outside to close (drag-safe)
2470 - mxchatDragSafeOverlayClose(noticeContainer, function() {
2471 - closeButton.click();
2285 +
2286 + // Click outside to close
2287 + noticeContainer.addEventListener('click', function(e) {
2288 + if (e.target === noticeContainer) {
2289 + closeButton.click();
2290 + }
2472 2291 });
2473 2292
2474 2293 // Show with animation
2475 2294 setTimeout(() => {
@@ -2638,40 +2457,43 @@
2638 2457 modal.classList.add('active');
2639 2458 });
2640 2459
2641 2460 // Set up close handlers
2642 - let detachEsc = null;
2643 2461 const closeModal = () => {
2644 2462 //console.log('Closing modal');
2645 - if (detachEsc) {
2646 - detachEsc();
2647 - detachEsc = null;
2648 - }
2649 2463 modal.classList.remove('active');
2650 2464 setTimeout(() => {
2651 2465 modal.style.display = 'none';
2652 2466 }, 300); // Match the CSS transition time
2653 2467 };
2654 -
2655 - // This modal holds edits: close only on explicit controls, confirming when dirty.
2656 - const guardedClose = mxchatGuardedClose(modal, closeModal);
2657 -
2468 +
2658 2469 // Close button handler
2659 2470 const closeBtn = modal.querySelector('.mxchat-modal-close');
2660 2471 if (closeBtn) {
2661 - closeBtn.onclick = guardedClose;
2472 + closeBtn.onclick = closeModal;
2662 2473 }
2663 -
2474 +
2664 2475 // Cancel button handler
2665 2476 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
2666 2477 if (cancelBtns) {
2667 2478 cancelBtns.forEach(btn => {
2668 - btn.onclick = guardedClose;
2479 + btn.onclick = closeModal;
2669 2480 });
2670 2481 }
2671 -
2482 +
2483 + // Click outside modal to close
2484 + modal.onclick = (e) => {
2485 + if (e.target === modal) {
2486 + closeModal();
2487 + }
2488 + };
2489 +
2672 2490 // Escape key to close modal
2673 - detachEsc = mxchatBindEscClose(modal, guardedClose);
2491 + document.addEventListener('keydown', function(e) {
2492 + if (e.key === 'Escape' && modal.classList.contains('active')) {
2493 + closeModal();
2494 + }
2495 + }, { once: true });
2674 2496
2675 2497 // Focus appropriate field based on current step
2676 2498 if (isEdit || actionStep2.classList.contains('active')) {
2677 2499 if (labelField) labelField.focus();
@@ -2777,12 +2599,15 @@
2777 2599 closeModal(e);
2778 2600 });
2779 2601 }
2780 2602
2781 - // Close on backdrop click ONLY (not on hover), and only when the whole gesture
2782 - // happened on the backdrop — selecting the sample text and releasing past the
2783 - // dialog edge otherwise dispatches the click on the overlay and closes it.
2784 - mxchatDragSafeOverlayClose(modal, closeModal);
2603 + // Close on backdrop click ONLY (not on hover)
2604 + modal.addEventListener('click', function(e) {
2605 + // Only close if clicking directly on the overlay, not on child elements
2606 + if (e.target === modal) {
2607 + closeModal(e);
2608 + }
2609 + });
2785 2610
2786 2611 // Prevent modal content clicks from closing the modal
2787 2612 if (modalContent) {
2788 2613 modalContent.addEventListener('click', function(e) {
@@ -3154,20 +2979,18 @@
3154 2979 var $unavailableMessage = $('#web-search-unavailable-message');
3155 2980
3156 2981 if (!$wrapper.length) return; // Element doesn't exist
3157 2982
3158 - // Get the list of web-search-capable models from data attributes (OpenAI + Gemini)
2983 + // Get the list of supported OpenAI models from data attribute
3159 2984 var openaiModelsAttr = $wrapper.data('openai-models');
3160 - var geminiModelsAttr = $wrapper.data('gemini-models');
3161 2985 var unsupportedModelsAttr = $wrapper.data('unsupported-models');
3162 2986
3163 - var openaiModels = openaiModelsAttr ? String(openaiModelsAttr).split(',') : [];
3164 - var geminiModels = geminiModelsAttr ? String(geminiModelsAttr).split(',') : [];
3165 - var unsupportedModels = unsupportedModelsAttr ? String(unsupportedModelsAttr).split(',') : [];
2987 + var openaiModels = openaiModelsAttr ? openaiModelsAttr.split(',') : [];
2988 + var unsupportedModels = unsupportedModelsAttr ? unsupportedModelsAttr.split(',') : [];
3166 2989
3167 - // Check if selected model is an OpenAI or Gemini model that supports web search
3168 - var isCapable = openaiModels.includes(selectedModel) || geminiModels.includes(selectedModel);
3169 - var isSupported = isCapable && !unsupportedModels.includes(selectedModel);
2990 + // Check if selected model is an OpenAI model that supports web search
2991 + var isOpenAI = openaiModels.includes(selectedModel);
2992 + var isSupported = isOpenAI && !unsupportedModels.includes(selectedModel);
3170 2993
3171 2994 if (isSupported) {
3172 2995 $wrapper.show();
3173 2996 $unavailableMessage.hide();
@@ -3708,49 +3531,6 @@
3708 3531
3709 3532 // Also refresh when switching to optimization tab
3710 3533 $(document).on('click', '[data-section="optimization"]', function() {
3711 3534 setTimeout(refreshDebugLog, 100);
3712 - });
3713 -});
3714 -
3715 -// Global rate-limit usage: "Reset counter" button (plan-mxchat-20260603-e9b3f9)
3716 -jQuery(document).ready(function($) {
3717 - var $usage = $('#mxch-global-usage');
3718 - if (!$usage.length) {
3719 - return;
3720 - }
3721 -
3722 - $('#mxch-global-usage-reset').on('click', function() {
3723 - var $btn = $(this);
3724 - if (!window.confirm('Reset the global usage counter to zero now? This clears how many messages have been used in the current window.')) {
3725 - return;
3726 - }
3727 -
3728 - var original = $btn.text();
3729 - $btn.prop('disabled', true).text('Resetting…');
3730 -
3731 - $.ajax({
3732 - url: mxchatAdmin.ajax_url,
3733 - type: 'POST',
3734 - data: {
3735 - action: 'mxchat_reset_global_rate_limit',
3736 - bot_id: $usage.data('bot-id') || 'default',
3737 - _ajax_nonce: $usage.data('reset-nonce')
3738 - },
3739 - success: function(response) {
3740 - $btn.prop('disabled', false).text(original);
3741 - if (response && response.success) {
3742 - $('#mxch-global-usage-fill').css('width', (response.data.pct || 0) + '%');
3743 - if (response.data.text) {
3744 - $('#mxch-global-usage-text').text(response.data.text);
3745 - }
3746 - } else {
3747 - window.alert((response && response.data && response.data.message) || 'Reset failed. Please try again.');
3748 - }
3749 - },
3750 - error: function() {
3751 - $btn.prop('disabled', false).text(original);
3752 - window.alert('Reset failed. Please try again.');
3753 - }
3754 - });
3755 3535 });
3756 3536 });