PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.19
MxChat – AI Chatbot & Content Generation for WordPress v3.2.19
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
mxchat-basic / js / mxchat-admin.js

mxchat-admin.js in MxChat – AI Chatbot & Content Generation for WordPress 3.2.19, at js/mxchat-admin.js

4,014 lines 184.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Simple debounce function implementation
2 function debounce(func, wait) {
3 let timeout;
4 return function executedFunction(...args) {
5 const later = () => {
6 clearTimeout(timeout);
7 func(...args);
8 };
9 clearTimeout(timeout);
10 timeout = setTimeout(later, wait);
11 };
12 }
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 // Helper function to open edit modal for intents/actions
72 function mxchatOpenEditModal(intentId, phrases) {
73 const modal = document.getElementById('mxchat-edit-modal');
74 if (!modal) return;
75
76 // Get form fields
77 const intentIdField = document.getElementById('edit_intent_id');
78 const phrasesField = document.getElementById('edit_phrases');
79
80 // Set values
81 intentIdField.value = intentId;
82 phrasesField.value = phrases;
83
84 // Show modal with animation
85 modal.style.display = 'flex';
86 requestAnimationFrame(() => {
87 modal.classList.add('active');
88 });
89
90 // Set up close handlers
91 let detachEsc = null;
92 const closeModal = () => {
93 if (detachEsc) {
94 detachEsc();
95 detachEsc = null;
96 }
97 modal.classList.remove('active');
98 setTimeout(() => {
99 modal.style.display = 'none';
100 }, 300); // Match the CSS transition time
101 };
102
103 // This modal holds edits: close only on explicit controls, confirming when dirty.
104 const guardedClose = mxchatGuardedClose(modal, closeModal);
105
106 // Close button handler
107 const closeBtn = modal.querySelector('.mxchat-modal-close');
108 if (closeBtn) {
109 closeBtn.onclick = guardedClose;
110 }
111
112 // Cancel button handler
113 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
114 if (cancelBtn) {
115 cancelBtn.onclick = guardedClose;
116 }
117
118 // Escape key to close modal
119 detachEsc = mxchatBindEscClose(modal, guardedClose);
120
121 // Focus the textarea
122 phrasesField.focus();
123 }
124 // Live Agent Notice Dismissal Function
125 function dismissLiveAgentNotice() {
126 if (typeof jQuery !== 'undefined' && typeof mxchatLiveAgent !== 'undefined') {
127 jQuery.post(mxchatLiveAgent.ajaxurl, {
128 action: 'dismiss_live_agent_notice',
129 nonce: mxchatLiveAgent.nonce
130 }, function(response) {
131 if (response.success) {
132 jQuery('#mxchat-disabled-notice').fadeOut(300);
133 }
134 }).fail(function() {
135 // Fallback: just hide the notice if AJAX fails
136 jQuery('#mxchat-disabled-notice').fadeOut(300);
137 });
138 } else {
139 // Fallback for cases where jQuery or localized data isn't available
140 var notice = document.getElementById('mxchat-disabled-notice');
141 if (notice) {
142 notice.style.display = 'none';
143 }
144 }
145 }
146
147 // Theme Migration Notice Dismissal Function
148 function dismissThemeMigrationNotice() {
149 if (typeof jQuery !== 'undefined' && typeof mxchatThemeMigration !== 'undefined') {
150 jQuery.post(mxchatThemeMigration.ajaxurl, {
151 action: 'dismiss_theme_migration_notice',
152 nonce: mxchatThemeMigration.nonce
153 }, function(response) {
154 if (response.success) {
155 jQuery('#mxchat-theme-migration-notice').fadeOut(300);
156 }
157 }).fail(function() {
158 // Fallback: just hide the notice if AJAX fails
159 jQuery('#mxchat-theme-migration-notice').fadeOut(300);
160 });
161 } else {
162 // Fallback for cases where jQuery or localized data isn't available
163 var notice = document.getElementById('mxchat-theme-migration-notice');
164 if (notice) {
165 notice.style.display = 'none';
166 }
167 }
168 }
169
170 // Updated mxchatOpenActionModal function to integrate with the new selector
171 function mxchatOpenActionModal(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
172 const modal = document.getElementById('mxchat-action-modal');
173 if (!modal) return;
174
175 // Get form fields
176 const actionIdField = document.getElementById('edit_action_id');
177 const labelField = document.getElementById('intent_label');
178 const phrasesField = document.getElementById('action_phrases');
179 const formActionType = document.getElementById('form_action_type');
180 const callbackGroup = document.getElementById('callback_selection_group');
181 const callbackSelect = document.getElementById('callback_function');
182 const saveButton = document.getElementById('mxchat-save-action-btn');
183 const nonceContainer = document.getElementById('action-nonce-container');
184 const thresholdSlider = document.getElementById('similarity_threshold');
185 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
186
187 // Set up modal for edit or create
188 if (isEdit) {
189 saveButton.textContent = 'Update Action';
190 formActionType.value = 'mxchat_edit_intent';
191 actionIdField.value = actionId;
192 labelField.value = label;
193 phrasesField.value = phrases;
194 callbackGroup.style.display = 'none'; // Hide callback selection when editing
195 thresholdSlider.value = threshold; // Set the current threshold value
196 thresholdDisplay.textContent = threshold + '%'; // Update display
197
198 // Remove the required attribute when editing
199 callbackSelect.removeAttribute('required');
200
201 // Update the nonce field for editing
202 nonceContainer.innerHTML = ''; // Clear existing nonce
203 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
204 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
205 }
206 } else {
207 saveButton.textContent = 'Save Action';
208 formActionType.value = 'mxchat_add_intent';
209 actionIdField.value = '';
210 labelField.value = '';
211 phrasesField.value = '';
212 callbackGroup.style.display = 'block'; // Show callback selection when creating
213 thresholdSlider.value = 85; // Default value for new actions
214 thresholdDisplay.textContent = '85%'; // Default display
215
216 // Ensure the required attribute is present when adding
217 callbackSelect.setAttribute('required', 'required');
218
219 // Update the nonce field for adding
220 nonceContainer.innerHTML = ''; // Clear existing nonce
221 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
222 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
223 }
224 }
225
226 // Show modal with animation
227 modal.style.display = 'flex';
228 requestAnimationFrame(() => {
229 modal.classList.add('active');
230 });
231
232 // Set up close handlers
233 let detachEsc = null;
234 const closeModal = () => {
235 if (detachEsc) {
236 detachEsc();
237 detachEsc = null;
238 }
239 modal.classList.remove('active');
240 setTimeout(() => {
241 modal.style.display = 'none';
242 }, 300); // Match the CSS transition time
243 };
244
245 // This modal holds edits: close only on explicit controls, confirming when dirty.
246 const guardedClose = mxchatGuardedClose(modal, closeModal);
247
248 // Close button handler
249 const closeBtn = modal.querySelector('.mxchat-modal-close');
250 if (closeBtn) {
251 closeBtn.onclick = guardedClose;
252 }
253
254 // Cancel button handler
255 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
256 if (cancelBtn) {
257 cancelBtn.onclick = guardedClose;
258 }
259
260 // Escape key to close modal
261 detachEsc = mxchatBindEscClose(modal, guardedClose);
262
263 // Focus the first field
264 labelField.focus();
265
266 // Dispatch an event for the action type selector to catch
267 const event = new CustomEvent('mxchatModalOpened', {
268 detail: {
269 isEdit: isEdit,
270 callbackFunction: callbackFunction || (isEdit ? callbackSelect.value : '')
271 }
272 });
273 document.dispatchEvent(event);
274
275 return closeModal; // Return close function for external use
276 }
277
278 // Initialize event listeners
279 document.addEventListener('DOMContentLoaded', () => {
280 // Set up edit button handlers for intents
281 document.querySelectorAll('.mxchat-edit-button').forEach(button => {
282 button.onclick = () => {
283 const intentId = button.dataset.intentId;
284 const phrases = button.dataset.phrases;
285 mxchatOpenEditModal(intentId, phrases);
286 };
287 });
288
289 document.querySelectorAll('.mxchat-action-card .mxchat-edit-button').forEach(button => {
290 button.onclick = () => {
291 const actionId = button.dataset.actionId;
292 const phrases = button.dataset.phrases;
293 const label = button.dataset.label;
294 const threshold = button.dataset.threshold || 85;
295 const callbackFunction = button.dataset.callbackFunction;
296 const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
297
298 mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
299 };
300 });
301
302 // Set up add new action buttons (new functionality)
303 const addActionBtn = document.getElementById('mxchat-add-action-btn');
304 if (addActionBtn) {
305 addActionBtn.onclick = () => mxchatOpenActionModal();
306 }
307
308 const createFirstAction = document.getElementById('mxchat-create-first-action');
309 if (createFirstAction) {
310 createFirstAction.onclick = () => mxchatOpenActionModal();
311 }
312
313 // Setup category-specific new action buttons (new functionality)
314 document.querySelectorAll('.mxchat-new-action-button').forEach(button => {
315 button.onclick = () => {
316 const category = button.closest('.mxchat-new-action-card').dataset.category;
317 const closeModal = mxchatOpenActionModal();
318
319 // Pre-select the appropriate callback based on category
320 if (category) {
321 const callbackSelect = document.getElementById('callback_function');
322 if (callbackSelect) {
323 setTimeout(() => {
324 // Map categories to default callbacks
325 const categoryToCallback = {
326 'data_collection': 'mxchat_handle_form_collection',
327 'integrations': 'mxchat_handle_slack_message',
328 'custom_actions': 'mxchat_handle_custom_action',
329 'recommendations': 'mxchat_handle_product_recommendations'
330 // Add more mappings as needed
331 };
332
333 if (categoryToCallback[category]) {
334 callbackSelect.value = categoryToCallback[category];
335 }
336 }, 100);
337 }
338 }
339 };
340 });
341
342 // Handle action toggle switches (new functionality)
343 document.querySelectorAll('.mxchat-action-toggle').forEach(toggle => {
344 toggle.onchange = function() {
345 const actionId = this.dataset.actionId;
346 const isEnabled = this.checked;
347
348 // Show loading indicator
349 const loadingEl = document.getElementById('mxchat-action-loading');
350 if (loadingEl) loadingEl.style.display = 'flex';
351
352 // Send AJAX request to update status
353 fetch(ajaxurl, {
354 method: 'POST',
355 headers: {
356 'Content-Type': 'application/x-www-form-urlencoded',
357 },
358 body: new URLSearchParams({
359 action: 'mxchat_toggle_action',
360 intent_id: actionId,
361 enabled: isEnabled ? 1 : 0,
362 nonce: mxchatAdmin.toggle_action_nonce // Use the correct nonce
363 })
364 })
365 .then(response => response.json())
366 .then(data => {
367 if (!data.success) {
368 alert('Failed to update action status: ' + (data.data?.message || 'Unknown error'));
369 this.checked = !isEnabled; // Revert the toggle
370 }
371 })
372 .catch(error => {
373 //console.error('Error:', error);
374 alert('Server error. Please try again.');
375 this.checked = !isEnabled; // Revert the toggle
376 })
377 .finally(() => {
378 if (loadingEl) loadingEl.style.display = 'none';
379 });
380 };
381 });
382
383 // Handle threshold sliders in action cards (new functionality)
384 document.querySelectorAll('.mxchat-threshold-slider').forEach(slider => {
385 slider.oninput = function() {
386 const actionId = this.id.replace('intent_threshold_', '');
387 document.getElementById('threshold_output_' + actionId).textContent = this.value + '%';
388 };
389 });
390
391 // Handle threshold save buttons in action cards (new functionality)
392 document.querySelectorAll('.mxchat-threshold-save').forEach(button => {
393 button.onclick = function(e) {
394 e.preventDefault();
395 const form = this.closest('form');
396 const intentId = form.querySelector('input[name="intent_id"]').value;
397 const threshold = form.querySelector('input[name="intent_threshold"]').value;
398 const nonce = form.querySelector('input[name="_wpnonce"]').value;
399
400 // Show loading indicator
401 const loadingEl = document.getElementById('mxchat-action-loading');
402 if (loadingEl) loadingEl.style.display = 'flex';
403
404 // Send AJAX request
405 fetch(ajaxurl, {
406 method: 'POST',
407 headers: {
408 'Content-Type': 'application/x-www-form-urlencoded',
409 },
410 body: new URLSearchParams({
411 action: 'mxchat_update_intent_threshold',
412 intent_id: intentId,
413 intent_threshold: threshold,
414 _wpnonce: nonce
415 })
416 })
417 .then(response => response.json())
418 .then(data => {
419 if (data.success) {
420 // Visual feedback of success
421 const card = this.closest('.mxchat-action-card');
422 card.style.background = 'rgba(120, 115, 245, 0.1)';
423 setTimeout(() => {
424 card.style.background = 'white';
425 }, 300);
426 } else {
427 alert('Failed to update threshold: ' + (data.data?.message || 'Unknown error'));
428 }
429 })
430 .catch(error => {
431 //console.error('Error:', error);
432 alert('Server error. Please try again.');
433 })
434 .finally(() => {
435 if (loadingEl) loadingEl.style.display = 'none';
436 });
437 };
438 });
439 });
440
441 jQuery(document).ready(function($) {
442 // Ensure we have a debounce function (use lodash if available, otherwise use our implementation)
443 const useDebounce = (window._ && window._.debounce) ? window._.debounce : debounce;
444
445 // --- AJAX Auto-Save ---
446 let $autosaveSections = $('.mxchat-autosave-section');
447
448 // *** ADD THIS: Extend auto-save sections to include Pinecone settings ***
449 const $pineconeAutosaveSection = $('#mxchat-kb-tab-pinecone');
450 if ($pineconeAutosaveSection.length) {
451 $autosaveSections = $autosaveSections.add($pineconeAutosaveSection);
452 //console.log('Added Pinecone section to auto-save monitoring');
453 }
454
455 // Track whether fields have been modified by user
456 const userModifiedFields = new Set();
457
458 if ($autosaveSections.length) {
459 // Track user interactions with input fields to determine if changes are user-initiated
460 $autosaveSections.find('input, textarea, select').on('focus keydown paste', function() {
461 const fieldName = $(this).attr('name');
462 if (fieldName) {
463 userModifiedFields.add(fieldName);
464 }
465 });
466
467 // Handle real-time range slider value updates
468 $autosaveSections.find('input[type="range"]').on('input', function() {
469 const value = $(this).val();
470 const $slider = $(this);
471 const sliderId = $slider.attr('id');
472 // Find the corresponding value display span (convention: id_value)
473 const $valueSpan = $('#' + sliderId + '_value');
474 if ($valueSpan.length) {
475 $valueSpan.text(value);
476 } else {
477 // Fallback for similarity_threshold which uses threshold_value
478 $('#threshold_value').text(value);
479 }
480 });
481
482 // Handle all input changes (including range slider)
483 // Store pending AJAX requests and debounce timers per field to prevent freezing
484 const pendingRequests = {};
485 const fieldDebounceTimers = {};
486
487 // Helper function to save rate limit fields with request tracking
488 function triggerFieldSave($field, name, pendingRequests) {
489 const value = $field.val();
490
491 // Remove any existing feedback containers for this field
492 $field.siblings('.feedback-container').remove();
493 $field.parent().find('.feedback-container').remove();
494
495 // Create feedback container
496 const feedbackContainer = $('<div class="feedback-container"></div>');
497 const spinner = $('<div class="saving-spinner"></div>');
498 const successIcon = $('<div class="success-icon">✔</div>');
499 $field.after(feedbackContainer);
500 feedbackContainer.append(spinner);
501
502 // Rate limits use the main settings action
503 const ajaxAction = 'mxchat_save_setting';
504 const nonce = mxchatAdmin.setting_nonce;
505
506 // Store the AJAX request so it can be aborted if needed
507 pendingRequests[name] = $.ajax({
508 url: mxchatAdmin.ajax_url,
509 type: 'POST',
510 data: {
511 action: ajaxAction,
512 name: name,
513 value: value,
514 _ajax_nonce: nonce
515 },
516 success: function(response) {
517 delete pendingRequests[name];
518 if (response.success) {
519 spinner.fadeOut(200, function() {
520 feedbackContainer.append(successIcon);
521 successIcon.fadeIn(200).delay(800).fadeOut(200, function() {
522 feedbackContainer.remove();
523 });
524 });
525 } else {
526 feedbackContainer.remove();
527 }
528 },
529 error: function(xhr, textStatus, error) {
530 delete pendingRequests[name];
531 // Don't show error for aborted requests
532 if (textStatus !== 'abort') {
533 feedbackContainer.remove();
534 }
535 }
536 });
537 }
538
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() {
546 const $field = $(this);
547 const name = $field.attr('name');
548
549 // Debounce rate limit fields to prevent UI freezing from rapid changes
550 if (name && name.indexOf('rate_limits') !== -1) {
551 // Clear any pending timer for this field
552 if (fieldDebounceTimers[name]) {
553 clearTimeout(fieldDebounceTimers[name]);
554 }
555 // Abort any pending AJAX request for this field
556 if (pendingRequests[name]) {
557 pendingRequests[name].abort();
558 // Remove any existing feedback containers for this field
559 $field.siblings('.feedback-container').remove();
560 $field.parent().find('.feedback-container').remove();
561 }
562 // Debounce the save operation
563 const fieldRef = $field;
564 fieldDebounceTimers[name] = setTimeout(function() {
565 triggerFieldSave(fieldRef, name, pendingRequests);
566 }, 300);
567 return;
568 }
569
570 // Skip saving for API key fields that haven't been interacted with and are empty
571 const isApiKeyField = name && (
572 name === 'loops_api_key' ||
573 name === 'api_key' ||
574 name === 'xai_api_key' ||
575 name === 'claude_api_key' ||
576 name === 'voyage_api_key' ||
577 name === 'gemini_api_key' ||
578 name === 'deepseek_api_key' ||
579 name.indexOf('_api_key') !== -1
580 );
581
582 // Skip processing if:
583 // 1. It's an API key field
584 // 2. The user hasn't interacted with it
585 // 3. The field is empty
586 if (isApiKeyField && !userModifiedFields.has(name) && (!$field.val() || $field.val().trim() === '')) {
587 //console.log('Skipping auto-save for untouched API key field:', name);
588 return;
589 }
590
591 let value;
592
593 // Handle different input types
594 if ($field.attr('type') === 'checkbox') {
595 // *** UPDATED: Handle Pinecone checkboxes differently ***
596 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
597 value = $field.is(':checked') ? '1' : '0';
598 } else {
599 value = $field.is(':checked') ? 'on' : 'off';
600 }
601 } else {
602 value = $field.val();
603 }
604
605 // Create feedback container
606 const feedbackContainer = $('<div class="feedback-container"></div>');
607 const spinner = $('<div class="saving-spinner"></div>');
608 const successIcon = $('<div class="success-icon">✔</div>');
609
610 // Position feedback container based on input type
611 if ($field.closest('.toggle-switch').length) {
612 // Try td first (old layout), then mxc-field-control (new card layout), then fallback to after toggle
613 var $container = $field.closest('td');
614 if (!$container.length) {
615 $container = $field.closest('.mxc-field-control');
616 }
617 if ($container.length) {
618 $container.append(feedbackContainer);
619 } else {
620 $field.closest('.toggle-switch').after(feedbackContainer);
621 }
622 } else if ($field.closest('.mxchat-toggle-switch').length) {
623 // Try mxchat-toggle-container first, then parent div, then fallback to after toggle
624 var $toggleContainer = $field.closest('.mxchat-toggle-container');
625 if ($toggleContainer.length) {
626 $toggleContainer.append(feedbackContainer);
627 } else {
628 $field.closest('.mxchat-toggle-switch').after(feedbackContainer);
629 }
630 } else if ($field.closest('.slider-container').length) {
631 $field.closest('.slider-container').after(feedbackContainer);
632 } else {
633 $field.after(feedbackContainer);
634 }
635 feedbackContainer.append(spinner);
636
637 // Determine which AJAX action and nonce to use:
638 var ajaxAction, nonce;
639 // *** UPDATED: Add Pinecone fields, chunking fields, ACF fields, and custom meta to prompts action ***
640 if (name.indexOf('mxchat_prompts_options') !== -1 ||
641 name === 'mxchat_auto_sync_posts' ||
642 name === 'mxchat_auto_sync_pages' ||
643 name.indexOf('mxchat_auto_sync_') === 0 ||
644 name.indexOf('mxchat_pinecone_addon_options') !== -1 ||
645 name.indexOf('mxchat_chunk') === 0 ||
646 name.indexOf('mxchat_acf_field_') === 0 ||
647 name === 'mxchat_custom_meta_whitelist') { // Chunking, ACF field settings, and custom meta
648 ajaxAction = 'mxchat_save_prompts_setting';
649 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
650 } else {
651 // Otherwise, use the existing AJAX action.
652 ajaxAction = 'mxchat_save_setting';
653 nonce = mxchatAdmin.setting_nonce;
654 }
655
656 // *** ADD THIS: Debug logging for Pinecone fields ***
657 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
658 //console.log('Saving Pinecone field:', name, '=', value);
659 }
660
661 // AJAX save request
662 $.ajax({
663 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
664 type: 'POST',
665 data: {
666 action: ajaxAction,
667 name: name,
668 value: value,
669 _ajax_nonce: nonce
670 },
671 success: function(response) {
672 if (response.success) {
673 spinner.fadeOut(200, function() {
674 feedbackContainer.append(successIcon);
675 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
676 feedbackContainer.remove();
677 });
678 });
679
680 // *** ADD THIS: Refresh API key status after saving an API key ***
681 const isApiKeyField = name && (
682 name === 'api_key' ||
683 name === 'xai_api_key' ||
684 name === 'claude_api_key' ||
685 name === 'voyage_api_key' ||
686 name === 'gemini_api_key' ||
687 name === 'deepseek_api_key' ||
688 name === 'openrouter_api_key' ||
689 name.indexOf('_api_key') !== -1
690 );
691
692 if (isApiKeyField && typeof window.mxchatRefreshAPIKeyStatus === 'function') {
693 window.mxchatRefreshAPIKeyStatus();
694 }
695
696 // *** ADD THIS: Update Pinecone checkbox state after successful save ***
697 if (name && name.indexOf('mxchat_pinecone_addon_options[mxchat_use_pinecone]') !== -1) {
698 //console.log('Pinecone toggle saved successfully, value:', value);
699
700 // The checkbox state is already updated by the user interaction
701 // But let's make sure the UI state matches the saved value
702 var $checkbox = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');
703 var settingsDiv = $('.mxchat-pinecone-settings');
704
705 // Double-check the UI state matches what was saved
706 if (value === '1' && !$checkbox.is(':checked')) {
707 $checkbox.prop('checked', true);
708 settingsDiv.slideDown(300);
709 } else if (value === '0' && $checkbox.is(':checked')) {
710 $checkbox.prop('checked', false);
711 settingsDiv.slideUp(300);
712 }
713
714 //console.log('Pinecone UI state synchronized');
715
716 // Check if Knowledge Import tab is currently active
717 if ($('.mxchat-kb-tab-button[data-tab="import"]').hasClass('active')) {
718 // Show a notice that we need to refresh
719 var $knowledgeCard = $('#mxchat-kb-tab-import .mxchat-card').eq(1);
720 if ($knowledgeCard.length > 0) {
721 // Add a refresh notice at the top of the knowledge base card
722 var refreshNotice = $('<div class="notice notice-warning" style="margin: 15px 0; padding: 10px 15px;">' +
723 '<p style="margin: 0;">' +
724 '<span class="dashicons dashicons-info" style="color: #f0ad4e; margin-right: 5px;"></span>' +
725 'Database settings have changed. ' +
726 '<a href="#" onclick="location.reload(); return false;" style="font-weight: bold;">Click here to refresh</a> to see the updated knowledge base.' +
727 '</p></div>');
728
729 $knowledgeCard.prepend(refreshNotice);
730 }
731 } else {
732 // If not on import tab, set a flag to refresh when they go there
733 sessionStorage.setItem('mxchat_pinecone_changed', 'true');
734 }
735 }
736
737 // *** ADD THIS: Debug logging for successful saves ***
738 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
739 //console.log('Pinecone field saved successfully:', name, '=', value);
740 }
741
742 // Check if the response contains a "no changes" message and log it
743 if (response.data && response.data.message === 'No changes detected') {
744 //console.log('No changes detected for field:', name);
745 }
746 } else {
747 // Only show alert for actual errors, not for "no changes"
748 let errorMessage = response.data?.message || 'Unknown error';
749
750 // Don't display an alert for "no changes" message
751 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
752 alert('Error saving: ' + errorMessage);
753 } else {
754 // Still provide visual feedback that no changes were needed
755 spinner.fadeOut(200, function() {
756 feedbackContainer.append(successIcon);
757 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
758 feedbackContainer.remove();
759 });
760 });
761 //console.log('No changes detected for field:', name);
762 return;
763 }
764
765 // Only revert checkbox state if it was an actual error
766 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
767 if ($field.attr('type') === 'checkbox') {
768 $field.prop('checked', !$field.is(':checked'));
769 }
770 }
771
772 // Always clean up the feedback container
773 feedbackContainer.remove();
774 }
775 },
776 error: function(xhr, textStatus, error) {
777 //console.error('AJAX Error:', textStatus, error);
778 alert('An error occurred while saving. Please try again.');
779
780 // Revert checkbox state on error
781 if ($field.attr('type') === 'checkbox') {
782 $field.prop('checked', !$field.is(':checked'));
783 }
784
785 feedbackContainer.remove();
786 }
787 });
788 });
789
790 // Initialize color pickers with debouncing
791 $autosaveSections.find('.my-color-field').each(function() {
792 const $colorField = $(this);
793
794 $(this).wpColorPicker({
795 change: useDebounce(function(event, ui) {
796 // Safety check - ensure we have a valid field and value
797 if (!$colorField || !$colorField.val()) {
798 //console.warn('Color picker not ready');
799 return;
800 }
801
802 const name = $colorField.attr('name');
803 const value = $colorField.val();
804
805 if (!name || !value) {
806 //console.warn('Missing required color picker values');
807 return;
808 }
809
810 // Create feedback container
811 const feedbackContainer = $('<div class="feedback-container"></div>');
812 const spinner = $('<div class="saving-spinner"></div>');
813 const successIcon = $('<div class="success-icon">✔</div>');
814
815 // Position feedback container
816 $colorField.closest('.wp-picker-container').after(feedbackContainer);
817 feedbackContainer.append(spinner);
818
819 // Determine which AJAX action and nonce to use:
820 var ajaxAction, nonce;
821 // Use the new AJAX action for submenu fields:
822 if (name.indexOf('mxchat_prompts_options') !== -1 ||
823 name === 'mxchat_auto_sync_posts' ||
824 name === 'mxchat_auto_sync_pages' ||
825 name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields
826 ajaxAction = 'mxchat_save_prompts_setting';
827 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
828 } else {
829 // Otherwise, use the existing AJAX action.
830 ajaxAction = 'mxchat_save_setting';
831 nonce = mxchatAdmin.setting_nonce;
832 }
833 // AJAX save request
834 $.ajax({
835 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
836 type: 'POST',
837 data: {
838 action: ajaxAction,
839 name: name,
840 value: value,
841 _ajax_nonce: nonce
842 },
843 success: function(response) {
844 if (response.success) {
845 spinner.fadeOut(200, function() {
846 feedbackContainer.append(successIcon);
847 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
848 feedbackContainer.remove();
849 });
850 });
851 } else {
852 alert('Error saving: ' + (response.data?.message || 'Unknown error'));
853 feedbackContainer.remove();
854 }
855 },
856 error: function() {
857 alert('An error occurred while saving.');
858 feedbackContainer.remove();
859 }
860 });
861 }, 500)
862 });
863 });
864
865 }
866
867 // ========================================
868 // POST TYPE VISIBILITY SETTINGS
869 // ========================================
870 (function() {
871 const $appendToBody = $('#append_to_body');
872 const $visibilityOptions = $('#post-type-visibility-options');
873 const $modeRadios = $('input[name="post_type_visibility_mode"]');
874 const $postTypeList = $('#post-type-list');
875 const $postTypeCheckboxes = $('input[name="post_type_visibility_list[]"]');
876
877 // Toggle visibility options based on auto-display toggle
878 $appendToBody.on('change', function() {
879 if ($(this).is(':checked')) {
880 $visibilityOptions.slideDown(200);
881 } else {
882 $visibilityOptions.slideUp(200);
883 }
884 });
885
886 // Toggle post type list based on mode selection
887 $modeRadios.on('change', function() {
888 const mode = $(this).val();
889 if (mode === 'all') {
890 $postTypeList.slideUp(200);
891 } else {
892 $postTypeList.slideDown(200);
893 }
894
895 // Save mode via AJAX
896 savePostTypeVisibility('post_type_visibility_mode', mode);
897 });
898
899 // Save post type list when checkboxes change
900 $postTypeCheckboxes.on('change', function() {
901 // Collect all checked post types
902 const selectedPostTypes = [];
903 $postTypeCheckboxes.filter(':checked').each(function() {
904 selectedPostTypes.push($(this).val());
905 });
906
907 // Save as JSON array
908 savePostTypeVisibility('post_type_visibility_list', JSON.stringify(selectedPostTypes));
909 });
910
911 // Helper function to save post type visibility settings
912 function savePostTypeVisibility(name, value) {
913 if (typeof mxchatAdmin === 'undefined') return;
914
915 // Find the container element for feedback
916 const $container = name === 'post_type_visibility_mode'
917 ? $('.mxchat-visibility-mode')
918 : $postTypeList;
919
920 // Remove any existing feedback
921 $container.find('.mxchat-save-feedback').remove();
922
923 // Create feedback element
924 const $feedback = $('<span class="mxchat-save-feedback" style="margin-left: 10px; font-size: 12px;"></span>');
925 $feedback.text('Saving...').css('color', '#666');
926
927 if (name === 'post_type_visibility_mode') {
928 $container.append($feedback);
929 } else {
930 $container.before($feedback);
931 }
932
933 $.ajax({
934 url: mxchatAdmin.ajax_url,
935 type: 'POST',
936 data: {
937 action: 'mxchat_save_setting',
938 name: name,
939 value: value,
940 _ajax_nonce: mxchatAdmin.setting_nonce
941 },
942 success: function(response) {
943 if (response.success) {
944 $feedback.text('✓ Saved').css('color', '#46b450');
945 setTimeout(function() {
946 $feedback.fadeOut(300, function() {
947 $(this).remove();
948 });
949 }, 1500);
950 } else {
951 $feedback.text('Error saving').css('color', '#dc3232');
952 }
953 },
954 error: function() {
955 $feedback.text('Error saving').css('color', '#dc3232');
956 }
957 });
958 }
959 })();
960
961 // Toggle visibility handlers
962 function toggleVisibility(selector) {
963 $(selector).on('click', function() {
964 var inputField = $(this).prev('input');
965 // Check if this is a CSS-masked field (type="text" with mxchat-api-key-field class)
966 if (inputField.hasClass('mxchat-api-key-field')) {
967 // Use CSS class toggle for CSS-masked fields
968 if (inputField.hasClass('mxchat-show-key')) {
969 inputField.removeClass('mxchat-show-key');
970 $(this).text('Show');
971 } else {
972 inputField.addClass('mxchat-show-key');
973 $(this).text('Hide');
974 }
975 } else {
976 // Legacy type toggle for password fields
977 if (inputField.attr('type') === 'password') {
978 inputField.attr('type', 'text');
979 $(this).text('Hide');
980 } else {
981 inputField.attr('type', 'password');
982 $(this).text('Show');
983 }
984 }
985 });
986 }
987
988 // Initialize all toggle visibility buttons
989 [
990 '#toggleApiKeyVisibility',
991 '#toggleWooCommerceSecretVisibility',
992 '#toggleVoyageAPIKeyVisibility',
993 '#toggleLoopsApiKeyVisibility',
994 '#toggleXaiApiKeyVisibility',
995 '#toggleClaudeApiKeyVisibility',
996 '#toggleBraveApiKeyVisibility',
997 '#toggleWebhookUrlVisibility',
998 '#toggleSecretKeyVisibility',
999 '#toggleBotTokenVisibility',
1000 '#toggleDeepSeekApiKeyVisibility',
1001 '#toggleGeminiApiKeyVisibility',
1002 '#toggleOpenRouterApiKeyVisibility'
1003 ].forEach(toggleVisibility);
1004
1005 function setupMxChatModelSelector() {
1006 const $modelSelect = $('#model');
1007 const $modelSelectorButton = $('<button>', {
1008 type: 'button',
1009 id: 'mxchat_model_selector_btn',
1010 class: 'button-primary mxchat-model-selector-btn',
1011 text: 'Select AI Model'
1012 });
1013
1014 // Replace the select dropdown with a button
1015 $modelSelect.hide().after($modelSelectorButton);
1016
1017 // Update button text to show currently selected model
1018 function updateButtonText() {
1019 const selectedModel = $modelSelect.val();
1020
1021 // Check if OpenRouter is selected
1022 if (selectedModel === 'openrouter') {
1023 const openrouterModelId = $('#openrouter_selected_model').val();
1024 const openrouterModelName = $('#openrouter_selected_model_name').val();
1025
1026 if (openrouterModelName && openrouterModelName.trim() !== '') {
1027 $modelSelectorButton.text('OpenRouter: ' + openrouterModelName);
1028 } else if (openrouterModelId && openrouterModelId.trim() !== '') {
1029 $modelSelectorButton.text('OpenRouter: ' + openrouterModelId);
1030 } else {
1031 $modelSelectorButton.text('OpenRouter - Select Model');
1032 }
1033 } else {
1034 const selectedModelText = $modelSelect.find('option:selected').text();
1035 $modelSelectorButton.text(selectedModelText);
1036 }
1037 }
1038
1039 // Initialize button text
1040 updateButtonText();
1041
1042 // Create and append modal HTML
1043 const modelSelectorModal = `
1044 <div id="mxchat_model_selector_modal" class="mxchat-model-selector-modal">
1045 <div class="mxchat-model-selector-modal-content">
1046 <div class="mxchat-model-selector-modal-header">
1047 <h3>Select AI Model</h3>
1048 <span class="mxchat-model-selector-modal-close">&times;</span>
1049 </div>
1050 <div class="mxchat-model-selector-modal-body">
1051 <div class="mxchat-model-selector-search-container">
1052 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1053 </div>
1054 <div class="mxchat-model-selector-categories">
1055 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1056 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1057 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1058 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1059 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1060 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1061 <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 </div>
1064 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1065 </div>
1066 <div class="mxchat-model-selector-modal-footer">
1067 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
1068 </div>
1069 </div>
1070 </div>
1071 `;
1072
1073 $('body').append(modelSelectorModal);
1074
1075 // MOVE THIS OUTSIDE - Make it a property of the window object so it's accessible globally
1076 window.populateModelsGrid = function(filter = '', category = 'all') {
1077 const $grid = $('#mxchat_models_grid');
1078 $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 : {
1086 openrouter: [
1087 { value: 'openrouter', label: 'OpenRouter', description: 'Access 100+ models from multiple providers (add API key to browse)' }
1088 ],
1089 gemini: [
1090 { value: 'gemini-3.5-flash', label: 'Gemini 3.5 Flash', description: 'Stable — newest Flash generation, recommended default' },
1091 ],
1092 openai: [
1093 { value: 'gpt-5.6-sol', label: 'GPT-5.6 Sol', description: 'Recommended — newest OpenAI flagship for reasoning, coding and chat' },
1094 ],
1095 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' },
1100 ],
1101 xai: [
1102 { value: 'grok-4.6', label: 'Grok 4.6', description: 'Newest xAI flagship — 500K context, accepts image input' },
1103 ],
1104 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' },
1107 ],
1108 custom: [
1109 { value: 'custom-provider', label: 'Custom Provider', description: 'OpenAI-compatible local LLM — configure in API Keys tab' },
1110 ],
1111 };
1112
1113 let allModels = [];
1114 Object.keys(models).forEach(key => {
1115 if (category === 'all' || category === key) {
1116 allModels = allModels.concat(models[key]);
1117 }
1118 });
1119
1120 // Filter by search term if present
1121 if (filter) {
1122 const lowerFilter = filter.toLowerCase();
1123 allModels = allModels.filter(model =>
1124 model.label.toLowerCase().includes(lowerFilter) ||
1125 model.description.toLowerCase().includes(lowerFilter)
1126 );
1127 }
1128
1129 // Create model cards
1130 allModels.forEach(model => {
1131 const isSelected = $modelSelect.val() === model.value;
1132 const $modelCard = $(`
1133 <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
1134 <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
1135 <div class="mxchat-model-selector-info">
1136 <h4 class="mxchat-model-selector-title">${model.label}</h4>
1137 <p class="mxchat-model-selector-description">${model.description}</p>
1138 </div>
1139 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1140 </div>
1141 `);
1142 $grid.append($modelCard);
1143 });
1144 };
1145
1146 // Helper function to get icon for each model
1147 function getModelIcon(modelValue) {
1148 if (modelValue === 'openrouter') return '<span class="dashicons dashicons-networking" style="font-size: 24px; color: #6750A4;"></span>';
1149 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 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 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 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 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 return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
1156 }
1157
1158 // Event handlers
1159 $modelSelectorButton.on('click', function() {
1160 $('#mxchat_model_selector_modal').show();
1161 window.populateModelsGrid('', 'all');
1162 });
1163
1164 // Deep link from the model-liveness admin notice (b65e8d): land on Settings
1165 // with the picker already open, so "pick a current model" is one click.
1166 try {
1167 if (new URLSearchParams(window.location.search).get('mxchat_open_model_picker') === '1') {
1168 $modelSelectorButton.trigger('click');
1169 }
1170 } catch (e) {}
1171
1172 $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
1173 $('#mxchat_model_selector_modal').hide();
1174 });
1175
1176 $('.mxchat-model-category-btn').on('click', function() {
1177 $('.mxchat-model-category-btn').removeClass('active');
1178 $(this).addClass('active');
1179 const category = $(this).data('category');
1180 const searchTerm = $('#mxchat_model_search_input').val();
1181 window.populateModelsGrid(searchTerm, category);
1182 });
1183
1184 $('#mxchat_model_search_input').on('input', function() {
1185 const searchTerm = $(this).val();
1186 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1187 window.populateModelsGrid(searchTerm, activeCategory);
1188 });
1189
1190 $(document).on('click', '.mxchat-model-selector-card', function() {
1191 const modelValue = $(this).data('value');
1192 const $modelSelect = $('#model');
1193 const $clickedCard = $(this);
1194
1195 // Check if OpenRouter was selected
1196 if (modelValue === 'openrouter') {
1197 // Load OpenRouter models instead of closing
1198 loadOpenRouterModels();
1199 } else {
1200 // Remove selection from all other cards
1201 $('.mxchat-model-selector-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1202
1203 // Add selection to clicked card immediately for instant feedback
1204 $clickedCard.addClass('mxchat-model-selected');
1205 if ($clickedCard.find('.mxchat-model-selector-checkmark').length === 0) {
1206 $clickedCard.append('<div class="mxchat-model-selector-checkmark">✓</div>');
1207 }
1208
1209 // Brief delay to show the selection, then start saving
1210 setTimeout(function() {
1211 // Normal model selection
1212 $modelSelect.val(modelValue).trigger('change');
1213
1214 // Show loading state on the card
1215 $clickedCard.css('pointer-events', 'none');
1216 const originalContent = $clickedCard.find('.mxchat-model-selector-title').html();
1217 $clickedCard.find('.mxchat-model-selector-title').html(
1218 '<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...'
1219 );
1220
1221 // Manually save the model via AJAX
1222 jQuery.ajax({
1223 url: mxchatAdmin.ajax_url,
1224 type: 'POST',
1225 data: {
1226 action: 'mxchat_save_setting',
1227 name: 'model',
1228 value: modelValue,
1229 _ajax_nonce: mxchatAdmin.setting_nonce
1230 },
1231 success: function(response) {
1232 if (response.success) {
1233 // Show success state
1234 $clickedCard.find('.mxchat-model-selector-title').html(
1235 '<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!'
1236 );
1237
1238 // Update button text after successful save
1239 const selectedModelText = $modelSelect.find('option:selected').text();
1240 $('#mxchat_model_selector_btn').text(selectedModelText);
1241
1242 // Close modal after a short delay to show the success message
1243 setTimeout(function() {
1244 $('#mxchat_model_selector_modal').hide();
1245 // Restore original content and remove selection for next time
1246 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1247 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1248 $clickedCard.css('pointer-events', 'auto');
1249 }, 600);
1250 } else {
1251 // Show error state
1252 $clickedCard.find('.mxchat-model-selector-title').html(
1253 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1254 );
1255
1256 setTimeout(function() {
1257 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1258 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1259 $clickedCard.css('pointer-events', 'auto');
1260 }, 1500);
1261 }
1262 },
1263 error: function() {
1264 // Show error state
1265 $clickedCard.find('.mxchat-model-selector-title').html(
1266 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1267 );
1268
1269 setTimeout(function() {
1270 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1271 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1272 $clickedCard.css('pointer-events', 'auto');
1273 }, 1500);
1274 }
1275 });
1276 }, 300); // 300ms delay to show the selection before starting to save
1277 }
1278 });
1279
1280 // Close modal when clicking outside
1281 $(window).on('click', function(event) {
1282 if ($(event.target).is('#mxchat_model_selector_modal')) {
1283 $('#mxchat_model_selector_modal').hide();
1284 }
1285 });
1286 }
1287
1288 function loadOpenRouterModels() {
1289 const apiKey = $('#openrouter_api_key').val(); // This line already re-checks the field
1290 const $modal = $('#mxchat_model_selector_modal');
1291 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1292
1293 if (!apiKey || apiKey.trim() === '') {
1294 // Show error message in modal
1295 $modalBody.html(`
1296 <div style="text-align: center; padding: 40px;">
1297 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1298 <h3>OpenRouter API Key Required</h3>
1299 <p>Please enter your OpenRouter API key in the settings before selecting a model. If you're seeing this message and recently entered API key, try refreshing.</p>
1300 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1301 </div>
1302 `);
1303
1304 $('#mxchat_back_to_models').on('click', function(e) {
1305 e.preventDefault();
1306 // CHANGE THIS: Instead of reloading, restore the original modal content
1307 $modalBody.html(`
1308 <div class="mxchat-model-selector-search-container">
1309 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1310 </div>
1311 <div class="mxchat-model-selector-categories">
1312 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1313 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1314 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1315 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1316 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1317 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1318 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1319 </div>
1320 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1321 `);
1322
1323 // Re-populate the grid
1324 populateModelsGrid('', 'all');
1325
1326 // Re-bind event handlers
1327 rebindModalEventHandlers();
1328 });
1329 return;
1330 }
1331
1332 // Show loading state
1333 $modalBody.html(`
1334 <div style="text-align: center; padding: 60px 20px;">
1335 <div class="spinner is-active" style="float: none; margin: 0 auto 20px;"></div>
1336 <h3>Loading OpenRouter Models...</h3>
1337 <p>Fetching available models from OpenRouter</p>
1338 </div>
1339 `);
1340
1341 // Fetch models from OpenRouter
1342 jQuery.ajax({
1343 url: mxchatAdmin.ajax_url,
1344 type: 'POST',
1345 data: {
1346 action: 'mxchat_fetch_openrouter_models',
1347 api_key: apiKey,
1348 nonce: mxchatAdmin.fetch_openrouter_models_nonce
1349 },
1350 success: function(response) {
1351 if (response.success && response.data.models) {
1352 displayOpenRouterModels(response.data.models);
1353 } else {
1354 $modalBody.html(`
1355 <div style="text-align: center; padding: 40px;">
1356 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1357 <h3>Error Loading Models</h3>
1358 <p>${response.data.message || 'Failed to load models from OpenRouter'}</p>
1359 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1360 </div>
1361 `);
1362
1363 $('#mxchat_back_to_models').on('click', function(e) {
1364 e.preventDefault();
1365 // CHANGE THIS: Restore original content instead of reloading
1366 restoreOriginalModalContent();
1367 });
1368 }
1369 },
1370 error: function() {
1371 $modalBody.html(`
1372 <div style="text-align: center; padding: 40px;">
1373 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1374 <h3>Connection Error</h3>
1375 <p>Failed to connect to OpenRouter. Please check your API key and try again.</p>
1376 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1377 </div>
1378 `);
1379
1380 $('#mxchat_back_to_models').on('click', function(e) {
1381 e.preventDefault();
1382 // CHANGE THIS: Restore original content instead of reloading
1383 restoreOriginalModalContent();
1384 });
1385 }
1386 });
1387 }
1388 function restoreOriginalModalContent() {
1389 const $modalBody = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-body');
1390
1391 $modalBody.html(`
1392 <div class="mxchat-model-selector-search-container">
1393 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1394 </div>
1395 <div class="mxchat-model-selector-categories">
1396 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1397 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1398 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1399 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1400 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1401 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1402 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1403 </div>
1404 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1405 `);
1406
1407 // Re-populate the grid
1408 window.populateModelsGrid('', 'all');
1409
1410 // Re-bind event handlers
1411 rebindModalEventHandlers();
1412 }
1413 function rebindModalEventHandlers() {
1414 const $modal = $('#mxchat_model_selector_modal');
1415
1416 // Re-bind category button clicks
1417 $('.mxchat-model-category-btn').off('click').on('click', function() {
1418 $('.mxchat-model-category-btn').removeClass('active');
1419 $(this).addClass('active');
1420 const category = $(this).data('category');
1421 const searchTerm = $('#mxchat_model_search_input').val();
1422 window.populateModelsGrid(searchTerm, category);
1423 });
1424
1425 // Re-bind search input
1426 $('#mxchat_model_search_input').off('input').on('input', function() {
1427 const searchTerm = $(this).val();
1428 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1429 populateModelsGrid(searchTerm, activeCategory);
1430 });
1431 }
1432
1433 function restoreDefaultModalFooter() {
1434 const $modalFooter = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-footer');
1435
1436 // Restore default footer buttons
1437 $modalFooter.html(`
1438 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
1439 `);
1440
1441 // Re-bind cancel button
1442 $('#mxchat_cancel_model_selection').on('click', function() {
1443 $('#mxchat_model_selector_modal').hide();
1444 });
1445 }
1446
1447 function displayOpenRouterModels(models) {
1448 const $modal = $('#mxchat_model_selector_modal');
1449 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1450 const $modalFooter = $modal.find('.mxchat-model-selector-modal-footer');
1451 const currentSelected = $('#openrouter_selected_model').val();
1452
1453 // Variable to store the currently selected model (in the UI, not yet saved)
1454 let pendingSelection = {
1455 modelId: currentSelected || null,
1456 modelName: $('#openrouter_selected_model_name').val() || null
1457 };
1458
1459 // Build new modal content with search and models
1460 const newContent = `
1461 <div class="mxchat-model-selector-search-container">
1462 <input type="text" id="mxchat_openrouter_search" class="mxchat-model-search-input" placeholder="Search OpenRouter models...">
1463 <p style="margin: 10px 0; color: #666; font-size: 13px;">
1464 <strong>${models.length} models available</strong> ·
1465 <a href="#" id="mxchat_back_to_provider_select" style="color: #2271b1;">← Back to providers</a>
1466 </p>
1467 </div>
1468 <div class="mxchat-model-selector-grid" id="mxchat_openrouter_models_grid"></div>
1469 `;
1470
1471 // Update footer with Save button for OpenRouter
1472 const footerContent = `
1473 <button id="mxchat_back_to_models_footer" class="button mxchat-model-cancel-btn">Back to Providers</button>
1474 <button id="mxchat_save_openrouter_model" class="button button-primary" disabled>
1475 <span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model
1476 </button>
1477 `;
1478
1479 $modalBody.html(newContent);
1480 $modalFooter.html(footerContent);
1481
1482 // Function to render models
1483 function renderOpenRouterModels(filterText = '') {
1484 const $grid = $('#mxchat_openrouter_models_grid');
1485 $grid.empty();
1486
1487 let filteredModels = models;
1488 if (filterText) {
1489 const lowerFilter = filterText.toLowerCase();
1490 filteredModels = models.filter(m =>
1491 m.id.toLowerCase().includes(lowerFilter) ||
1492 m.name.toLowerCase().includes(lowerFilter) ||
1493 (m.description && m.description.toLowerCase().includes(lowerFilter))
1494 );
1495 }
1496
1497 filteredModels.forEach(model => {
1498 const isSelected = pendingSelection.modelId === model.id;
1499 const contextLength = model.context_length ? `${(model.context_length / 1000).toFixed(0)}K` : '';
1500 const promptPrice = model.pricing.prompt ? `$${(model.pricing.prompt * 1000000).toFixed(2)}/1M` : '';
1501
1502 const $card = jQuery(`
1503 <div class="mxchat-openrouter-card ${isSelected ? 'mxchat-model-selected' : ''}" data-model-id="${model.id}" data-model-name="${model.name}">
1504 <div class="mxchat-model-selector-icon">
1505 ${getOpenRouterIcon(model.id)}
1506 </div>
1507 <div class="mxchat-model-selector-info">
1508 <h4 class="mxchat-model-selector-title">${model.name}</h4>
1509 <div style="font-size: 12px; color: #666; margin-top: 5px;">
1510 ${contextLength ? '<span style="margin-right: 12px;">📄 ' + contextLength + '</span>' : ''}
1511 ${promptPrice ? '<span>💰 ' + promptPrice + '</span>' : ''}
1512 </div>
1513 </div>
1514 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1515 </div>
1516 `);
1517
1518 $grid.append($card);
1519 });
1520 }
1521
1522 // Helper to get icon
1523 function getOpenRouterIcon(modelId) {
1524 if (modelId.includes('gpt') || modelId.includes('openai')) {
1525 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>';
1526 } else if (modelId.includes('claude') || modelId.includes('anthropic')) {
1527 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>';
1528 } else if (modelId.includes('gemini') || modelId.includes('google')) {
1529 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><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>';
1530 }
1531 return '<span class="dashicons dashicons-cloud" style="font-size: 24px; color: #6750A4;"></span>';
1532 }
1533
1534 // Initial render
1535 renderOpenRouterModels();
1536
1537 // Search handler
1538 $('#mxchat_openrouter_search').on('input', function() {
1539 renderOpenRouterModels($(this).val());
1540 });
1541
1542 $('#mxchat_back_to_provider_select').on('click', function(e) {
1543 e.preventDefault();
1544 // Restore footer to default state
1545 restoreDefaultModalFooter();
1546 // Instead of location.reload(), restore original content
1547 restoreOriginalModalContent();
1548 });
1549
1550 // Back to providers footer button
1551 $('#mxchat_back_to_models_footer').on('click', function(e) {
1552 e.preventDefault();
1553 // Restore footer to default state
1554 restoreDefaultModalFooter();
1555 // Restore original content
1556 restoreOriginalModalContent();
1557 });
1558
1559 // Model selection - just highlight, don't save yet
1560 $(document).on('click', '.mxchat-openrouter-card', function(e) {
1561 e.stopPropagation(); // Prevent triggering the regular model card handler
1562
1563 const modelId = $(this).data('model-id');
1564 const modelName = $(this).data('model-name');
1565
1566 // Remove selection from all cards
1567 $('.mxchat-openrouter-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1568
1569 // Add selection to clicked card
1570 $(this).addClass('mxchat-model-selected');
1571 if ($(this).find('.mxchat-model-selector-checkmark').length === 0) {
1572 $(this).append('<div class="mxchat-model-selector-checkmark">✓</div>');
1573 }
1574
1575 // Update pending selection
1576 pendingSelection.modelId = modelId;
1577 pendingSelection.modelName = modelName;
1578
1579 // Enable the save button
1580 $('#mxchat_save_openrouter_model').prop('disabled', false);
1581 });
1582
1583 // Save button handler
1584 $('#mxchat_save_openrouter_model').on('click', function() {
1585 const $saveButton = $(this);
1586
1587 if (!pendingSelection.modelId) {
1588 return;
1589 }
1590
1591 // Disable button and show loading state
1592 $saveButton.prop('disabled', true).html('<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...');
1593
1594 // First, save that we're using OpenRouter
1595 jQuery.ajax({
1596 url: mxchatAdmin.ajax_url,
1597 type: 'POST',
1598 data: {
1599 action: 'mxchat_save_setting',
1600 name: 'model',
1601 value: 'openrouter',
1602 _ajax_nonce: mxchatAdmin.setting_nonce
1603 },
1604 success: function() {
1605 // After model is set, save the model ID
1606 jQuery.ajax({
1607 url: mxchatAdmin.ajax_url,
1608 type: 'POST',
1609 data: {
1610 action: 'mxchat_save_setting',
1611 name: 'openrouter_selected_model',
1612 value: pendingSelection.modelId,
1613 _ajax_nonce: mxchatAdmin.setting_nonce
1614 },
1615 success: function() {
1616 // Update DOM immediately
1617 $('#openrouter_selected_model').val(pendingSelection.modelId);
1618
1619 // After model ID is saved, save the display name
1620 jQuery.ajax({
1621 url: mxchatAdmin.ajax_url,
1622 type: 'POST',
1623 data: {
1624 action: 'mxchat_save_setting',
1625 name: 'openrouter_selected_model_name',
1626 value: pendingSelection.modelName,
1627 _ajax_nonce: mxchatAdmin.setting_nonce
1628 },
1629 success: function() {
1630 // Update DOM immediately
1631 $('#openrouter_selected_model_name').val(pendingSelection.modelName);
1632
1633 // Update button text
1634 $('#mxchat_model_selector_btn').text('OpenRouter: ' + pendingSelection.modelName);
1635
1636 // Update the "Currently using" message
1637 const $currentSelection = $('#openrouter-current-selection');
1638 if ($currentSelection.length) {
1639 $currentSelection.html(
1640 '<span class="dashicons dashicons-yes" style="font-size: 16px; vertical-align: middle;"></span> ' +
1641 'Currently using: <strong>' + pendingSelection.modelName + '</strong>'
1642 ).show();
1643 }
1644
1645 // Show success state briefly
1646 $saveButton.html('<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!');
1647
1648 // Close modal after short delay
1649 setTimeout(function() {
1650 // Restore footer to default state
1651 restoreDefaultModalFooter();
1652 $modal.hide();
1653 }, 800);
1654 },
1655 error: function() {
1656 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1657 alert('Failed to save model name. Please try again.');
1658 }
1659 });
1660 },
1661 error: function() {
1662 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1663 alert('Failed to save model ID. Please try again.');
1664 }
1665 });
1666 },
1667 error: function() {
1668 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1669 alert('Failed to save OpenRouter selection. Please try again.');
1670 }
1671 });
1672 });
1673
1674 }
1675
1676 // 3.2.3: Confirmation dialog shown when the user attempts to switch embedding
1677 // models after they've already embedded content with a different model. Mixing
1678 // embeddings from two models silently breaks similarity matching.
1679 function showEmbeddingSwitchWarning(data, newValue, onChoice) {
1680 $('#mxchat_embedding_switch_warning').remove();
1681
1682 const dimsBlock = data.dims_differ ? `
1683 <div class="mxchat-embed-warn-dims">
1684 <strong>Dimension mismatch:</strong>
1685 Existing vectors are ${data.active_dims}-dimensional, but ${data.new_label}
1686 produces ${data.new_dims}-dimensional vectors.
1687 If you use Pinecone, your index will reject queries entirely until you re-embed.
1688 </div>` : '';
1689
1690 const $modal = $(`
1691 <div id="mxchat_embedding_switch_warning" class="mxchat-embed-warn-overlay">
1692 <div class="mxchat-embed-warn-dialog">
1693 <div class="mxchat-embed-warn-header">
1694 <span class="mxchat-embed-warn-icon">⚠️</span>
1695 <h3>Switching embedding models will break similarity matching</h3>
1696 </div>
1697 <div class="mxchat-embed-warn-body">
1698 <p>
1699 Your knowledge base and actions are currently embedded with
1700 <code>${data.active_label}</code>. Switching to
1701 <code>${data.new_label}</code> means new queries are embedded with
1702 a different model than the stored vectors — the chatbot will return
1703 inaccurate results or fail to match anything.
1704 </p>
1705 ${dimsBlock}
1706 <p><strong>To switch safely:</strong></p>
1707 <ol>
1708 <li>Go to <em>Knowledge Base</em> and delete all existing entries.</li>
1709 <li>Go to <em>Actions</em> and delete all existing actions.</li>
1710 <li>Come back here, switch the model, then re-import your content and re-add your actions.</li>
1711 </ol>
1712 </div>
1713 <div class="mxchat-embed-warn-footer">
1714 <button type="button" class="button button-secondary" id="mxchat_embed_warn_cancel">Cancel — keep ${data.active_label}</button>
1715 <button type="button" class="button button-primary mxchat-embed-warn-danger" id="mxchat_embed_warn_continue">Switch anyway (I'll handle it)</button>
1716 </div>
1717 </div>
1718 </div>
1719 `);
1720
1721 $('body').append($modal);
1722
1723 let resolved = false;
1724 function resolve(confirmed) {
1725 if (resolved) return;
1726 resolved = true;
1727 $modal.remove();
1728 if (typeof onChoice === 'function') onChoice(confirmed);
1729 }
1730
1731 // Click on the overlay background dismisses (cancel)
1732 $modal.on('click', function(e) {
1733 if (e.target === $modal[0]) resolve(false);
1734 });
1735 $modal.find('#mxchat_embed_warn_cancel').on('click', function() { resolve(false); });
1736 $modal.find('#mxchat_embed_warn_continue').on('click', function() { resolve(true); });
1737 }
1738
1739 // Embedding model selector - completely separate from chat model selector
1740 function setupMxChatEmbeddingModelSelector() {
1741 const $embeddingModelSelect = $('#embedding_model');
1742
1743 // Skip if the element doesn't exist on the page
1744 if ($embeddingModelSelect.length === 0) {
1745 return;
1746 }
1747
1748 const $embeddingModelSelectorButton = $('<button>', {
1749 type: 'button',
1750 id: 'mxchat_embedding_model_selector_btn',
1751 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
1752 text: 'Select Embedding Model'
1753 });
1754
1755 // Replace the select dropdown with a button
1756 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
1757
1758 // Custom-provider embeddings lock (plan ae02cb): while "Use custom provider
1759 // for embeddings" is on, the standard picker is inert — the effective model
1760 // is the Custom Embedding Model field. Lock the button (which also makes the
1761 // switch-warning preflight unreachable), show the explanatory note, and keep
1762 // both in sync with the toggle without a reload.
1763 const $customEmbedToggle = $('#custom_provider_for_embeddings');
1764 function syncCustomEmbeddingLock() {
1765 const locked = $customEmbedToggle.length
1766 ? $customEmbedToggle.is(':checked')
1767 : $embeddingModelSelect.prop('disabled');
1768 $embeddingModelSelect.prop('disabled', locked);
1769 $embeddingModelSelectorButton.prop('disabled', locked);
1770 $('#mxchat_embedding_custom_note').toggle(locked);
1771 if (locked) {
1772 $('#' + embeddingModalId).hide();
1773 $('.mxchat-embedding-api-status').hide();
1774 } else if (typeof window.mxchatRefreshAPIKeyStatus === 'function') {
1775 window.mxchatRefreshAPIKeyStatus();
1776 }
1777 }
1778 // NOTE: invoked further down, after embeddingModalId exists — calling it
1779 // here would hit the const's temporal dead zone whenever the page loads
1780 // with the lock already on.
1781
1782 // Update button text to show currently selected model
1783 function updateButtonText() {
1784 const selectedModel = $embeddingModelSelect.val();
1785 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
1786 $embeddingModelSelectorButton.text(selectedModelText);
1787 }
1788
1789 // Initialize button text
1790 updateButtonText();
1791
1792 // Create a unique ID for the modal to avoid conflicts
1793 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
1794
1795 // Create and append modal HTML with unique IDs
1796 const embeddingModelSelectorModal = `
1797 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
1798 <div class="mxchat-embedding-model-selector-modal-content">
1799 <div class="mxchat-embedding-model-selector-modal-header">
1800 <h3>Select Embedding Model</h3>
1801 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
1802 </div>
1803 <div class="mxchat-embedding-model-selector-modal-body">
1804 <div class="mxchat-embedding-model-selector-search-container">
1805 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
1806 </div>
1807 <div class="mxchat-embedding-model-selector-categories">
1808 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
1809 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
1810 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
1811 <button class="mxchat-embedding-model-category-btn" data-category="gemini">Google Gemini</button>
1812 </div>
1813 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
1814 </div>
1815 <div class="mxchat-embedding-model-selector-modal-footer">
1816 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
1817 </div>
1818 </div>
1819 </div>
1820 `;
1821
1822 // Use jQuery's append to ensure it doesn't clash with existing modals
1823 $('body').append(embeddingModelSelectorModal);
1824
1825 // Apply the custom-embeddings lock now that the modal id is live, and keep
1826 // it in sync with the toggle.
1827 syncCustomEmbeddingLock();
1828 $customEmbedToggle.on('change.embeddingModelSelector', syncCustomEmbeddingLock);
1829
1830 // Populate models grid
1831 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1832 const $grid = $('#mxchat_embedding_models_grid');
1833 $grid.empty();
1834
1835 // Define embedding models with descriptions and context lengths
1836 const models = {
1837 openai: [
1838 {
1839 value: 'text-embedding-3-small',
1840 label: 'TE3 Small',
1841 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1842 },
1843 {
1844 value: 'text-embedding-ada-002',
1845 label: 'Ada 2',
1846 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1847 },
1848 {
1849 value: 'text-embedding-3-large',
1850 label: 'TE3 Large',
1851 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1852 }
1853 ],
1854 voyage: [
1855 {
1856 value: 'voyage-3-large',
1857 label: 'Voyage-3 Large',
1858 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1859 }
1860 ],
1861 gemini: [
1862 {
1863 value: 'gemini-embedding-001',
1864 label: 'Gemini Embedding',
1865 description: 'Stable SOTA embeddings (1536 dimensions, 8K context)'
1866 }
1867 ]
1868 };
1869
1870 let allModels = [];
1871 Object.keys(models).forEach(key => {
1872 if (category === 'all' || category === key) {
1873 allModels = allModels.concat(models[key]);
1874 }
1875 });
1876
1877 // Filter by search term if present
1878 if (filter) {
1879 const lowerFilter = filter.toLowerCase();
1880 allModels = allModels.filter(model =>
1881 model.label.toLowerCase().includes(lowerFilter) ||
1882 model.description.toLowerCase().includes(lowerFilter)
1883 );
1884 }
1885
1886 // Create model cards
1887 allModels.forEach(model => {
1888 const isSelected = $embeddingModelSelect.val() === model.value;
1889 let providerClass = 'mxchat-embedding-model-provider-openai';
1890
1891 if (model.value.startsWith('voyage-')) {
1892 providerClass = 'mxchat-embedding-model-provider-voyage';
1893 } else if (model.value.startsWith('gemini-embedding-')) {
1894 providerClass = 'mxchat-embedding-model-provider-gemini';
1895 }
1896
1897 let iconHTML = '';
1898 if (model.value.startsWith('voyage-')) {
1899 iconHTML = '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>';
1900 } else if (model.value.startsWith('gemini-embedding-')) {
1901 iconHTML = '<span class="dashicons dashicons-google mxchat-embedding-model-icon-gemini"></span>';
1902 } else {
1903 iconHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" class="mxchat-embedding-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>';
1904 }
1905
1906 const $modelCard = $(`
1907 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1908 <div class="mxchat-embedding-model-selector-icon">
1909 ${iconHTML}
1910 </div>
1911 <div class="mxchat-embedding-model-selector-info">
1912 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1913 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1914 </div>
1915 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1916 </div>
1917 `);
1918
1919 $grid.append($modelCard);
1920 });
1921 }
1922
1923 // Event handlers - use namespaced events to avoid conflicts
1924 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1925 e.stopPropagation(); // Prevent event bubbling
1926 $('#' + embeddingModalId).show();
1927 populateEmbeddingModelsGrid('', 'all');
1928 });
1929
1930 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1931 e.stopPropagation(); // Prevent event bubbling
1932 $('#' + embeddingModalId).hide();
1933 });
1934
1935 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1936 e.stopPropagation(); // Prevent event bubbling
1937 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1938 $(this).addClass('active');
1939 const category = $(this).data('category');
1940 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1941 populateEmbeddingModelsGrid(searchTerm, category);
1942 });
1943
1944 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1945 const searchTerm = $(this).val();
1946 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1947 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1948 });
1949
1950 // 3.2.3: Commit a model change — same as the original click handler, just
1951 // factored out so it can be invoked from both the safe path and the
1952 // post-confirmation path.
1953 function commitEmbeddingModelChange(modelValue) {
1954 $embeddingModelSelect.val(modelValue);
1955 const changeEvent = new Event('change', { bubbles: true });
1956 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1957 updateButtonText();
1958 $('#' + embeddingModalId).hide();
1959 }
1960
1961 // Use a direct selector to avoid conflicts with other card elements
1962 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1963 e.stopPropagation(); // Prevent event bubbling
1964 const modelValue = $(this).data('value');
1965 const previousValue = $embeddingModelSelect.val();
1966
1967 // No-op if the user clicked the already-selected card
1968 if (modelValue === previousValue) {
1969 $('#' + embeddingModalId).hide();
1970 return;
1971 }
1972
1973 // 3.2.3: Preflight — if the user has content embedded with a different
1974 // model, surface a confirmation dialog before committing the switch.
1975 $.post(ajaxurl, {
1976 action: 'mxchat_check_embedding_switch',
1977 security: (typeof mxchatAdmin !== 'undefined' ? mxchatAdmin.nonce : ''),
1978 new_model: modelValue
1979 }).done(function(resp) {
1980 if (resp && resp.success && resp.data && resp.data.is_mismatch) {
1981 showEmbeddingSwitchWarning(resp.data, modelValue, function(confirmed) {
1982 if (confirmed) {
1983 commitEmbeddingModelChange(modelValue);
1984 }
1985 });
1986 } else {
1987 commitEmbeddingModelChange(modelValue);
1988 }
1989 }).fail(function() {
1990 // If preflight fails, fall back to the original behavior so a network
1991 // hiccup doesn't block legitimate model switches.
1992 commitEmbeddingModelChange(modelValue);
1993 });
1994 return;
1995 });
1996
1997 // Close modal when clicking outside - use namespaced events
1998 $(window).on('click.embeddingModelSelector', function(event) {
1999 if ($(event.target).is('#' + embeddingModalId)) {
2000 $('#' + embeddingModalId).hide();
2001 }
2002 });
2003 }
2004
2005 // Call this function after the DOM is fully loaded
2006 $(document).ready(function() {
2007 setupMxChatModelSelector();
2008 setupMxChatEmbeddingModelSelector();
2009 });
2010
2011 // Add Intent Form Submission
2012 $('#mxchat-add-intent-form').on('submit', function(event) {
2013 $('#mxchat-intent-loading').show();
2014 $('#mxchat-intent-loading-text').show();
2015 $(this).find('button[type="submit"]').hide();
2016 });
2017
2018 // Inline Edit Functionality
2019 $('.edit-button').on('click', function() {
2020 var row = $(this).closest('tr');
2021
2022 // Clear URL field if it's a manual content URL (mxchat:// protocol)
2023 var urlEdit = row.find('.url-edit');
2024 if (urlEdit.length && urlEdit.val().indexOf('mxchat://') === 0) {
2025 urlEdit.val('');
2026 }
2027
2028 // Expand the accordion to show the edit textarea (fixes short content editing)
2029 var contentFull = row.find('.mxchat-content-full');
2030 if (contentFull.length && contentFull.is(':hidden')) {
2031 contentFull.show();
2032 row.find('.mxchat-content-preview').hide();
2033 }
2034
2035 row.find('.content-view, .url-view').hide();
2036 row.find('.content-edit, .url-edit').show();
2037 row.find('.edit-button').hide();
2038 row.find('.save-button').show();
2039 });
2040
2041 // Save button handler
2042 // Save button handler
2043 $('.save-button').on('click', function() {
2044 var button = $(this);
2045 var row = button.closest('tr');
2046 var id = button.data('id');
2047 var nonce = button.data('nonce'); // Get nonce from button data attribute
2048 var newContent = row.find('.content-edit').val();
2049 var newUrl = row.find('.url-edit').val();
2050
2051 //console.log('Nonce from button:', nonce); // Debug
2052
2053 button.prop('disabled', true);
2054 button.text('Saving...');
2055
2056 $.ajax({
2057 url: mxchatAdmin.ajax_url,
2058 type: 'POST',
2059 data: {
2060 action: 'mxchat_save_inline_prompt',
2061 id: id,
2062 article_content: newContent,
2063 article_url: newUrl,
2064 _ajax_nonce: nonce // Use nonce from button
2065 },
2066 success: function(response) {
2067 button.prop('disabled', false);
2068 button.text('Save');
2069
2070 if (response.success) {
2071 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
2072 if (newUrl) {
2073 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank"><span class="dashicons dashicons-external"></span> View Source</a>');
2074 } else {
2075 row.find('.url-view').html('<span class="mxchat-na">Manual Content</span>');
2076 }
2077
2078 row.find('.content-edit, .url-edit').hide();
2079 row.find('.content-view, .url-view').show();
2080 row.find('.save-button').hide();
2081 row.find('.edit-button').show();
2082
2083 // Restore accordion state - show preview, hide full content
2084 row.find('.mxchat-content-preview').show();
2085 row.find('.mxchat-content-full').hide();
2086 } else {
2087 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
2088 }
2089 },
2090 error: function() {
2091 button.prop('disabled', false);
2092 button.text('Save');
2093 alert('An error occurred while saving.');
2094 }
2095 });
2096 });
2097
2098
2099 // Questions handling
2100 $('.mxchat-add-question').on('click', function () {
2101 const container = $('#mxchat-additional-questions-container');
2102 const questionCount = container.find('.mxchat-question-row').length + 4;
2103 const questionIndex = container.find('.mxchat-question-row').length;
2104
2105 const newQuestion = `
2106 <div class="mxchat-question-row">
2107 <input type="text"
2108 name="additional_popular_questions[]"
2109 placeholder="Enter Additional Popular Question ${questionCount}"
2110 class="regular-text mxchat-question-input"
2111 data-question-index="${questionIndex}" />
2112 <button type="button" class="button mxchat-remove-question"
2113 aria-label="Remove question">Remove</button>
2114 </div>
2115 `;
2116 container.append(newQuestion);
2117 });
2118
2119 $(document).on('click', '.mxchat-remove-question', function () {
2120 $(this).closest('.mxchat-question-row').remove();
2121 saveQuestions();
2122 });
2123
2124 $(document).on('change', '.mxchat-question-input', function() {
2125 saveQuestions();
2126 });
2127
2128 function saveQuestions() {
2129 const questions = [];
2130 $('.mxchat-question-input').each(function() {
2131 const value = $(this).val().trim();
2132 if (value) {
2133 questions.push(value);
2134 }
2135 });
2136
2137 const feedbackContainer = $('<div class="feedback-container"></div>');
2138 const spinner = $('<div class="saving-spinner"></div>');
2139 const successIcon = $('<div class="success-icon">✔</div>');
2140
2141 // Append feedback after the add button
2142 $('.mxchat-add-question').after(feedbackContainer);
2143 feedbackContainer.append(spinner);
2144
2145 // Save via AJAX
2146 $.ajax({
2147 url: mxchatAdmin.ajax_url,
2148 type: 'POST',
2149 data: {
2150 action: 'mxchat_save_setting',
2151 name: 'additional_popular_questions',
2152 value: JSON.stringify(questions),
2153 _ajax_nonce: mxchatAdmin.setting_nonce
2154 },
2155 success: function(response) {
2156 if (response.success) {
2157 spinner.fadeOut(200, function() {
2158 feedbackContainer.append(successIcon);
2159 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
2160 feedbackContainer.remove();
2161 });
2162 });
2163 } else {
2164 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
2165 feedbackContainer.remove();
2166 }
2167 },
2168 error: function() {
2169 alert('An error occurred while saving questions.');
2170 feedbackContainer.remove();
2171 }
2172 });
2173 }
2174
2175 // Live agent status handler
2176 const statusToggle = document.getElementById('live_agent_status');
2177 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
2178 if (statusToggle && statusText) {
2179 statusToggle.addEventListener('change', function() {
2180 // Update display text
2181 statusText.textContent = this.checked ? 'Online' : 'Offline';
2182
2183 // Send the correct on/off value to the server
2184 if (window.mxchatSaveSetting) {
2185 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
2186 }
2187 });
2188 }
2189
2190 // Live agent availability schedules (plans 8ccaa2 + 99d7a4).
2191 // The editor markup exists once PER CHANNEL (Slack + Telegram tabs), so
2192 // everything is scoped inside each .mxchat-la-schedule container via classes
2193 // — no page-global ids. Each editor collects its own day grid into its own
2194 // hidden live_agent_schedule_<channel> input as JSON and fires one change
2195 // event, so the existing autosave handler above does the POST.
2196 $('.mxchat-la-schedule').each(function() {
2197 const $laSchedule = $(this);
2198 const $laEnabled = $laSchedule.find('.mxchat-la-enabled');
2199 const $laHidden = $laSchedule.find('.mxchat-la-hidden');
2200 const $laDays = $laSchedule.find('.mxchat-la-days');
2201 const $laStatus = $laSchedule.find('.mxchat-la-status-text');
2202
2203 function laCollect() {
2204 const days = {};
2205 $laDays.find('.mxchat-la-day').each(function() {
2206 const $day = $(this);
2207 const n = $day.data('day');
2208 days[n] = {
2209 enabled: $day.find('.mxchat-la-day-enabled').is(':checked'),
2210 start: $day.find('.mxchat-la-start').val() || '09:00',
2211 end: $day.find('.mxchat-la-end').val() || '17:00'
2212 };
2213 });
2214 return { enabled: $laEnabled.is(':checked'), days: days };
2215 }
2216
2217 function laSync() {
2218 const schedule = laCollect();
2219 $laSchedule.toggleClass('is-active', schedule.enabled);
2220 $laDays.attr('aria-hidden', schedule.enabled ? 'false' : 'true');
2221 $laStatus.text(
2222 schedule.enabled
2223 ? (mxchatAdmin.i18n_scheduled_hours || 'Scheduled hours')
2224 : (mxchatAdmin.i18n_always_available || 'Always available')
2225 );
2226 $laDays.find('.mxchat-la-day').each(function() {
2227 const $day = $(this);
2228 $day.toggleClass('is-on', $day.find('.mxchat-la-day-enabled').is(':checked'));
2229 });
2230 // Hand this channel's schedule to the shared autosave transport.
2231 $laHidden.val(JSON.stringify(schedule)).trigger('change');
2232 }
2233
2234 $laSchedule.find('.mxchat-la-field').on('change', laSync);
2235 });
2236
2237 // Function to adjust the textarea height to content
2238 function adjustTextareaHeight() {
2239 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
2240 this.style.height = this.scrollHeight + 'px'; // Expand to content height
2241 }
2242
2243 // Function to reset the textarea height to initial
2244 function resetTextareaHeight() {
2245 this.style.height = ''; // Remove inline height, reverting to CSS default
2246 }
2247
2248 // Target the specific textarea by ID
2249 var $textarea = $('#system_prompt_instructions');
2250
2251 // Bind events
2252 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
2253 .on('blur', resetTextareaHeight); // Reset on blur
2254 });
2255
2256
2257
2258 document.addEventListener('DOMContentLoaded', function() {
2259 // Check if we're on the correct page before initializing
2260 const modal = document.getElementById('mxchat-action-modal');
2261
2262 // Only initialize if the modal exists on this page
2263 if (modal) {
2264 //console.log('MXChat Action Modal JS Loaded');
2265
2266 // Initialize the action modal functionality
2267 initStepBasedActionModal();
2268 }
2269
2270 // Function to initialize the step-based action modal
2271 function initStepBasedActionModal() {
2272 // We already checked for modal existence above, so no need to check again
2273
2274 const actionStep1 = document.getElementById('mxchat-action-step-1');
2275 const actionStep2 = document.getElementById('mxchat-action-step-2');
2276 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
2277 const searchInput = document.getElementById('action-type-search');
2278 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
2279 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
2280 const actionForm = document.getElementById('mxchat-action-form');
2281 const callbackInput = document.getElementById('callback_function');
2282 const actionIdField = document.getElementById('edit_action_id');
2283 const labelField = document.getElementById('intent_label');
2284 const phrasesField = document.getElementById('action_phrases');
2285 const formActionType = document.getElementById('form_action_type');
2286 const nonceContainer = document.getElementById('action-nonce-container');
2287 const thresholdSlider = document.getElementById('similarity_threshold');
2288 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2289
2290 // Rest of your initialization code remains the same...
2291
2292 // Log the structure of one action card for debugging
2293 if (actionCards.length > 0) {
2294 //console.log('First action card data attributes:', actionCards[0].dataset);
2295 //console.log('First action card HTML:', actionCards[0].outerHTML);
2296 }
2297
2298 // Add click event listeners to category buttons
2299 categoryButtons.forEach(button => {
2300 button.addEventListener('click', function() {
2301 //console.log('Category button clicked:', this.dataset.category);
2302
2303 // Remove active class from all buttons
2304 categoryButtons.forEach(btn => btn.classList.remove('active'));
2305
2306 // Add active class to clicked button
2307 this.classList.add('active');
2308
2309 // Get selected category
2310 const category = this.dataset.category;
2311
2312 // Filter action cards
2313 filterActionCards(category, searchInput.value);
2314 });
2315 });
2316
2317 // Add search functionality
2318 if (searchInput) {
2319 searchInput.addEventListener('input', function() {
2320 // Get active category
2321 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
2322 //console.log('Search input changed, active category:', activeCategory);
2323
2324 // Filter action cards
2325 filterActionCards(activeCategory, this.value);
2326 });
2327 }
2328
2329 // Add click event listeners to action cards
2330 actionCards.forEach(card => {
2331 card.addEventListener('click', function() {
2332 // Get the action data
2333 const isPro = this.dataset.pro === 'true';
2334 const isInstalled = this.dataset.installed === 'true';
2335 const addonName = this.dataset.addon || '';
2336 const actionValue = this.dataset.value;
2337 const actionLabel = this.dataset.label;
2338 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
2339 const actionDescription = this.querySelector('p').textContent;
2340
2341 // Check if this is a promotional card (add-on not installed)
2342 const isPromo = this.dataset.promo === 'true';
2343
2344 if (isPromo || (addonName && !isInstalled)) {
2345 // Add-on required but not installed — show informational notice
2346 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '').replace('— Get Add-on', '').trim() || addonName + ' Add-on';
2347 showAddonRequiredNotice(addonDisplayName);
2348 return;
2349 }
2350
2351 // If we get here, the action is available - proceed as normal
2352 callbackInput.value = actionValue;
2353
2354 // Update the selected action display in step 2
2355 document.getElementById('selected-action-title').textContent = actionLabel;
2356 document.getElementById('selected-action-description').textContent = actionDescription;
2357 document.getElementById('selected-action-icon').innerHTML =
2358 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2359
2360 // Set a default label based on the action type (user can change it)
2361 if (!labelField.value) {
2362 labelField.value = actionLabel;
2363 }
2364
2365 // Move to step 2
2366 actionStep1.classList.remove('active');
2367 actionStep2.classList.add('active');
2368
2369 // Update modal title
2370 });
2371 });
2372
2373 // Back button functionality
2374 if (backToStep1Btn) {
2375 backToStep1Btn.addEventListener('click', function() {
2376 //console.log('Back button clicked');
2377 actionStep2.classList.remove('active');
2378 actionStep1.classList.add('active');
2379 });
2380 }
2381
2382 // Function to filter action cards by category and search term
2383 function filterActionCards(category, searchTerm) {
2384 searchTerm = searchTerm.toLowerCase().trim();
2385 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
2386
2387 let visibleCount = 0;
2388
2389 // Show all cards initially with animation
2390 actionCards.forEach((card, index) => {
2391 // Reset animation
2392 card.style.animation = 'none';
2393 // Trigger reflow
2394 void card.offsetWidth;
2395
2396 // Determine if card should be visible based on category and search term
2397 const cardCategory = card.dataset.category || '';
2398 const matchesCategory = category === 'all' || cardCategory === category;
2399
2400 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
2401 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
2402 const matchesSearch = searchTerm === '' ||
2403 cardTitle.includes(searchTerm) ||
2404 cardDesc.includes(searchTerm);
2405
2406 // Show/hide card with animation
2407 if (matchesCategory && matchesSearch) {
2408 card.style.display = 'flex';
2409 // Staggered animation for cards
2410 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
2411 visibleCount++;
2412 } else {
2413 card.style.display = 'none';
2414 }
2415 });
2416
2417 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
2418 }
2419
2420 // Function to show notice for Pro features
2421 function showProFeatureNotice() {
2422 //console.log('Showing Pro feature notice');
2423 // Check if we already have a notification container
2424 let noticeContainer = document.querySelector('.mxchat-pro-notice');
2425
2426 if (!noticeContainer) {
2427 // Create the notice container
2428 noticeContainer = document.createElement('div');
2429 noticeContainer.className = 'mxchat-pro-notice';
2430
2431 // Create content
2432 noticeContainer.innerHTML = `
2433 <div class="mxchat-pro-notice-content">
2434 <h3>MxChat Pro Feature</h3>
2435 <p>This action is available in the Pro version only.</p>
2436 <div class="mxchat-pro-notice-buttons">
2437 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
2438 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
2439 </div>
2440 </div>
2441 `;
2442
2443 // Append to body
2444 document.body.appendChild(noticeContainer);
2445
2446 // Add close functionality
2447 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
2448 closeButton.addEventListener('click', function() {
2449 noticeContainer.classList.remove('active');
2450 setTimeout(() => {
2451 noticeContainer.remove();
2452 }, 300);
2453 });
2454
2455 // Click outside to close (drag-safe)
2456 mxchatDragSafeOverlayClose(noticeContainer, function() {
2457 closeButton.click();
2458 });
2459
2460 // Show with animation
2461 setTimeout(() => {
2462 noticeContainer.classList.add('active');
2463 }, 10);
2464 } else {
2465 // If it already exists, just make it visible again
2466 noticeContainer.classList.add('active');
2467 }
2468 }
2469
2470 // Function to show notice for add-on requirements
2471 function showAddonRequiredNotice(addonName) {
2472 //console.log(`Showing add-on notice for: ${addonName}`);
2473 // Check if we already have a notification container
2474 let noticeContainer = document.querySelector('.mxchat-addon-notice');
2475
2476 if (!noticeContainer) {
2477 // Create the notice container
2478 noticeContainer = document.createElement('div');
2479 noticeContainer.className = 'mxchat-addon-notice';
2480
2481 // Create content
2482 noticeContainer.innerHTML = `
2483 <div class="mxchat-addon-notice-content">
2484 <span class="mxchat-addon-notice-icon">🧩</span>
2485 <h3>Add-on Required</h3>
2486 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
2487 <div class="mxchat-addon-notice-buttons">
2488 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
2489 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
2490 </div>
2491 </div>
2492 `;
2493
2494 // Append to body
2495 document.body.appendChild(noticeContainer);
2496
2497 // Add close functionality
2498 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
2499 closeButton.addEventListener('click', function() {
2500 noticeContainer.classList.remove('active');
2501 setTimeout(() => {
2502 noticeContainer.remove();
2503 }, 300);
2504 });
2505
2506 // Click outside to close (drag-safe)
2507 mxchatDragSafeOverlayClose(noticeContainer, function() {
2508 closeButton.click();
2509 });
2510
2511 // Show with animation
2512 setTimeout(() => {
2513 noticeContainer.classList.add('active');
2514 }, 10);
2515 } else {
2516 // If it already exists, update the content
2517 const addonNameElement = noticeContainer.querySelector('p strong');
2518 if (addonNameElement) {
2519 addonNameElement.textContent = addonName;
2520 }
2521
2522 // Make it visible again
2523 noticeContainer.classList.add('active');
2524 }
2525 }
2526
2527 // Form submission handling
2528 if (actionForm) {
2529 actionForm.addEventListener('submit', function() {
2530 //console.log('Form submitted');
2531 document.getElementById('mxchat-action-loading').style.display = 'flex';
2532 this.querySelector('button[type="submit"]').disabled = true;
2533 });
2534 }
2535 }
2536
2537 // Setup add action buttons (only if we're on the correct page)
2538 if (modal) {
2539 // Update the modal open function to support the step-based flow
2540 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '', enabledBots = null) {
2541 //console.log('Modal opening, edit mode:', isEdit);
2542
2543 // Get form fields
2544 const actionIdField = document.getElementById('edit_action_id');
2545 const labelField = document.getElementById('intent_label');
2546 const phrasesField = document.getElementById('action_phrases');
2547 const formActionType = document.getElementById('form_action_type');
2548 const callbackInput = document.getElementById('callback_function');
2549 const saveButton = document.getElementById('mxchat-save-action-btn');
2550 const nonceContainer = document.getElementById('action-nonce-container');
2551 const thresholdSlider = document.getElementById('similarity_threshold');
2552 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2553 const actionStep1 = document.getElementById('mxchat-action-step-1');
2554 const actionStep2 = document.getElementById('mxchat-action-step-2');
2555 const searchInput = document.getElementById('action-type-search');
2556
2557 // Set up modal for edit or create
2558 if (isEdit) {
2559 saveButton.textContent = 'Update Action';
2560 formActionType.value = 'mxchat_edit_intent';
2561 actionIdField.value = actionId;
2562 labelField.value = label;
2563 phrasesField.value = phrases;
2564 callbackInput.value = callbackFunction;
2565 thresholdSlider.value = threshold; // Set the current threshold value
2566 thresholdDisplay.textContent = threshold + '%'; // Update display
2567
2568 // NEW: Handle bot selection checkboxes for editing
2569 // First uncheck all bot checkboxes
2570 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2571 checkbox.checked = false;
2572 });
2573
2574 // Then check the ones that should be enabled
2575 if (enabledBots) {
2576 let botsArray;
2577 if (typeof enabledBots === 'string') {
2578 try {
2579 botsArray = JSON.parse(enabledBots);
2580 } catch (e) {
2581 console.warn('Failed to parse enabledBots:', enabledBots);
2582 botsArray = ['default']; // fallback
2583 }
2584 } else if (Array.isArray(enabledBots)) {
2585 botsArray = enabledBots;
2586 } else {
2587 botsArray = ['default']; // fallback
2588 }
2589
2590 botsArray.forEach(botId => {
2591 const checkbox = document.querySelector(`input[name="enabled_bots[]"][value="${botId}"]`);
2592 if (checkbox) {
2593 checkbox.checked = true;
2594 }
2595 });
2596 } else {
2597 // Fallback to default if no bot data
2598 const defaultCheckbox = document.querySelector('input[name="enabled_bots[]"][value="default"]');
2599 if (defaultCheckbox) {
2600 defaultCheckbox.checked = true;
2601 }
2602 }
2603
2604 // Update the nonce field for editing
2605 nonceContainer.innerHTML = ''; // Clear existing nonce
2606 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
2607 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
2608 }
2609
2610 // For editing, go directly to step 2 and update the selected action display
2611 actionStep1.classList.remove('active');
2612 actionStep2.classList.add('active');
2613
2614 // Find the matching action card to get its details
2615 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
2616 let foundCard = null;
2617
2618 actionCards.forEach(card => {
2619 if (card.dataset.value === callbackFunction) {
2620 foundCard = card;
2621 }
2622 });
2623
2624 if (foundCard) {
2625 //console.log('Found matching action card for:', callbackFunction);
2626 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
2627 const actionIconElement = foundCard.querySelector('.dashicons');
2628 const actionIcon = actionIconElement
2629 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
2630 : 'admin-generic';
2631 const actionDescription = foundCard.querySelector('p')?.textContent || '';
2632
2633 document.getElementById('selected-action-title').textContent = actionLabel;
2634 document.getElementById('selected-action-description').textContent = actionDescription;
2635 document.getElementById('selected-action-icon').innerHTML =
2636 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2637 } else {
2638 //console.log('No matching action card found for:', callbackFunction);
2639 // Fallback if we can't find the card
2640 document.getElementById('selected-action-title').textContent = label;
2641 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
2642 document.getElementById('selected-action-icon').innerHTML =
2643 `<span class="dashicons dashicons-admin-generic"></span>`;
2644 }
2645 } else {
2646 //console.log('Setting up create mode');
2647 saveButton.textContent = 'Save Action';
2648 formActionType.value = 'mxchat_add_intent';
2649 actionIdField.value = '';
2650 labelField.value = '';
2651 phrasesField.value = '';
2652 callbackInput.value = '';
2653 thresholdSlider.value = 85; // Default value for new actions
2654 thresholdDisplay.textContent = '85%'; // Default display
2655
2656 // For new actions, ensure default is checked and others are unchecked
2657 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2658 checkbox.checked = (checkbox.value === 'default');
2659 });
2660
2661 // Update the nonce field for adding
2662 nonceContainer.innerHTML = ''; // Clear existing nonce
2663 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
2664 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
2665 }
2666
2667 // For creating new, start at step 1
2668 actionStep1.classList.add('active');
2669 actionStep2.classList.remove('active');
2670 }
2671
2672 // Show modal with animation
2673 modal.style.display = 'flex';
2674 requestAnimationFrame(() => {
2675 modal.classList.add('active');
2676 });
2677
2678 // Set up close handlers
2679 let detachEsc = null;
2680 const closeModal = () => {
2681 //console.log('Closing modal');
2682 if (detachEsc) {
2683 detachEsc();
2684 detachEsc = null;
2685 }
2686 modal.classList.remove('active');
2687 setTimeout(() => {
2688 modal.style.display = 'none';
2689 }, 300); // Match the CSS transition time
2690 };
2691
2692 // This modal holds edits: close only on explicit controls, confirming when dirty.
2693 const guardedClose = mxchatGuardedClose(modal, closeModal);
2694
2695 // Close button handler
2696 const closeBtn = modal.querySelector('.mxchat-modal-close');
2697 if (closeBtn) {
2698 closeBtn.onclick = guardedClose;
2699 }
2700
2701 // Cancel button handler
2702 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
2703 if (cancelBtns) {
2704 cancelBtns.forEach(btn => {
2705 btn.onclick = guardedClose;
2706 });
2707 }
2708
2709 // Escape key to close modal
2710 detachEsc = mxchatBindEscClose(modal, guardedClose);
2711
2712 // Focus appropriate field based on current step
2713 if (isEdit || actionStep2.classList.contains('active')) {
2714 if (labelField) labelField.focus();
2715 } else {
2716 if (searchInput) searchInput.focus();
2717 }
2718
2719 return closeModal; // Return close function for external use
2720 };
2721 // Setup add action buttons
2722 const addActionBtn = document.getElementById('mxchat-add-action-btn');
2723 if (addActionBtn) {
2724 //console.log('Add action button found');
2725 addActionBtn.onclick = () => window.mxchatOpenActionModal();
2726 }
2727
2728 const createFirstAction = document.getElementById('mxchat-create-first-action');
2729 if (createFirstAction) {
2730 //console.log('Create first action button found');
2731 createFirstAction.onclick = () => window.mxchatOpenActionModal();
2732 }
2733
2734 // Setup edit buttons
2735 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
2736 //console.log('Edit buttons found:', editButtons.length);
2737 editButtons.forEach(button => {
2738 button.onclick = () => {
2739 const actionId = button.dataset.actionId;
2740 const phrases = button.dataset.phrases;
2741 const label = button.dataset.label;
2742 const threshold = button.dataset.threshold || 85;
2743 const callbackFunction = button.dataset.callbackFunction;
2744 const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
2745
2746 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
2747 };
2748 });
2749 }
2750 });
2751
2752 jQuery(document).ready(function($) {
2753 // Auto-expand custom post types container if any are checked
2754 // Note: Click handler is in admin-knowledge-page.php inline script (initCustomPostTypesToggle)
2755 function autoExpandIfNeeded() {
2756 const $container = $('#mxchat-custom-post-types-container');
2757 const $toggleBtn = $('#mxchat-custom-post-types-toggle');
2758
2759 if ($container.length === 0) return;
2760
2761 const hasCheckedItems = $container.find('input[type="checkbox"]:checked').length > 0;
2762
2763 if (hasCheckedItems) {
2764 $container.show();
2765 const $icon = $toggleBtn.find('span:last-child');
2766 if ($icon.length) {
2767 $icon.text('');
2768 }
2769 }
2770 }
2771
2772 // Run on page load
2773 autoExpandIfNeeded();
2774 });
2775
2776 document.addEventListener('DOMContentLoaded', function() {
2777 var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
2778 var modal = document.getElementById('mxchatSampleModal');
2779 var modalClose = document.getElementById('mxchatModalClose');
2780 var closeBtn = document.getElementById('mxchatCloseBtn');
2781 var copyBtn = document.getElementById('mxchatCopyBtn');
2782 var instructionsContent = document.querySelector('.mxchat-instructions-content');
2783 var modalContent = document.querySelector('.mxchat-instructions-modal-content');
2784
2785 if (!viewSampleBtn || !modal) {
2786 return;
2787 }
2788
2789 // Open modal
2790 viewSampleBtn.addEventListener('click', function(e) {
2791 e.preventDefault();
2792 e.stopPropagation();
2793 modal.classList.add('mxchat-instructions-show');
2794 });
2795
2796 // Close modal function
2797 function closeModal(e) {
2798 if (e) {
2799 e.preventDefault();
2800 e.stopPropagation();
2801 }
2802 modal.classList.remove('mxchat-instructions-show');
2803 }
2804
2805 // Close modal events
2806 if (modalClose) {
2807 modalClose.addEventListener('click', function(e) {
2808 closeModal(e);
2809 });
2810 }
2811
2812 if (closeBtn) {
2813 closeBtn.addEventListener('click', function(e) {
2814 closeModal(e);
2815 });
2816 }
2817
2818 // Close on backdrop click ONLY (not on hover), and only when the whole gesture
2819 // happened on the backdrop — selecting the sample text and releasing past the
2820 // dialog edge otherwise dispatches the click on the overlay and closes it.
2821 mxchatDragSafeOverlayClose(modal, closeModal);
2822
2823 // Prevent modal content clicks from closing the modal
2824 if (modalContent) {
2825 modalContent.addEventListener('click', function(e) {
2826 e.stopPropagation();
2827 });
2828 }
2829
2830 // Close on escape key
2831 document.addEventListener('keydown', function(e) {
2832 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2833 closeModal();
2834 }
2835 });
2836
2837 // Copy functionality
2838 if (copyBtn && instructionsContent) {
2839 copyBtn.addEventListener('click', function(e) {
2840 e.preventDefault();
2841 e.stopPropagation();
2842
2843 var text = instructionsContent.textContent;
2844
2845 if (navigator.clipboard) {
2846 navigator.clipboard.writeText(text).then(function() {
2847 showCopySuccess();
2848 }).catch(function() {
2849 fallbackCopy(text);
2850 });
2851 } else {
2852 fallbackCopy(text);
2853 }
2854 });
2855 }
2856
2857 function fallbackCopy(text) {
2858 var textArea = document.createElement('textarea');
2859 textArea.value = text;
2860 textArea.style.position = 'fixed';
2861 textArea.style.left = '-999999px';
2862 textArea.style.top = '-999999px';
2863 document.body.appendChild(textArea);
2864 textArea.select();
2865 try {
2866 document.execCommand('copy');
2867 showCopySuccess();
2868 } catch (err) {
2869 console.error('Copy failed');
2870 }
2871 document.body.removeChild(textArea);
2872 }
2873
2874 function showCopySuccess() {
2875 var originalText = copyBtn.innerHTML;
2876 copyBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20,6 9,17 4,12"/></svg>Copied!';
2877
2878 setTimeout(function() {
2879 copyBtn.innerHTML = originalText;
2880 }, 2000);
2881 }
2882 });
2883
2884 jQuery(document).ready(function($) {
2885 var emailDependentFields = [
2886 '#email_blocker_header_content',
2887 '#email_blocker_button_text',
2888 '#enable_name_field',
2889 '#name_field_placeholder'
2890 ];
2891
2892 // Add visual indicators
2893 emailDependentFields.forEach(function(fieldId) {
2894 var $row = $(fieldId).closest('tr');
2895 $row.addClass('email-dependent-field');
2896
2897 // Add an icon to show it's dependent
2898 var $label = $row.find('th label, th');
2899 $label.prepend('<span class="email-dependent-icon" style="color: #0073aa; margin-right: 5px;">↳</span>');
2900 });
2901
2902 // Hide initially with opacity for smoother transition
2903 emailDependentFields.forEach(function(fieldId) {
2904 $(fieldId).closest('tr').hide().css('opacity', '0');
2905 });
2906
2907 function toggleEmailFields() {
2908 var emailEnabled = $('#enable_email_block').is(':checked');
2909
2910 emailDependentFields.forEach(function(fieldId) {
2911 var $row = $(fieldId).closest('tr');
2912 if (emailEnabled) {
2913 $row.slideDown(400).animate({opacity: 1}, 200);
2914 } else {
2915 $row.animate({opacity: 0}, 200).slideUp(400);
2916 }
2917 });
2918 }
2919
2920 toggleEmailFields();
2921 $('#enable_email_block').on('change', toggleEmailFields);
2922 });
2923
2924
2925
2926 //Handle role restriction changes
2927 jQuery(document).ready(function($) {
2928 //Handle role restriction changes for both data sources
2929 $(document).on('change', '.mxchat-role-select', function() {
2930 const $select = $(this);
2931 const entryId = $select.data('entry-id');
2932 const dataSource = $select.data('data-source');
2933 const roleRestriction = $select.val();
2934 const nonce = $select.data('nonce');
2935
2936 // Visual feedback
2937 $select.prop('disabled', true).addClass('updating');
2938
2939 $.ajax({
2940 url: ajaxurl,
2941 type: 'POST',
2942 data: {
2943 action: 'mxchat_update_role_restriction',
2944 nonce: nonce,
2945 entry_id: entryId,
2946 data_source: dataSource, // NEW: Include data source
2947 role_restriction: roleRestriction
2948 },
2949 success: function(response) {
2950 if (response.success) {
2951 // Show success feedback
2952 $select.removeClass('updating').addClass('updated');
2953 setTimeout(() => {
2954 $select.removeClass('updated');
2955 }, 2000);
2956 } else {
2957 alert('Failed to update role restriction: ' + response.data);
2958 // Revert selection
2959 $select.val($select.data('original-value'));
2960 }
2961 },
2962 error: function() {
2963 alert('Error updating role restriction');
2964 // Revert selection
2965 $select.val($select.data('original-value'));
2966 },
2967 complete: function() {
2968 $select.prop('disabled', false);
2969 }
2970 });
2971
2972 // Store original value for potential revert
2973 $select.data('original-value', roleRestriction);
2974 });
2975
2976 // Store initial values
2977 $('.mxchat-role-select').each(function() {
2978 $(this).data('original-value', $(this).val());
2979 });
2980 });
2981
2982 // Bot Selector Handler
2983 jQuery(document).ready(function($) {
2984 var saveTimer;
2985
2986 // Function to update bot_id in forms
2987 function updateBotIdInForm(formSelector) {
2988 var botId = $('#mxchat-bot-selector').val();
2989 var form = $(formSelector);
2990
2991 if (form.length > 0) {
2992 // Remove existing bot_id hidden input
2993 form.find('input[name="bot_id"]').remove();
2994
2995 // Add new bot_id hidden input if not default
2996 if (botId && botId !== 'default') {
2997 form.append('<input type="hidden" name="bot_id" value="' + botId + '">');
2998 }
2999 }
3000 }
3001
3002 $('#mxchat-bot-selector').on('change', function() {
3003 var botId = $(this).val();
3004
3005 // Clear any existing timer
3006 clearTimeout(saveTimer);
3007
3008 // Update all forms with new bot_id when bot selection changes
3009 updateBotIdInForm('#mxchat-url-form');
3010 updateBotIdInForm('#mxchat-content-form');
3011
3012 // Save the selection via AJAX
3013 $.ajax({
3014 url: ajaxurl,
3015 type: 'POST',
3016 data: {
3017 action: 'mxchat_save_selected_bot',
3018 bot_id: botId,
3019 nonce: mxchatAdmin.setting_nonce
3020 },
3021 success: function(response) {
3022 if (response.success) {
3023 // Show saved indicator
3024 $('#mxchat-bot-save-status').fadeIn().delay(2000).fadeOut();
3025
3026 // Reload the page after a short delay to refresh content
3027 saveTimer = setTimeout(function() {
3028 var currentUrl = new URL(window.location.href);
3029 currentUrl.searchParams.set('bot_id', botId);
3030 currentUrl.searchParams.set('page', 'mxchat-prompts');
3031 window.location.href = currentUrl.toString();
3032 }, 500);
3033 }
3034 },
3035 error: function() {
3036 console.error('Failed to save bot selection');
3037 }
3038 });
3039 });
3040
3041 // Initialize forms with current bot_id when the page loads
3042 setTimeout(function() {
3043 updateBotIdInForm('#mxchat-url-form');
3044 updateBotIdInForm('#mxchat-content-form');
3045 }, 100);
3046 });
3047
3048 // API Key Status Indicator for Chat Models
3049 jQuery(document).ready(function($) {
3050 function updateChatModelAPIStatus(apiKeyStatuses) {
3051 var selectedModel = $('#model').val();
3052
3053 // Hide all status messages
3054 $('.mxchat-api-status').hide();
3055
3056 // Return early if no model is selected
3057 if (!selectedModel) {
3058 return;
3059 }
3060
3061 // Map models to providers
3062 var provider = null;
3063
3064 if (selectedModel.startsWith('gpt-')) {
3065 provider = 'openai';
3066 } else if (selectedModel.startsWith('claude-')) {
3067 provider = 'claude';
3068 } else if (selectedModel.startsWith('grok-')) {
3069 provider = 'xai';
3070 } else if (selectedModel.startsWith('deepseek-')) {
3071 provider = 'deepseek';
3072 } else if (selectedModel.startsWith('gemini-')) {
3073 provider = 'gemini';
3074 } else if (selectedModel === 'openrouter') {
3075 provider = 'openrouter';
3076 }
3077
3078 // If we have fresh API key data, update the messages
3079 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
3080 var $statusElement = $('.mxchat-api-status[data-provider="' + provider + '"]');
3081 var hasKey = apiKeyStatuses[provider];
3082
3083 if (hasKey) {
3084 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
3085 } else {
3086 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
3087 }
3088 }
3089
3090 // Show the appropriate status message
3091 if (provider) {
3092 $('.mxchat-api-status[data-provider="' + provider + '"]').show();
3093 }
3094 }
3095
3096 function updateEmbeddingModelAPIStatus(apiKeyStatuses) {
3097 var selectedModel = $('#embedding_model').val();
3098
3099 // Hide all status messages
3100 $('.mxchat-embedding-api-status').hide();
3101
3102 // Custom-provider embeddings in use: the standard picker is inert, so
3103 // its provider key statuses are noise (plan ae02cb).
3104 if ($('#embedding_model').prop('disabled')) {
3105 return;
3106 }
3107
3108 // Return early if no model is selected
3109 if (!selectedModel) {
3110 return;
3111 }
3112
3113 // Map models to providers
3114 var provider = null;
3115
3116 if (selectedModel.startsWith('text-embedding-')) {
3117 provider = 'openai';
3118 } else if (selectedModel.startsWith('voyage-')) {
3119 provider = 'voyage';
3120 } else if (selectedModel.startsWith('gemini-embedding-')) {
3121 provider = 'gemini';
3122 }
3123
3124 // If we have fresh API key data, update the messages
3125 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
3126 var $statusElement = $('.mxchat-embedding-api-status[data-provider="' + provider + '"]');
3127 var hasKey = apiKeyStatuses[provider];
3128
3129 if (hasKey) {
3130 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
3131 } else {
3132 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
3133 }
3134 }
3135
3136 // Show the appropriate status message
3137 if (provider) {
3138 $('.mxchat-embedding-api-status[data-provider="' + provider + '"]').show();
3139 }
3140 }
3141
3142 function getProviderName(provider) {
3143 var names = {
3144 'openai': 'OpenAI',
3145 'claude': 'Anthropic (Claude)',
3146 'xai': 'X.AI (Grok)',
3147 'deepseek': 'DeepSeek',
3148 'gemini': 'Google Gemini',
3149 'openrouter': 'OpenRouter',
3150 'voyage': 'Voyage AI'
3151 };
3152 return names[provider] || provider;
3153 }
3154
3155 function refreshAPIKeyStatus() {
3156 $.ajax({
3157 url: ajaxurl,
3158 type: 'POST',
3159 data: {
3160 action: 'mxchat_check_api_keys',
3161 nonce: mxchatAdmin.setting_nonce
3162 },
3163 success: function(response) {
3164 if (response.success && response.data) {
3165 updateChatModelAPIStatus(response.data);
3166 updateEmbeddingModelAPIStatus(response.data);
3167 }
3168 }
3169 });
3170 }
3171
3172 // Expose refresh function globally so auto-save can call it
3173 window.mxchatRefreshAPIKeyStatus = refreshAPIKeyStatus;
3174
3175 // Run on page load
3176 updateChatModelAPIStatus();
3177 updateEmbeddingModelAPIStatus();
3178
3179 // Check if we just saved settings (WordPress redirects with ?settings-updated=true)
3180 var urlParams = new URLSearchParams(window.location.search);
3181 if (urlParams.get('settings-updated') === 'true') {
3182 // Page was just reloaded after save, fetch fresh API key status
3183 refreshAPIKeyStatus();
3184 }
3185
3186 // Run when model changes
3187 $('#model').on('change', function() {
3188 updateChatModelAPIStatus();
3189 updateWebSearchToggleVisibility();
3190 });
3191 $('#embedding_model').on('change', function() { updateEmbeddingModelAPIStatus(); });
3192
3193 // Web Search toggle visibility based on model
3194 function updateWebSearchToggleVisibility() {
3195 var selectedModel = $('#model').val();
3196 var $wrapper = $('#web-search-toggle-wrapper');
3197 var $unavailableMessage = $('#web-search-unavailable-message');
3198
3199 if (!$wrapper.length) return; // Element doesn't exist
3200
3201 // Get the list of web-search-capable models from data attributes (OpenAI + Gemini)
3202 var openaiModelsAttr = $wrapper.data('openai-models');
3203 var geminiModelsAttr = $wrapper.data('gemini-models');
3204 var unsupportedModelsAttr = $wrapper.data('unsupported-models');
3205
3206 var openaiModels = openaiModelsAttr ? String(openaiModelsAttr).split(',') : [];
3207 var geminiModels = geminiModelsAttr ? String(geminiModelsAttr).split(',') : [];
3208 var unsupportedModels = unsupportedModelsAttr ? String(unsupportedModelsAttr).split(',') : [];
3209
3210 // Check if selected model is an OpenAI or Gemini model that supports web search
3211 var isCapable = openaiModels.includes(selectedModel) || geminiModels.includes(selectedModel);
3212 var isSupported = isCapable && !unsupportedModels.includes(selectedModel);
3213
3214 if (isSupported) {
3215 $wrapper.show();
3216 $unavailableMessage.hide();
3217 } else {
3218 $wrapper.hide();
3219 $unavailableMessage.show();
3220 }
3221 }
3222
3223 // Run on page load
3224 updateWebSearchToggleVisibility();
3225 });
3226
3227 // Transcripts Metrics Dashboard
3228 jQuery(document).ready(function($) {
3229 // Tab switching functionality
3230 $('.mxchat-metrics-tab').on('click', function() {
3231 const tabName = $(this).data('tab');
3232
3233 // Update tab buttons
3234 $('.mxchat-metrics-tab').removeClass('active');
3235 $(this).addClass('active');
3236
3237 // Update panels
3238 $('.mxchat-metrics-panel').removeClass('active');
3239 $(`.mxchat-metrics-panel[data-panel="${tabName}"]`).addClass('active');
3240
3241 // Initialize chart if activity tab is shown
3242 if (tabName === 'activity' && typeof mxchatChartData !== 'undefined') {
3243 // Small delay to ensure the canvas is visible
3244 setTimeout(function() {
3245 initActivityChart();
3246 }, 50);
3247 }
3248 });
3249
3250 // Initialize chart function
3251 function initActivityChart() {
3252 const canvas = document.getElementById('mxchat-activity-chart');
3253 if (!canvas) return;
3254
3255 // Check if chart already exists and destroy it
3256 if (canvas.chartInstance) {
3257 canvas.chartInstance.destroy();
3258 }
3259
3260 const ctx = canvas.getContext('2d');
3261
3262 // Create gradient for chats line
3263 const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
3264 chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
3265 chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
3266
3267 // Create gradient for messages line
3268 const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
3269 messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
3270 messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
3271
3272 // Simple chart without external library
3273 canvas.chartInstance = new SimpleChart(canvas, {
3274 labels: mxchatChartData.labels,
3275 datasets: [
3276 {
3277 label: 'Chats',
3278 data: mxchatChartData.chats,
3279 borderColor: '#667eea',
3280 backgroundColor: chatsGradient,
3281 fill: true
3282 },
3283 {
3284 label: 'Messages',
3285 data: mxchatChartData.messages,
3286 borderColor: '#764ba2',
3287 backgroundColor: messagesGradient,
3288 fill: true
3289 }
3290 ]
3291 });
3292 }
3293
3294 // Simple chart implementation (no external dependencies)
3295 class SimpleChart {
3296 constructor(canvas, config) {
3297 this.canvas = canvas;
3298 this.ctx = canvas.getContext('2d');
3299 this.config = config;
3300 this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
3301 this.render();
3302 }
3303
3304 destroy() {
3305 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3306 }
3307
3308 render() {
3309 const dpr = window.devicePixelRatio || 1;
3310 const rect = this.canvas.getBoundingClientRect();
3311
3312 this.canvas.width = rect.width * dpr;
3313 this.canvas.height = rect.height * dpr;
3314 this.ctx.scale(dpr, dpr);
3315
3316 this.canvas.style.width = rect.width + 'px';
3317 this.canvas.style.height = rect.height + 'px';
3318
3319 const width = rect.width - this.padding.left - this.padding.right;
3320 const height = rect.height - this.padding.top - this.padding.bottom;
3321
3322 // Find max value
3323 let maxValue = 0;
3324 this.config.datasets.forEach(dataset => {
3325 const max = Math.max(...dataset.data);
3326 if (max > maxValue) maxValue = max;
3327 });
3328
3329 // Add some padding to max value
3330 maxValue = Math.ceil(maxValue * 1.1);
3331 if (maxValue === 0) maxValue = 10;
3332
3333 // Draw grid lines
3334 this.ctx.strokeStyle = '#e5e7eb';
3335 this.ctx.lineWidth = 1;
3336 const gridLines = 5;
3337
3338 for (let i = 0; i <= gridLines; i++) {
3339 const y = this.padding.top + (height / gridLines) * i;
3340 this.ctx.beginPath();
3341 this.ctx.moveTo(this.padding.left, y);
3342 this.ctx.lineTo(this.padding.left + width, y);
3343 this.ctx.stroke();
3344
3345 // Draw y-axis labels
3346 const value = maxValue - (maxValue / gridLines) * i;
3347 this.ctx.fillStyle = '#6b7280';
3348 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3349 this.ctx.textAlign = 'right';
3350 this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
3351 }
3352
3353 // Draw datasets
3354 this.config.datasets.forEach(dataset => {
3355 const points = [];
3356 const xStep = width / (this.config.labels.length - 1 || 1);
3357
3358 dataset.data.forEach((value, index) => {
3359 const x = this.padding.left + (xStep * index);
3360 const y = this.padding.top + height - (value / maxValue * height);
3361 points.push({ x, y, value });
3362 });
3363
3364 // Draw filled area
3365 if (dataset.fill && dataset.backgroundColor) {
3366 this.ctx.fillStyle = dataset.backgroundColor;
3367 this.ctx.beginPath();
3368 this.ctx.moveTo(points[0].x, this.padding.top + height);
3369 points.forEach(point => {
3370 this.ctx.lineTo(point.x, point.y);
3371 });
3372 this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
3373 this.ctx.closePath();
3374 this.ctx.fill();
3375 }
3376
3377 // Draw line
3378 this.ctx.strokeStyle = dataset.borderColor;
3379 this.ctx.lineWidth = 3;
3380 this.ctx.lineCap = 'round';
3381 this.ctx.lineJoin = 'round';
3382
3383 this.ctx.beginPath();
3384 points.forEach((point, index) => {
3385 if (index === 0) {
3386 this.ctx.moveTo(point.x, point.y);
3387 } else {
3388 this.ctx.lineTo(point.x, point.y);
3389 }
3390 });
3391 this.ctx.stroke();
3392
3393 // Draw points
3394 points.forEach(point => {
3395 this.ctx.fillStyle = '#ffffff';
3396 this.ctx.beginPath();
3397 this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
3398 this.ctx.fill();
3399 this.ctx.strokeStyle = dataset.borderColor;
3400 this.ctx.lineWidth = 2;
3401 this.ctx.stroke();
3402 });
3403 });
3404
3405 // Draw x-axis labels
3406 const xStep = width / (this.config.labels.length - 1 || 1);
3407 this.ctx.fillStyle = '#6b7280';
3408 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3409 this.ctx.textAlign = 'center';
3410
3411 this.config.labels.forEach((label, index) => {
3412 const x = this.padding.left + (xStep * index);
3413 this.ctx.fillText(label, x, this.padding.top + height + 20);
3414 });
3415
3416 // Draw legend
3417 let legendX = this.padding.left;
3418 const legendY = rect.height - 10;
3419
3420 this.config.datasets.forEach((dataset, index) => {
3421 // Color box
3422 this.ctx.fillStyle = dataset.borderColor;
3423 this.ctx.fillRect(legendX, legendY - 8, 12, 12);
3424
3425 // Label
3426 this.ctx.fillStyle = '#374151';
3427 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3428 this.ctx.textAlign = 'left';
3429 this.ctx.fillText(dataset.label, legendX + 18, legendY);
3430
3431 legendX += this.ctx.measureText(dataset.label).width + 40;
3432 });
3433 }
3434 }
3435
3436 // Initialize chart on page load if we're on the activity tab
3437 if ($('.mxchat-metrics-tab.active').data('tab') === 'activity' && typeof mxchatChartData !== 'undefined') {
3438 setTimeout(function() {
3439 initActivityChart();
3440 }, 100);
3441 }
3442
3443 // ========================================
3444 // SLACK TEST CONNECTION
3445 // ========================================
3446 $('#mxchat-test-slack-connection').on('click', function() {
3447 var $button = $(this);
3448 var $result = $('#mxchat-slack-test-result');
3449 var originalText = $button.html();
3450
3451 // Show loading state
3452 $button.prop('disabled', true).html(
3453 '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 8px; animation: spin 1s linear infinite;"><circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"/></svg>' +
3454 'Testing...'
3455 );
3456
3457 $.ajax({
3458 url: ajaxurl,
3459 type: 'POST',
3460 data: {
3461 action: 'mxchat_test_slack_connection',
3462 nonce: mxchatAdmin.nonce
3463 },
3464 success: function(response) {
3465 $result.show();
3466 if (response.success) {
3467 $result.html(
3468 '<div style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 16px; border-radius: 8px;">' +
3469 '<strong style="display: block; margin-bottom: 8px;">✓ Connection Successful!</strong>' +
3470 '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3471 '</div>'
3472 );
3473 } else {
3474 var bgColor = response.data.partial ? '#fff3cd' : '#f8d7da';
3475 var borderColor = response.data.partial ? '#ffeeba' : '#f5c6cb';
3476 var textColor = response.data.partial ? '#856404' : '#721c24';
3477 var icon = response.data.partial ? '' : '';
3478 var title = response.data.partial ? 'Partial Success - Missing Scopes' : 'Connection Failed';
3479
3480 $result.html(
3481 '<div style="background: ' + bgColor + '; border: 1px solid ' + borderColor + '; color: ' + textColor + '; padding: 16px; border-radius: 8px;">' +
3482 '<strong style="display: block; margin-bottom: 8px;">' + icon + ' ' + title + '</strong>' +
3483 '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3484 (response.data.missing_scopes ? '<p style="margin: 12px 0 0; font-size: 13px;">Add these scopes in your Slack app settings and reinstall the app.</p>' : '') +
3485 '</div>'
3486 );
3487 }
3488 },
3489 error: function() {
3490 $result.show().html(
3491 '<div style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 16px; border-radius: 8px;">' +
3492 '<strong>✗ Request Failed</strong><br>Could not connect to the server. Please try again.' +
3493 '</div>'
3494 );
3495 },
3496 complete: function() {
3497 $button.prop('disabled', false).html(originalText);
3498 }
3499 });
3500 });
3501
3502 // Helper function to escape HTML
3503 function escapeHtml(text) {
3504 var div = document.createElement('div');
3505 div.appendChild(document.createTextNode(text));
3506 return div.innerHTML;
3507 }
3508
3509 // ========================================
3510 // DEBUG & OPTIMIZATION TOOLS
3511 // ========================================
3512
3513 // Debug Mode Toggle
3514 $('#mxchat_debug_mode').on('change', function() {
3515 var $toggle = $(this);
3516 var enabled = $toggle.is(':checked') ? 'on' : 'off';
3517 var $label = $toggle.closest('label');
3518
3519 // Add loading indicator next to the toggle label
3520 var $indicator = $label.find('.mxchat-save-indicator');
3521 if ($indicator.length === 0) {
3522 $indicator = $('<span class="mxchat-save-indicator" style="margin-left: 10px;"></span>');
3523 $label.append($indicator);
3524 }
3525 $indicator.html('<span class="mxchat-saving-spinner"></span>').show();
3526
3527 $.ajax({
3528 url: ajaxurl,
3529 type: 'POST',
3530 data: {
3531 action: 'mxchat_toggle_debug_mode',
3532 enabled: enabled,
3533 _ajax_nonce: mxchatAdmin.setting_nonce
3534 },
3535 success: function(response) {
3536 if (response.success) {
3537 // Show success indicator
3538 $indicator.html('<span class="mxchat-save-success">✓</span>');
3539 setTimeout(function() {
3540 $indicator.fadeOut(300);
3541 }, 2000);
3542
3543 // Always refresh the log after toggling
3544 refreshDebugLog();
3545 } else {
3546 $indicator.html('<span class="mxchat-save-error">✗</span>');
3547 alert(response.data.message || 'Error toggling debug mode');
3548 $toggle.prop('checked', !$toggle.is(':checked'));
3549 }
3550 },
3551 error: function() {
3552 $indicator.html('<span class="mxchat-save-error">✗</span>');
3553 alert('Error toggling debug mode');
3554 $toggle.prop('checked', !$toggle.is(':checked'));
3555 }
3556 });
3557 });
3558
3559 // Refresh Debug Log
3560 function refreshDebugLog() {
3561 var $container = $('#mxchat-debug-log');
3562 var $countBadge = $('#mxchat-log-count');
3563
3564 $.ajax({
3565 url: ajaxurl,
3566 type: 'POST',
3567 data: {
3568 action: 'mxchat_get_debug_log',
3569 _ajax_nonce: mxchatAdmin.setting_nonce
3570 },
3571 success: function(response) {
3572 if (response.success) {
3573 var log = response.data.log;
3574 var count = response.data.count;
3575
3576 if (count > 0) {
3577 $countBadge.text(count + ' entries').show();
3578 var html = '<div class="mxchat-debug-log-entries">';
3579 log.forEach(function(entry) {
3580 var typeClass = 'mxchat-log-type-' + entry.type;
3581 var typeLabel = entry.type.replace(/_/g, ' ').toUpperCase();
3582 html += '<div class="mxchat-debug-log-entry ' + typeClass + '">';
3583 html += '<div class="mxchat-log-header">';
3584 html += '<span class="mxchat-log-type">' + escapeHtml(typeLabel) + '</span>';
3585 html += '<span class="mxchat-log-time">' + escapeHtml(entry.time) + '</span>';
3586 html += '</div>';
3587 html += '<div class="mxchat-log-message">' + escapeHtml(entry.message) + '</div>';
3588 if (entry.data) {
3589 html += '<div class="mxchat-log-data">' + escapeHtml(JSON.stringify(entry.data, null, 2)) + '</div>';
3590 }
3591 html += '</div>';
3592 });
3593 html += '</div>';
3594 $container.html(html);
3595 } else {
3596 $countBadge.hide();
3597 $container.html(
3598 '<div class="mxchat-debug-log-empty">' +
3599 '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" style="opacity: 0.3;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' +
3600 '<p>No log entries yet. Enable debug mode to start logging.</p>' +
3601 '</div>'
3602 );
3603 }
3604 }
3605 }
3606 });
3607 }
3608
3609 // Load debug log on page load
3610 if ($('#mxchat-debug-log').length) {
3611 refreshDebugLog();
3612 }
3613
3614 // Refresh Log Button
3615 $('#mxchat-refresh-log').on('click', function() {
3616 var $btn = $(this);
3617 $btn.prop('disabled', true);
3618 refreshDebugLog();
3619 setTimeout(function() {
3620 $btn.prop('disabled', false);
3621 }, 500);
3622 });
3623
3624 // Clear Log Button
3625 $('#mxchat-clear-log').on('click', function() {
3626 if (!confirm('Are you sure you want to clear the debug log?')) {
3627 return;
3628 }
3629
3630 var $btn = $(this);
3631 $btn.prop('disabled', true);
3632
3633 $.ajax({
3634 url: ajaxurl,
3635 type: 'POST',
3636 data: {
3637 action: 'mxchat_clear_debug_log',
3638 _ajax_nonce: mxchatAdmin.setting_nonce
3639 },
3640 success: function(response) {
3641 if (response.success) {
3642 refreshDebugLog();
3643 } else {
3644 alert(response.data.message || 'Error clearing log');
3645 }
3646 },
3647 error: function() {
3648 alert('Error clearing log');
3649 },
3650 complete: function() {
3651 $btn.prop('disabled', false);
3652 }
3653 });
3654 });
3655
3656 // Export Settings Button
3657 $('#mxchat-export-settings').on('click', function() {
3658 var $btn = $(this);
3659 var originalHtml = $btn.html();
3660 $btn.prop('disabled', true).html(
3661 '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation: spin 1s linear infinite;"><circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"/></svg> Exporting...'
3662 );
3663
3664 $.ajax({
3665 url: ajaxurl,
3666 type: 'POST',
3667 data: {
3668 action: 'mxchat_export_settings',
3669 _ajax_nonce: mxchatAdmin.setting_nonce
3670 },
3671 success: function(response) {
3672 if (response.success) {
3673 // Create and download the JSON file
3674 var dataStr = JSON.stringify(response.data.settings, null, 2);
3675 var blob = new Blob([dataStr], { type: 'application/json' });
3676 var url = URL.createObjectURL(blob);
3677 var a = document.createElement('a');
3678 a.href = url;
3679 a.download = response.data.filename;
3680 document.body.appendChild(a);
3681 a.click();
3682 document.body.removeChild(a);
3683 URL.revokeObjectURL(url);
3684 } else {
3685 alert(response.data.message || 'Error exporting settings');
3686 }
3687 },
3688 error: function() {
3689 alert('Error exporting settings');
3690 },
3691 complete: function() {
3692 $btn.prop('disabled', false).html(originalHtml);
3693 }
3694 });
3695 });
3696
3697 // Reset Settings Modal
3698 var $resetModal = $('#mxchat-reset-modal');
3699 var $resetConfirmInput = $('#mxchat-reset-confirmation');
3700 var $resetConfirmBtn = $('#mxchat-reset-confirm');
3701
3702 $('#mxchat-reset-settings').on('click', function() {
3703 $resetModal.fadeIn(200);
3704 $resetConfirmInput.val('').focus();
3705 $resetConfirmBtn.prop('disabled', true);
3706 });
3707
3708 $('#mxchat-reset-modal-close, #mxchat-reset-cancel, .mxch-modal-backdrop').on('click', function() {
3709 $resetModal.fadeOut(200);
3710 });
3711
3712 $resetConfirmInput.on('input', function() {
3713 var value = $(this).val().toUpperCase();
3714 $resetConfirmBtn.prop('disabled', value !== 'RESET');
3715 });
3716
3717 $resetConfirmBtn.on('click', function() {
3718 var $btn = $(this);
3719 var confirmation = $resetConfirmInput.val();
3720
3721 $btn.prop('disabled', true).text('Resetting...');
3722
3723 $.ajax({
3724 url: ajaxurl,
3725 type: 'POST',
3726 data: {
3727 action: 'mxchat_reset_all_settings',
3728 confirmation: confirmation,
3729 _ajax_nonce: mxchatAdmin.setting_nonce
3730 },
3731 success: function(response) {
3732 if (response.success) {
3733 alert(response.data.message);
3734 window.location.reload();
3735 } else {
3736 alert(response.data.message || 'Error resetting settings');
3737 $btn.prop('disabled', false).text('Reset All Settings');
3738 }
3739 },
3740 error: function() {
3741 alert('Error resetting settings');
3742 $btn.prop('disabled', false).text('Reset All Settings');
3743 }
3744 });
3745 });
3746
3747 // Load debug log on page load if we're on the optimization section
3748 if ($('#optimization').hasClass('active') || window.location.hash === '#optimization') {
3749 refreshDebugLog();
3750 }
3751
3752 // Also refresh when switching to optimization tab
3753 $(document).on('click', '[data-section="optimization"]', function() {
3754 setTimeout(refreshDebugLog, 100);
3755 });
3756 });
3757
3758 // Global rate-limit usage: "Reset counter" button (plan-mxchat-20260603-e9b3f9)
3759 jQuery(document).ready(function($) {
3760 var $usage = $('#mxch-global-usage');
3761 if (!$usage.length) {
3762 return;
3763 }
3764
3765 $('#mxch-global-usage-reset').on('click', function() {
3766 var $btn = $(this);
3767 if (!window.confirm('Reset the global usage counter to zero now? This clears how many messages have been used in the current window.')) {
3768 return;
3769 }
3770
3771 var original = $btn.text();
3772 $btn.prop('disabled', true).text('Resetting…');
3773
3774 $.ajax({
3775 url: mxchatAdmin.ajax_url,
3776 type: 'POST',
3777 data: {
3778 action: 'mxchat_reset_global_rate_limit',
3779 bot_id: $usage.data('bot-id') || 'default',
3780 _ajax_nonce: $usage.data('reset-nonce')
3781 },
3782 success: function(response) {
3783 $btn.prop('disabled', false).text(original);
3784 if (response && response.success) {
3785 $('#mxch-global-usage-fill').css('width', (response.data.pct || 0) + '%');
3786 if (response.data.text) {
3787 $('#mxch-global-usage-text').text(response.data.text);
3788 }
3789 } else {
3790 window.alert((response && response.data && response.data.message) || 'Reset failed. Please try again.');
3791 }
3792 },
3793 error: function() {
3794 $btn.prop('disabled', false).text(original);
3795 window.alert('Reset failed. Please try again.');
3796 }
3797 });
3798 });
3799 });
3800 // ─── Unsaved-edit guard (plan 7787f8) ────────────────────────────────────
3801 // Autosave fires on `change`, which for text fields means blur — an edit made
3802 // with the cursor still in the box is unsaved until the user clicks out. Two
3803 // purely additive affordances close the loss window without touching how or
3804 // when saves fire: a persistent "Unsaved" badge on the field's label while its
3805 // value differs from the last-saved value, and a beforeunload prompt armed
3806 // only while some field is dirty. Baselines re-sync by observing the existing
3807 // autosave requests via ajaxSuccess — the save path itself is not modified.
3808 jQuery(document).ready(function($) {
3809 let $sections = $('.mxchat-autosave-section');
3810 const $pinecone = $('#mxchat-kb-tab-pinecone');
3811 if ($pinecone.length) {
3812 $sections = $sections.add($pinecone);
3813 }
3814 if (!$sections.length) return; // not a MxChat settings screen
3815
3816 const unsavedLabel = (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.unsaved_label)
3817 ? mxchatAdmin.unsaved_label
3818 : 'Unsaved';
3819
3820 function fieldValue($f) {
3821 const type = $f.attr('type');
3822 if (type === 'checkbox' || type === 'radio') {
3823 return $f.is(':checked') ? '1' : '0';
3824 }
3825 const v = $f.val();
3826 return v == null ? '' : String(v);
3827 }
3828
3829 function trackable($f) {
3830 if (!$f.attr('name')) return false;
3831 if ($f.attr('type') === 'hidden') return false;
3832 if ($f.is('#model, #openrouter_selected_model, .mxchat-la-field, select[multiple]')) return false;
3833 return true;
3834 }
3835
3836 // name -> last value confirmed persisted. Captured on the user's FIRST
3837 // focus of a field, not at page load: other ready/async code (model
3838 // pickers, key masks, slider inits) mutates values after load, and a
3839 // load-time snapshot reads those programmatic fills as "unsaved edits"
3840 // and false-arms the guard at rest. An untouched field cannot hold an
3841 // unsaved user edit — same insight as the autosave path's own
3842 // userModifiedFields tracking.
3843 const baseline = new Map();
3844 $sections.on('focusin', 'input, textarea, select', function() {
3845 const $f = $(this);
3846 if (!trackable($f)) return;
3847 const name = $f.attr('name');
3848 if (!baseline.has(name)) {
3849 baseline.set(name, fieldValue($f));
3850 }
3851 });
3852
3853 function isDirty($f) {
3854 const name = $f.attr('name');
3855 return baseline.has(name) && fieldValue($f) !== baseline.get(name);
3856 }
3857
3858 function syncBadge($f) {
3859 const $wrapper = $f.closest('.mxch-field');
3860 const $home = $wrapper.length ? $wrapper.find('.mxch-field-label').first() : $();
3861 let $badge = $home.length
3862 ? $home.children('.mxchat-unsaved-badge')
3863 : $f.nextAll('.mxchat-unsaved-badge').first();
3864 if (isDirty($f)) {
3865 if (!$badge.length) {
3866 $badge = $('<span class="mxchat-unsaved-badge"></span>').text(unsavedLabel);
3867 if ($home.length) {
3868 $home.append($badge);
3869 } else {
3870 $f.after($badge);
3871 }
3872 }
3873 } else {
3874 $badge.remove();
3875 }
3876 }
3877
3878 // Persistent marker only for free-text fields — toggles and selects save on
3879 // the same interaction that changes them; the transient spinner covers those.
3880 const textTypes = ['text', 'number', 'url', 'email', 'password', 'search', 'tel'];
3881 $sections.on('input change', 'input, textarea, select', function() {
3882 const $f = $(this);
3883 if (!trackable($f)) return;
3884 const textLike = $f.is('textarea') || textTypes.indexOf(($f.attr('type') || '').toLowerCase()) !== -1;
3885 if (textLike) {
3886 syncBadge($f);
3887 }
3888 });
3889
3890 // A successful autosave round-trip re-baselines the field it saved. The
3891 // baseline takes the value the request actually SENT, so edits made while
3892 // the save was in flight keep the field dirty and the guard armed.
3893 $(document).ajaxSuccess(function(event, xhr, settings) {
3894 if (!settings || typeof settings.data !== 'string') return;
3895 if (settings.data.indexOf('action=mxchat_save_setting') === -1 &&
3896 settings.data.indexOf('action=mxchat_save_prompts_setting') === -1) {
3897 return;
3898 }
3899 let params;
3900 try { params = new URLSearchParams(settings.data); } catch (err) { return; }
3901 const name = params.get('name');
3902 if (!name || !baseline.has(name)) return;
3903 if (!xhr || !xhr.responseJSON || xhr.responseJSON.success !== true) return;
3904 let saved = params.get('value');
3905 saved = saved == null ? '' : String(saved);
3906 // Checkboxes go over the wire as on/off (or 1/0); normalize to 1/0.
3907 if (saved === 'on') saved = '1';
3908 if (saved === 'off') saved = '0';
3909 baseline.set(name, saved);
3910 const $f = $sections.find('[name="' + name.replace(/"/g, '\\"') + '"]').first();
3911 if ($f.length) {
3912 syncBadge($f);
3913 }
3914 });
3915
3916 // Navigation guard — recomputed per-field at the moment of leaving, so it
3917 // arms only when a tracked value genuinely differs from last-saved.
3918 window.addEventListener('beforeunload', function(e) {
3919 let dirty = false;
3920 $sections.find('input, textarea, select').each(function() {
3921 const $f = $(this);
3922 if (trackable($f) && isDirty($f)) {
3923 dirty = true;
3924 return false;
3925 }
3926 });
3927 if (dirty) {
3928 e.preventDefault();
3929 e.returnValue = '';
3930 return '';
3931 }
3932 });
3933
3934 // ─── sendBeacon save-on-exit (plan 18fd68) ───────────────────────────
3935 // The prompt above only warns — the user can click "Leave", and browsers
3936 // skip the dialog entirely without a prior user gesture. sendBeacon
3937 // survives page teardown by design, so each dirty tracked field is also
3938 // posted to the same autosave endpoint on the way out. Bound to pagehide
3939 // ONLY: it fires once per real teardown, after the leave dialog resolves,
3940 // so a cancelled leave never posts and no sent-once flag is needed (a
3941 // beforeunload beacon would fire before the user answers the dialog).
3942 // Fire-and-forget: baseline and badge stay untouched — if the beacon
3943 // lands the server persists it; if the user returns via bfcache the
3944 // field is still tracked dirty and the normal flow continues.
3945 const BEACON_MAX_FIELDS = 8; // a pathological page state must not machine-gun admin-ajax
3946 const BEACON_MAX_VALUE = 60000; // sendBeacon's queue budget is ~64KB; the prompt covered oversized edits
3947
3948 function beaconRoute(name) {
3949 // Mirror of the autosave action routing above — prompts-page fields
3950 // go to mxchat_save_prompts_setting with its own nonce and URL.
3951 const prompts = name.indexOf('mxchat_prompts_options') !== -1 ||
3952 name.indexOf('mxchat_auto_sync_') === 0 ||
3953 name.indexOf('mxchat_pinecone_addon_options') !== -1 ||
3954 name.indexOf('mxchat_chunk') === 0 ||
3955 name.indexOf('mxchat_acf_field_') === 0 ||
3956 name === 'mxchat_custom_meta_whitelist';
3957 if (prompts) {
3958 if (typeof mxchatPromptsAdmin === 'undefined') return null;
3959 return {
3960 url: mxchatPromptsAdmin.ajax_url,
3961 action: 'mxchat_save_prompts_setting',
3962 nonce: mxchatPromptsAdmin.prompts_setting_nonce
3963 };
3964 }
3965 if (typeof mxchatAdmin === 'undefined') return null;
3966 return {
3967 url: mxchatAdmin.ajax_url,
3968 action: 'mxchat_save_setting',
3969 nonce: mxchatAdmin.setting_nonce
3970 };
3971 }
3972
3973 function beaconWireValue($f) {
3974 // Wire format matches the autosave path, not fieldValue()'s 1/0
3975 // dirty-compare normalization: checkboxes post on/off (Pinecone's
3976 // post 1/0), everything else posts val().
3977 if ($f.attr('type') === 'checkbox') {
3978 if (($f.attr('name') || '').indexOf('mxchat_pinecone_addon_options') !== -1) {
3979 return $f.is(':checked') ? '1' : '0';
3980 }
3981 return $f.is(':checked') ? 'on' : 'off';
3982 }
3983 const v = $f.val();
3984 return v == null ? '' : String(v);
3985 }
3986
3987 window.addEventListener('pagehide', function() {
3988 if (!navigator.sendBeacon) return;
3989 let sent = 0;
3990 $sections.find('input, textarea, select').each(function() {
3991 const $f = $(this);
3992 if (!trackable($f) || !isDirty($f)) return;
3993 // An unchecked radio is "dirty" versus its baseline but its val()
3994 // is the wrong group value to persist — the checked sibling (if
3995 // dirty itself) carries the group's real state.
3996 if ($f.attr('type') === 'radio' && !$f.is(':checked')) return;
3997 const name = $f.attr('name');
3998 const route = beaconRoute(name);
3999 if (!route) return;
4000 const value = beaconWireValue($f);
4001 if (value.length > BEACON_MAX_VALUE) return;
4002 const fd = new FormData();
4003 fd.append('action', route.action);
4004 fd.append('name', name);
4005 fd.append('value', value);
4006 fd.append('_ajax_nonce', route.nonce);
4007 if (navigator.sendBeacon(route.url, fd)) {
4008 sent++;
4009 }
4010 if (sent >= BEACON_MAX_FIELDS) return false;
4011 });
4012 });
4013 });
4014