PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.15
MxChat – AI Chatbot & Content Generation for WordPress v3.2.15
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.15, at js/mxchat-admin.js

3,756 lines 172.4 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.1-chat-latest', label: 'GPT-5.1 Chat Latest', description: 'Recommended for most use cases' },
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-0709', label: 'Grok 4', description: 'Latest flagship model' },
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 $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
1165 $('#mxchat_model_selector_modal').hide();
1166 });
1167
1168 $('.mxchat-model-category-btn').on('click', function() {
1169 $('.mxchat-model-category-btn').removeClass('active');
1170 $(this).addClass('active');
1171 const category = $(this).data('category');
1172 const searchTerm = $('#mxchat_model_search_input').val();
1173 window.populateModelsGrid(searchTerm, category);
1174 });
1175
1176 $('#mxchat_model_search_input').on('input', function() {
1177 const searchTerm = $(this).val();
1178 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1179 window.populateModelsGrid(searchTerm, activeCategory);
1180 });
1181
1182 $(document).on('click', '.mxchat-model-selector-card', function() {
1183 const modelValue = $(this).data('value');
1184 const $modelSelect = $('#model');
1185 const $clickedCard = $(this);
1186
1187 // Check if OpenRouter was selected
1188 if (modelValue === 'openrouter') {
1189 // Load OpenRouter models instead of closing
1190 loadOpenRouterModels();
1191 } else {
1192 // Remove selection from all other cards
1193 $('.mxchat-model-selector-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1194
1195 // Add selection to clicked card immediately for instant feedback
1196 $clickedCard.addClass('mxchat-model-selected');
1197 if ($clickedCard.find('.mxchat-model-selector-checkmark').length === 0) {
1198 $clickedCard.append('<div class="mxchat-model-selector-checkmark">✓</div>');
1199 }
1200
1201 // Brief delay to show the selection, then start saving
1202 setTimeout(function() {
1203 // Normal model selection
1204 $modelSelect.val(modelValue).trigger('change');
1205
1206 // Show loading state on the card
1207 $clickedCard.css('pointer-events', 'none');
1208 const originalContent = $clickedCard.find('.mxchat-model-selector-title').html();
1209 $clickedCard.find('.mxchat-model-selector-title').html(
1210 '<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...'
1211 );
1212
1213 // Manually save the model via AJAX
1214 jQuery.ajax({
1215 url: mxchatAdmin.ajax_url,
1216 type: 'POST',
1217 data: {
1218 action: 'mxchat_save_setting',
1219 name: 'model',
1220 value: modelValue,
1221 _ajax_nonce: mxchatAdmin.setting_nonce
1222 },
1223 success: function(response) {
1224 if (response.success) {
1225 // Show success state
1226 $clickedCard.find('.mxchat-model-selector-title').html(
1227 '<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!'
1228 );
1229
1230 // Update button text after successful save
1231 const selectedModelText = $modelSelect.find('option:selected').text();
1232 $('#mxchat_model_selector_btn').text(selectedModelText);
1233
1234 // Close modal after a short delay to show the success message
1235 setTimeout(function() {
1236 $('#mxchat_model_selector_modal').hide();
1237 // Restore original content and remove selection for next time
1238 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1239 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1240 $clickedCard.css('pointer-events', 'auto');
1241 }, 600);
1242 } else {
1243 // Show error state
1244 $clickedCard.find('.mxchat-model-selector-title').html(
1245 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1246 );
1247
1248 setTimeout(function() {
1249 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1250 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1251 $clickedCard.css('pointer-events', 'auto');
1252 }, 1500);
1253 }
1254 },
1255 error: function() {
1256 // Show error state
1257 $clickedCard.find('.mxchat-model-selector-title').html(
1258 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1259 );
1260
1261 setTimeout(function() {
1262 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1263 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1264 $clickedCard.css('pointer-events', 'auto');
1265 }, 1500);
1266 }
1267 });
1268 }, 300); // 300ms delay to show the selection before starting to save
1269 }
1270 });
1271
1272 // Close modal when clicking outside
1273 $(window).on('click', function(event) {
1274 if ($(event.target).is('#mxchat_model_selector_modal')) {
1275 $('#mxchat_model_selector_modal').hide();
1276 }
1277 });
1278 }
1279
1280 function loadOpenRouterModels() {
1281 const apiKey = $('#openrouter_api_key').val(); // This line already re-checks the field
1282 const $modal = $('#mxchat_model_selector_modal');
1283 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1284
1285 if (!apiKey || apiKey.trim() === '') {
1286 // Show error message in modal
1287 $modalBody.html(`
1288 <div style="text-align: center; padding: 40px;">
1289 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1290 <h3>OpenRouter API Key Required</h3>
1291 <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>
1292 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1293 </div>
1294 `);
1295
1296 $('#mxchat_back_to_models').on('click', function(e) {
1297 e.preventDefault();
1298 // CHANGE THIS: Instead of reloading, restore the original modal content
1299 $modalBody.html(`
1300 <div class="mxchat-model-selector-search-container">
1301 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1302 </div>
1303 <div class="mxchat-model-selector-categories">
1304 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1305 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1306 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1307 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1308 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1309 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1310 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1311 </div>
1312 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1313 `);
1314
1315 // Re-populate the grid
1316 populateModelsGrid('', 'all');
1317
1318 // Re-bind event handlers
1319 rebindModalEventHandlers();
1320 });
1321 return;
1322 }
1323
1324 // Show loading state
1325 $modalBody.html(`
1326 <div style="text-align: center; padding: 60px 20px;">
1327 <div class="spinner is-active" style="float: none; margin: 0 auto 20px;"></div>
1328 <h3>Loading OpenRouter Models...</h3>
1329 <p>Fetching available models from OpenRouter</p>
1330 </div>
1331 `);
1332
1333 // Fetch models from OpenRouter
1334 jQuery.ajax({
1335 url: mxchatAdmin.ajax_url,
1336 type: 'POST',
1337 data: {
1338 action: 'mxchat_fetch_openrouter_models',
1339 api_key: apiKey,
1340 nonce: mxchatAdmin.fetch_openrouter_models_nonce
1341 },
1342 success: function(response) {
1343 if (response.success && response.data.models) {
1344 displayOpenRouterModels(response.data.models);
1345 } else {
1346 $modalBody.html(`
1347 <div style="text-align: center; padding: 40px;">
1348 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1349 <h3>Error Loading Models</h3>
1350 <p>${response.data.message || 'Failed to load models from OpenRouter'}</p>
1351 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1352 </div>
1353 `);
1354
1355 $('#mxchat_back_to_models').on('click', function(e) {
1356 e.preventDefault();
1357 // CHANGE THIS: Restore original content instead of reloading
1358 restoreOriginalModalContent();
1359 });
1360 }
1361 },
1362 error: function() {
1363 $modalBody.html(`
1364 <div style="text-align: center; padding: 40px;">
1365 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1366 <h3>Connection Error</h3>
1367 <p>Failed to connect to OpenRouter. Please check your API key and try again.</p>
1368 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1369 </div>
1370 `);
1371
1372 $('#mxchat_back_to_models').on('click', function(e) {
1373 e.preventDefault();
1374 // CHANGE THIS: Restore original content instead of reloading
1375 restoreOriginalModalContent();
1376 });
1377 }
1378 });
1379 }
1380 function restoreOriginalModalContent() {
1381 const $modalBody = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-body');
1382
1383 $modalBody.html(`
1384 <div class="mxchat-model-selector-search-container">
1385 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1386 </div>
1387 <div class="mxchat-model-selector-categories">
1388 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1389 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1390 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1391 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1392 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1393 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1394 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1395 </div>
1396 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1397 `);
1398
1399 // Re-populate the grid
1400 window.populateModelsGrid('', 'all');
1401
1402 // Re-bind event handlers
1403 rebindModalEventHandlers();
1404 }
1405 function rebindModalEventHandlers() {
1406 const $modal = $('#mxchat_model_selector_modal');
1407
1408 // Re-bind category button clicks
1409 $('.mxchat-model-category-btn').off('click').on('click', function() {
1410 $('.mxchat-model-category-btn').removeClass('active');
1411 $(this).addClass('active');
1412 const category = $(this).data('category');
1413 const searchTerm = $('#mxchat_model_search_input').val();
1414 window.populateModelsGrid(searchTerm, category);
1415 });
1416
1417 // Re-bind search input
1418 $('#mxchat_model_search_input').off('input').on('input', function() {
1419 const searchTerm = $(this).val();
1420 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1421 populateModelsGrid(searchTerm, activeCategory);
1422 });
1423 }
1424
1425 function restoreDefaultModalFooter() {
1426 const $modalFooter = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-footer');
1427
1428 // Restore default footer buttons
1429 $modalFooter.html(`
1430 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
1431 `);
1432
1433 // Re-bind cancel button
1434 $('#mxchat_cancel_model_selection').on('click', function() {
1435 $('#mxchat_model_selector_modal').hide();
1436 });
1437 }
1438
1439 function displayOpenRouterModels(models) {
1440 const $modal = $('#mxchat_model_selector_modal');
1441 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1442 const $modalFooter = $modal.find('.mxchat-model-selector-modal-footer');
1443 const currentSelected = $('#openrouter_selected_model').val();
1444
1445 // Variable to store the currently selected model (in the UI, not yet saved)
1446 let pendingSelection = {
1447 modelId: currentSelected || null,
1448 modelName: $('#openrouter_selected_model_name').val() || null
1449 };
1450
1451 // Build new modal content with search and models
1452 const newContent = `
1453 <div class="mxchat-model-selector-search-container">
1454 <input type="text" id="mxchat_openrouter_search" class="mxchat-model-search-input" placeholder="Search OpenRouter models...">
1455 <p style="margin: 10px 0; color: #666; font-size: 13px;">
1456 <strong>${models.length} models available</strong> ·
1457 <a href="#" id="mxchat_back_to_provider_select" style="color: #2271b1;">← Back to providers</a>
1458 </p>
1459 </div>
1460 <div class="mxchat-model-selector-grid" id="mxchat_openrouter_models_grid"></div>
1461 `;
1462
1463 // Update footer with Save button for OpenRouter
1464 const footerContent = `
1465 <button id="mxchat_back_to_models_footer" class="button mxchat-model-cancel-btn">Back to Providers</button>
1466 <button id="mxchat_save_openrouter_model" class="button button-primary" disabled>
1467 <span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model
1468 </button>
1469 `;
1470
1471 $modalBody.html(newContent);
1472 $modalFooter.html(footerContent);
1473
1474 // Function to render models
1475 function renderOpenRouterModels(filterText = '') {
1476 const $grid = $('#mxchat_openrouter_models_grid');
1477 $grid.empty();
1478
1479 let filteredModels = models;
1480 if (filterText) {
1481 const lowerFilter = filterText.toLowerCase();
1482 filteredModels = models.filter(m =>
1483 m.id.toLowerCase().includes(lowerFilter) ||
1484 m.name.toLowerCase().includes(lowerFilter) ||
1485 (m.description && m.description.toLowerCase().includes(lowerFilter))
1486 );
1487 }
1488
1489 filteredModels.forEach(model => {
1490 const isSelected = pendingSelection.modelId === model.id;
1491 const contextLength = model.context_length ? `${(model.context_length / 1000).toFixed(0)}K` : '';
1492 const promptPrice = model.pricing.prompt ? `$${(model.pricing.prompt * 1000000).toFixed(2)}/1M` : '';
1493
1494 const $card = jQuery(`
1495 <div class="mxchat-openrouter-card ${isSelected ? 'mxchat-model-selected' : ''}" data-model-id="${model.id}" data-model-name="${model.name}">
1496 <div class="mxchat-model-selector-icon">
1497 ${getOpenRouterIcon(model.id)}
1498 </div>
1499 <div class="mxchat-model-selector-info">
1500 <h4 class="mxchat-model-selector-title">${model.name}</h4>
1501 <div style="font-size: 12px; color: #666; margin-top: 5px;">
1502 ${contextLength ? '<span style="margin-right: 12px;">📄 ' + contextLength + '</span>' : ''}
1503 ${promptPrice ? '<span>💰 ' + promptPrice + '</span>' : ''}
1504 </div>
1505 </div>
1506 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1507 </div>
1508 `);
1509
1510 $grid.append($card);
1511 });
1512 }
1513
1514 // Helper to get icon
1515 function getOpenRouterIcon(modelId) {
1516 if (modelId.includes('gpt') || modelId.includes('openai')) {
1517 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>';
1518 } else if (modelId.includes('claude') || modelId.includes('anthropic')) {
1519 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>';
1520 } else if (modelId.includes('gemini') || modelId.includes('google')) {
1521 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>';
1522 }
1523 return '<span class="dashicons dashicons-cloud" style="font-size: 24px; color: #6750A4;"></span>';
1524 }
1525
1526 // Initial render
1527 renderOpenRouterModels();
1528
1529 // Search handler
1530 $('#mxchat_openrouter_search').on('input', function() {
1531 renderOpenRouterModels($(this).val());
1532 });
1533
1534 $('#mxchat_back_to_provider_select').on('click', function(e) {
1535 e.preventDefault();
1536 // Restore footer to default state
1537 restoreDefaultModalFooter();
1538 // Instead of location.reload(), restore original content
1539 restoreOriginalModalContent();
1540 });
1541
1542 // Back to providers footer button
1543 $('#mxchat_back_to_models_footer').on('click', function(e) {
1544 e.preventDefault();
1545 // Restore footer to default state
1546 restoreDefaultModalFooter();
1547 // Restore original content
1548 restoreOriginalModalContent();
1549 });
1550
1551 // Model selection - just highlight, don't save yet
1552 $(document).on('click', '.mxchat-openrouter-card', function(e) {
1553 e.stopPropagation(); // Prevent triggering the regular model card handler
1554
1555 const modelId = $(this).data('model-id');
1556 const modelName = $(this).data('model-name');
1557
1558 // Remove selection from all cards
1559 $('.mxchat-openrouter-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1560
1561 // Add selection to clicked card
1562 $(this).addClass('mxchat-model-selected');
1563 if ($(this).find('.mxchat-model-selector-checkmark').length === 0) {
1564 $(this).append('<div class="mxchat-model-selector-checkmark">✓</div>');
1565 }
1566
1567 // Update pending selection
1568 pendingSelection.modelId = modelId;
1569 pendingSelection.modelName = modelName;
1570
1571 // Enable the save button
1572 $('#mxchat_save_openrouter_model').prop('disabled', false);
1573 });
1574
1575 // Save button handler
1576 $('#mxchat_save_openrouter_model').on('click', function() {
1577 const $saveButton = $(this);
1578
1579 if (!pendingSelection.modelId) {
1580 return;
1581 }
1582
1583 // Disable button and show loading state
1584 $saveButton.prop('disabled', true).html('<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...');
1585
1586 // First, save that we're using OpenRouter
1587 jQuery.ajax({
1588 url: mxchatAdmin.ajax_url,
1589 type: 'POST',
1590 data: {
1591 action: 'mxchat_save_setting',
1592 name: 'model',
1593 value: 'openrouter',
1594 _ajax_nonce: mxchatAdmin.setting_nonce
1595 },
1596 success: function() {
1597 // After model is set, save the model ID
1598 jQuery.ajax({
1599 url: mxchatAdmin.ajax_url,
1600 type: 'POST',
1601 data: {
1602 action: 'mxchat_save_setting',
1603 name: 'openrouter_selected_model',
1604 value: pendingSelection.modelId,
1605 _ajax_nonce: mxchatAdmin.setting_nonce
1606 },
1607 success: function() {
1608 // Update DOM immediately
1609 $('#openrouter_selected_model').val(pendingSelection.modelId);
1610
1611 // After model ID is saved, save the display name
1612 jQuery.ajax({
1613 url: mxchatAdmin.ajax_url,
1614 type: 'POST',
1615 data: {
1616 action: 'mxchat_save_setting',
1617 name: 'openrouter_selected_model_name',
1618 value: pendingSelection.modelName,
1619 _ajax_nonce: mxchatAdmin.setting_nonce
1620 },
1621 success: function() {
1622 // Update DOM immediately
1623 $('#openrouter_selected_model_name').val(pendingSelection.modelName);
1624
1625 // Update button text
1626 $('#mxchat_model_selector_btn').text('OpenRouter: ' + pendingSelection.modelName);
1627
1628 // Update the "Currently using" message
1629 const $currentSelection = $('#openrouter-current-selection');
1630 if ($currentSelection.length) {
1631 $currentSelection.html(
1632 '<span class="dashicons dashicons-yes" style="font-size: 16px; vertical-align: middle;"></span> ' +
1633 'Currently using: <strong>' + pendingSelection.modelName + '</strong>'
1634 ).show();
1635 }
1636
1637 // Show success state briefly
1638 $saveButton.html('<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!');
1639
1640 // Close modal after short delay
1641 setTimeout(function() {
1642 // Restore footer to default state
1643 restoreDefaultModalFooter();
1644 $modal.hide();
1645 }, 800);
1646 },
1647 error: function() {
1648 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1649 alert('Failed to save model name. Please try again.');
1650 }
1651 });
1652 },
1653 error: function() {
1654 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1655 alert('Failed to save model ID. Please try again.');
1656 }
1657 });
1658 },
1659 error: function() {
1660 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1661 alert('Failed to save OpenRouter selection. Please try again.');
1662 }
1663 });
1664 });
1665
1666 }
1667
1668 // 3.2.3: Confirmation dialog shown when the user attempts to switch embedding
1669 // models after they've already embedded content with a different model. Mixing
1670 // embeddings from two models silently breaks similarity matching.
1671 function showEmbeddingSwitchWarning(data, newValue, onChoice) {
1672 $('#mxchat_embedding_switch_warning').remove();
1673
1674 const dimsBlock = data.dims_differ ? `
1675 <div class="mxchat-embed-warn-dims">
1676 <strong>Dimension mismatch:</strong>
1677 Existing vectors are ${data.active_dims}-dimensional, but ${data.new_label}
1678 produces ${data.new_dims}-dimensional vectors.
1679 If you use Pinecone, your index will reject queries entirely until you re-embed.
1680 </div>` : '';
1681
1682 const $modal = $(`
1683 <div id="mxchat_embedding_switch_warning" class="mxchat-embed-warn-overlay">
1684 <div class="mxchat-embed-warn-dialog">
1685 <div class="mxchat-embed-warn-header">
1686 <span class="mxchat-embed-warn-icon">⚠️</span>
1687 <h3>Switching embedding models will break similarity matching</h3>
1688 </div>
1689 <div class="mxchat-embed-warn-body">
1690 <p>
1691 Your knowledge base and actions are currently embedded with
1692 <code>${data.active_label}</code>. Switching to
1693 <code>${data.new_label}</code> means new queries are embedded with
1694 a different model than the stored vectors — the chatbot will return
1695 inaccurate results or fail to match anything.
1696 </p>
1697 ${dimsBlock}
1698 <p><strong>To switch safely:</strong></p>
1699 <ol>
1700 <li>Go to <em>Knowledge Base</em> and delete all existing entries.</li>
1701 <li>Go to <em>Actions</em> and delete all existing actions.</li>
1702 <li>Come back here, switch the model, then re-import your content and re-add your actions.</li>
1703 </ol>
1704 </div>
1705 <div class="mxchat-embed-warn-footer">
1706 <button type="button" class="button button-secondary" id="mxchat_embed_warn_cancel">Cancel — keep ${data.active_label}</button>
1707 <button type="button" class="button button-primary mxchat-embed-warn-danger" id="mxchat_embed_warn_continue">Switch anyway (I'll handle it)</button>
1708 </div>
1709 </div>
1710 </div>
1711 `);
1712
1713 $('body').append($modal);
1714
1715 let resolved = false;
1716 function resolve(confirmed) {
1717 if (resolved) return;
1718 resolved = true;
1719 $modal.remove();
1720 if (typeof onChoice === 'function') onChoice(confirmed);
1721 }
1722
1723 // Click on the overlay background dismisses (cancel)
1724 $modal.on('click', function(e) {
1725 if (e.target === $modal[0]) resolve(false);
1726 });
1727 $modal.find('#mxchat_embed_warn_cancel').on('click', function() { resolve(false); });
1728 $modal.find('#mxchat_embed_warn_continue').on('click', function() { resolve(true); });
1729 }
1730
1731 // Embedding model selector - completely separate from chat model selector
1732 function setupMxChatEmbeddingModelSelector() {
1733 const $embeddingModelSelect = $('#embedding_model');
1734
1735 // Skip if the element doesn't exist on the page
1736 if ($embeddingModelSelect.length === 0) {
1737 return;
1738 }
1739
1740 const $embeddingModelSelectorButton = $('<button>', {
1741 type: 'button',
1742 id: 'mxchat_embedding_model_selector_btn',
1743 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
1744 text: 'Select Embedding Model'
1745 });
1746
1747 // Replace the select dropdown with a button
1748 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
1749
1750 // Update button text to show currently selected model
1751 function updateButtonText() {
1752 const selectedModel = $embeddingModelSelect.val();
1753 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
1754 $embeddingModelSelectorButton.text(selectedModelText);
1755 }
1756
1757 // Initialize button text
1758 updateButtonText();
1759
1760 // Create a unique ID for the modal to avoid conflicts
1761 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
1762
1763 // Create and append modal HTML with unique IDs
1764 const embeddingModelSelectorModal = `
1765 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
1766 <div class="mxchat-embedding-model-selector-modal-content">
1767 <div class="mxchat-embedding-model-selector-modal-header">
1768 <h3>Select Embedding Model</h3>
1769 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
1770 </div>
1771 <div class="mxchat-embedding-model-selector-modal-body">
1772 <div class="mxchat-embedding-model-selector-search-container">
1773 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
1774 </div>
1775 <div class="mxchat-embedding-model-selector-categories">
1776 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
1777 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
1778 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
1779 <button class="mxchat-embedding-model-category-btn" data-category="gemini">Google Gemini</button>
1780 </div>
1781 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
1782 </div>
1783 <div class="mxchat-embedding-model-selector-modal-footer">
1784 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
1785 </div>
1786 </div>
1787 </div>
1788 `;
1789
1790 // Use jQuery's append to ensure it doesn't clash with existing modals
1791 $('body').append(embeddingModelSelectorModal);
1792
1793 // Populate models grid
1794 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1795 const $grid = $('#mxchat_embedding_models_grid');
1796 $grid.empty();
1797
1798 // Define embedding models with descriptions and context lengths
1799 const models = {
1800 openai: [
1801 {
1802 value: 'text-embedding-3-small',
1803 label: 'TE3 Small',
1804 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1805 },
1806 {
1807 value: 'text-embedding-ada-002',
1808 label: 'Ada 2',
1809 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1810 },
1811 {
1812 value: 'text-embedding-3-large',
1813 label: 'TE3 Large',
1814 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1815 }
1816 ],
1817 voyage: [
1818 {
1819 value: 'voyage-3-large',
1820 label: 'Voyage-3 Large',
1821 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1822 }
1823 ],
1824 gemini: [
1825 {
1826 value: 'gemini-embedding-001',
1827 label: 'Gemini Embedding',
1828 description: 'Stable SOTA embeddings (1536 dimensions, 8K context)'
1829 }
1830 ]
1831 };
1832
1833 let allModels = [];
1834 Object.keys(models).forEach(key => {
1835 if (category === 'all' || category === key) {
1836 allModels = allModels.concat(models[key]);
1837 }
1838 });
1839
1840 // Filter by search term if present
1841 if (filter) {
1842 const lowerFilter = filter.toLowerCase();
1843 allModels = allModels.filter(model =>
1844 model.label.toLowerCase().includes(lowerFilter) ||
1845 model.description.toLowerCase().includes(lowerFilter)
1846 );
1847 }
1848
1849 // Create model cards
1850 allModels.forEach(model => {
1851 const isSelected = $embeddingModelSelect.val() === model.value;
1852 let providerClass = 'mxchat-embedding-model-provider-openai';
1853
1854 if (model.value.startsWith('voyage-')) {
1855 providerClass = 'mxchat-embedding-model-provider-voyage';
1856 } else if (model.value.startsWith('gemini-embedding-')) {
1857 providerClass = 'mxchat-embedding-model-provider-gemini';
1858 }
1859
1860 let iconHTML = '';
1861 if (model.value.startsWith('voyage-')) {
1862 iconHTML = '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>';
1863 } else if (model.value.startsWith('gemini-embedding-')) {
1864 iconHTML = '<span class="dashicons dashicons-google mxchat-embedding-model-icon-gemini"></span>';
1865 } else {
1866 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>';
1867 }
1868
1869 const $modelCard = $(`
1870 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1871 <div class="mxchat-embedding-model-selector-icon">
1872 ${iconHTML}
1873 </div>
1874 <div class="mxchat-embedding-model-selector-info">
1875 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1876 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1877 </div>
1878 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1879 </div>
1880 `);
1881
1882 $grid.append($modelCard);
1883 });
1884 }
1885
1886 // Event handlers - use namespaced events to avoid conflicts
1887 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1888 e.stopPropagation(); // Prevent event bubbling
1889 $('#' + embeddingModalId).show();
1890 populateEmbeddingModelsGrid('', 'all');
1891 });
1892
1893 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1894 e.stopPropagation(); // Prevent event bubbling
1895 $('#' + embeddingModalId).hide();
1896 });
1897
1898 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1899 e.stopPropagation(); // Prevent event bubbling
1900 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1901 $(this).addClass('active');
1902 const category = $(this).data('category');
1903 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1904 populateEmbeddingModelsGrid(searchTerm, category);
1905 });
1906
1907 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1908 const searchTerm = $(this).val();
1909 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1910 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1911 });
1912
1913 // 3.2.3: Commit a model change — same as the original click handler, just
1914 // factored out so it can be invoked from both the safe path and the
1915 // post-confirmation path.
1916 function commitEmbeddingModelChange(modelValue) {
1917 $embeddingModelSelect.val(modelValue);
1918 const changeEvent = new Event('change', { bubbles: true });
1919 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1920 updateButtonText();
1921 $('#' + embeddingModalId).hide();
1922 }
1923
1924 // Use a direct selector to avoid conflicts with other card elements
1925 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1926 e.stopPropagation(); // Prevent event bubbling
1927 const modelValue = $(this).data('value');
1928 const previousValue = $embeddingModelSelect.val();
1929
1930 // No-op if the user clicked the already-selected card
1931 if (modelValue === previousValue) {
1932 $('#' + embeddingModalId).hide();
1933 return;
1934 }
1935
1936 // 3.2.3: Preflight — if the user has content embedded with a different
1937 // model, surface a confirmation dialog before committing the switch.
1938 $.post(ajaxurl, {
1939 action: 'mxchat_check_embedding_switch',
1940 security: (typeof mxchatAdmin !== 'undefined' ? mxchatAdmin.nonce : ''),
1941 new_model: modelValue
1942 }).done(function(resp) {
1943 if (resp && resp.success && resp.data && resp.data.is_mismatch) {
1944 showEmbeddingSwitchWarning(resp.data, modelValue, function(confirmed) {
1945 if (confirmed) {
1946 commitEmbeddingModelChange(modelValue);
1947 }
1948 });
1949 } else {
1950 commitEmbeddingModelChange(modelValue);
1951 }
1952 }).fail(function() {
1953 // If preflight fails, fall back to the original behavior so a network
1954 // hiccup doesn't block legitimate model switches.
1955 commitEmbeddingModelChange(modelValue);
1956 });
1957 return;
1958 });
1959
1960 // Close modal when clicking outside - use namespaced events
1961 $(window).on('click.embeddingModelSelector', function(event) {
1962 if ($(event.target).is('#' + embeddingModalId)) {
1963 $('#' + embeddingModalId).hide();
1964 }
1965 });
1966 }
1967
1968 // Call this function after the DOM is fully loaded
1969 $(document).ready(function() {
1970 setupMxChatModelSelector();
1971 setupMxChatEmbeddingModelSelector();
1972 });
1973
1974 // Add Intent Form Submission
1975 $('#mxchat-add-intent-form').on('submit', function(event) {
1976 $('#mxchat-intent-loading').show();
1977 $('#mxchat-intent-loading-text').show();
1978 $(this).find('button[type="submit"]').hide();
1979 });
1980
1981 // Inline Edit Functionality
1982 $('.edit-button').on('click', function() {
1983 var row = $(this).closest('tr');
1984
1985 // Clear URL field if it's a manual content URL (mxchat:// protocol)
1986 var urlEdit = row.find('.url-edit');
1987 if (urlEdit.length && urlEdit.val().indexOf('mxchat://') === 0) {
1988 urlEdit.val('');
1989 }
1990
1991 // Expand the accordion to show the edit textarea (fixes short content editing)
1992 var contentFull = row.find('.mxchat-content-full');
1993 if (contentFull.length && contentFull.is(':hidden')) {
1994 contentFull.show();
1995 row.find('.mxchat-content-preview').hide();
1996 }
1997
1998 row.find('.content-view, .url-view').hide();
1999 row.find('.content-edit, .url-edit').show();
2000 row.find('.edit-button').hide();
2001 row.find('.save-button').show();
2002 });
2003
2004 // Save button handler
2005 // Save button handler
2006 $('.save-button').on('click', function() {
2007 var button = $(this);
2008 var row = button.closest('tr');
2009 var id = button.data('id');
2010 var nonce = button.data('nonce'); // Get nonce from button data attribute
2011 var newContent = row.find('.content-edit').val();
2012 var newUrl = row.find('.url-edit').val();
2013
2014 //console.log('Nonce from button:', nonce); // Debug
2015
2016 button.prop('disabled', true);
2017 button.text('Saving...');
2018
2019 $.ajax({
2020 url: mxchatAdmin.ajax_url,
2021 type: 'POST',
2022 data: {
2023 action: 'mxchat_save_inline_prompt',
2024 id: id,
2025 article_content: newContent,
2026 article_url: newUrl,
2027 _ajax_nonce: nonce // Use nonce from button
2028 },
2029 success: function(response) {
2030 button.prop('disabled', false);
2031 button.text('Save');
2032
2033 if (response.success) {
2034 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
2035 if (newUrl) {
2036 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank"><span class="dashicons dashicons-external"></span> View Source</a>');
2037 } else {
2038 row.find('.url-view').html('<span class="mxchat-na">Manual Content</span>');
2039 }
2040
2041 row.find('.content-edit, .url-edit').hide();
2042 row.find('.content-view, .url-view').show();
2043 row.find('.save-button').hide();
2044 row.find('.edit-button').show();
2045
2046 // Restore accordion state - show preview, hide full content
2047 row.find('.mxchat-content-preview').show();
2048 row.find('.mxchat-content-full').hide();
2049 } else {
2050 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
2051 }
2052 },
2053 error: function() {
2054 button.prop('disabled', false);
2055 button.text('Save');
2056 alert('An error occurred while saving.');
2057 }
2058 });
2059 });
2060
2061
2062 // Questions handling
2063 $('.mxchat-add-question').on('click', function () {
2064 const container = $('#mxchat-additional-questions-container');
2065 const questionCount = container.find('.mxchat-question-row').length + 4;
2066 const questionIndex = container.find('.mxchat-question-row').length;
2067
2068 const newQuestion = `
2069 <div class="mxchat-question-row">
2070 <input type="text"
2071 name="additional_popular_questions[]"
2072 placeholder="Enter Additional Popular Question ${questionCount}"
2073 class="regular-text mxchat-question-input"
2074 data-question-index="${questionIndex}" />
2075 <button type="button" class="button mxchat-remove-question"
2076 aria-label="Remove question">Remove</button>
2077 </div>
2078 `;
2079 container.append(newQuestion);
2080 });
2081
2082 $(document).on('click', '.mxchat-remove-question', function () {
2083 $(this).closest('.mxchat-question-row').remove();
2084 saveQuestions();
2085 });
2086
2087 $(document).on('change', '.mxchat-question-input', function() {
2088 saveQuestions();
2089 });
2090
2091 function saveQuestions() {
2092 const questions = [];
2093 $('.mxchat-question-input').each(function() {
2094 const value = $(this).val().trim();
2095 if (value) {
2096 questions.push(value);
2097 }
2098 });
2099
2100 const feedbackContainer = $('<div class="feedback-container"></div>');
2101 const spinner = $('<div class="saving-spinner"></div>');
2102 const successIcon = $('<div class="success-icon">✔</div>');
2103
2104 // Append feedback after the add button
2105 $('.mxchat-add-question').after(feedbackContainer);
2106 feedbackContainer.append(spinner);
2107
2108 // Save via AJAX
2109 $.ajax({
2110 url: mxchatAdmin.ajax_url,
2111 type: 'POST',
2112 data: {
2113 action: 'mxchat_save_setting',
2114 name: 'additional_popular_questions',
2115 value: JSON.stringify(questions),
2116 _ajax_nonce: mxchatAdmin.setting_nonce
2117 },
2118 success: function(response) {
2119 if (response.success) {
2120 spinner.fadeOut(200, function() {
2121 feedbackContainer.append(successIcon);
2122 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
2123 feedbackContainer.remove();
2124 });
2125 });
2126 } else {
2127 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
2128 feedbackContainer.remove();
2129 }
2130 },
2131 error: function() {
2132 alert('An error occurred while saving questions.');
2133 feedbackContainer.remove();
2134 }
2135 });
2136 }
2137
2138 // Live agent status handler
2139 const statusToggle = document.getElementById('live_agent_status');
2140 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
2141 if (statusToggle && statusText) {
2142 statusToggle.addEventListener('change', function() {
2143 // Update display text
2144 statusText.textContent = this.checked ? 'Online' : 'Offline';
2145
2146 // Send the correct on/off value to the server
2147 if (window.mxchatSaveSetting) {
2148 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
2149 }
2150 });
2151 }
2152
2153 // Live agent availability schedules (plans 8ccaa2 + 99d7a4).
2154 // The editor markup exists once PER CHANNEL (Slack + Telegram tabs), so
2155 // everything is scoped inside each .mxchat-la-schedule container via classes
2156 // — no page-global ids. Each editor collects its own day grid into its own
2157 // hidden live_agent_schedule_<channel> input as JSON and fires one change
2158 // event, so the existing autosave handler above does the POST.
2159 $('.mxchat-la-schedule').each(function() {
2160 const $laSchedule = $(this);
2161 const $laEnabled = $laSchedule.find('.mxchat-la-enabled');
2162 const $laHidden = $laSchedule.find('.mxchat-la-hidden');
2163 const $laDays = $laSchedule.find('.mxchat-la-days');
2164 const $laStatus = $laSchedule.find('.mxchat-la-status-text');
2165
2166 function laCollect() {
2167 const days = {};
2168 $laDays.find('.mxchat-la-day').each(function() {
2169 const $day = $(this);
2170 const n = $day.data('day');
2171 days[n] = {
2172 enabled: $day.find('.mxchat-la-day-enabled').is(':checked'),
2173 start: $day.find('.mxchat-la-start').val() || '09:00',
2174 end: $day.find('.mxchat-la-end').val() || '17:00'
2175 };
2176 });
2177 return { enabled: $laEnabled.is(':checked'), days: days };
2178 }
2179
2180 function laSync() {
2181 const schedule = laCollect();
2182 $laSchedule.toggleClass('is-active', schedule.enabled);
2183 $laDays.attr('aria-hidden', schedule.enabled ? 'false' : 'true');
2184 $laStatus.text(
2185 schedule.enabled
2186 ? (mxchatAdmin.i18n_scheduled_hours || 'Scheduled hours')
2187 : (mxchatAdmin.i18n_always_available || 'Always available')
2188 );
2189 $laDays.find('.mxchat-la-day').each(function() {
2190 const $day = $(this);
2191 $day.toggleClass('is-on', $day.find('.mxchat-la-day-enabled').is(':checked'));
2192 });
2193 // Hand this channel's schedule to the shared autosave transport.
2194 $laHidden.val(JSON.stringify(schedule)).trigger('change');
2195 }
2196
2197 $laSchedule.find('.mxchat-la-field').on('change', laSync);
2198 });
2199
2200 // Function to adjust the textarea height to content
2201 function adjustTextareaHeight() {
2202 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
2203 this.style.height = this.scrollHeight + 'px'; // Expand to content height
2204 }
2205
2206 // Function to reset the textarea height to initial
2207 function resetTextareaHeight() {
2208 this.style.height = ''; // Remove inline height, reverting to CSS default
2209 }
2210
2211 // Target the specific textarea by ID
2212 var $textarea = $('#system_prompt_instructions');
2213
2214 // Bind events
2215 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
2216 .on('blur', resetTextareaHeight); // Reset on blur
2217 });
2218
2219
2220
2221 document.addEventListener('DOMContentLoaded', function() {
2222 // Check if we're on the correct page before initializing
2223 const modal = document.getElementById('mxchat-action-modal');
2224
2225 // Only initialize if the modal exists on this page
2226 if (modal) {
2227 //console.log('MXChat Action Modal JS Loaded');
2228
2229 // Initialize the action modal functionality
2230 initStepBasedActionModal();
2231 }
2232
2233 // Function to initialize the step-based action modal
2234 function initStepBasedActionModal() {
2235 // We already checked for modal existence above, so no need to check again
2236
2237 const actionStep1 = document.getElementById('mxchat-action-step-1');
2238 const actionStep2 = document.getElementById('mxchat-action-step-2');
2239 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
2240 const searchInput = document.getElementById('action-type-search');
2241 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
2242 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
2243 const actionForm = document.getElementById('mxchat-action-form');
2244 const callbackInput = document.getElementById('callback_function');
2245 const actionIdField = document.getElementById('edit_action_id');
2246 const labelField = document.getElementById('intent_label');
2247 const phrasesField = document.getElementById('action_phrases');
2248 const formActionType = document.getElementById('form_action_type');
2249 const nonceContainer = document.getElementById('action-nonce-container');
2250 const thresholdSlider = document.getElementById('similarity_threshold');
2251 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2252
2253 // Rest of your initialization code remains the same...
2254
2255 // Log the structure of one action card for debugging
2256 if (actionCards.length > 0) {
2257 //console.log('First action card data attributes:', actionCards[0].dataset);
2258 //console.log('First action card HTML:', actionCards[0].outerHTML);
2259 }
2260
2261 // Add click event listeners to category buttons
2262 categoryButtons.forEach(button => {
2263 button.addEventListener('click', function() {
2264 //console.log('Category button clicked:', this.dataset.category);
2265
2266 // Remove active class from all buttons
2267 categoryButtons.forEach(btn => btn.classList.remove('active'));
2268
2269 // Add active class to clicked button
2270 this.classList.add('active');
2271
2272 // Get selected category
2273 const category = this.dataset.category;
2274
2275 // Filter action cards
2276 filterActionCards(category, searchInput.value);
2277 });
2278 });
2279
2280 // Add search functionality
2281 if (searchInput) {
2282 searchInput.addEventListener('input', function() {
2283 // Get active category
2284 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
2285 //console.log('Search input changed, active category:', activeCategory);
2286
2287 // Filter action cards
2288 filterActionCards(activeCategory, this.value);
2289 });
2290 }
2291
2292 // Add click event listeners to action cards
2293 actionCards.forEach(card => {
2294 card.addEventListener('click', function() {
2295 // Get the action data
2296 const isPro = this.dataset.pro === 'true';
2297 const isInstalled = this.dataset.installed === 'true';
2298 const addonName = this.dataset.addon || '';
2299 const actionValue = this.dataset.value;
2300 const actionLabel = this.dataset.label;
2301 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
2302 const actionDescription = this.querySelector('p').textContent;
2303
2304 // Check if this is a promotional card (add-on not installed)
2305 const isPromo = this.dataset.promo === 'true';
2306
2307 if (isPromo || (addonName && !isInstalled)) {
2308 // Add-on required but not installed — show informational notice
2309 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '').replace('— Get Add-on', '').trim() || addonName + ' Add-on';
2310 showAddonRequiredNotice(addonDisplayName);
2311 return;
2312 }
2313
2314 // If we get here, the action is available - proceed as normal
2315 callbackInput.value = actionValue;
2316
2317 // Update the selected action display in step 2
2318 document.getElementById('selected-action-title').textContent = actionLabel;
2319 document.getElementById('selected-action-description').textContent = actionDescription;
2320 document.getElementById('selected-action-icon').innerHTML =
2321 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2322
2323 // Set a default label based on the action type (user can change it)
2324 if (!labelField.value) {
2325 labelField.value = actionLabel;
2326 }
2327
2328 // Move to step 2
2329 actionStep1.classList.remove('active');
2330 actionStep2.classList.add('active');
2331
2332 // Update modal title
2333 });
2334 });
2335
2336 // Back button functionality
2337 if (backToStep1Btn) {
2338 backToStep1Btn.addEventListener('click', function() {
2339 //console.log('Back button clicked');
2340 actionStep2.classList.remove('active');
2341 actionStep1.classList.add('active');
2342 });
2343 }
2344
2345 // Function to filter action cards by category and search term
2346 function filterActionCards(category, searchTerm) {
2347 searchTerm = searchTerm.toLowerCase().trim();
2348 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
2349
2350 let visibleCount = 0;
2351
2352 // Show all cards initially with animation
2353 actionCards.forEach((card, index) => {
2354 // Reset animation
2355 card.style.animation = 'none';
2356 // Trigger reflow
2357 void card.offsetWidth;
2358
2359 // Determine if card should be visible based on category and search term
2360 const cardCategory = card.dataset.category || '';
2361 const matchesCategory = category === 'all' || cardCategory === category;
2362
2363 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
2364 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
2365 const matchesSearch = searchTerm === '' ||
2366 cardTitle.includes(searchTerm) ||
2367 cardDesc.includes(searchTerm);
2368
2369 // Show/hide card with animation
2370 if (matchesCategory && matchesSearch) {
2371 card.style.display = 'flex';
2372 // Staggered animation for cards
2373 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
2374 visibleCount++;
2375 } else {
2376 card.style.display = 'none';
2377 }
2378 });
2379
2380 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
2381 }
2382
2383 // Function to show notice for Pro features
2384 function showProFeatureNotice() {
2385 //console.log('Showing Pro feature notice');
2386 // Check if we already have a notification container
2387 let noticeContainer = document.querySelector('.mxchat-pro-notice');
2388
2389 if (!noticeContainer) {
2390 // Create the notice container
2391 noticeContainer = document.createElement('div');
2392 noticeContainer.className = 'mxchat-pro-notice';
2393
2394 // Create content
2395 noticeContainer.innerHTML = `
2396 <div class="mxchat-pro-notice-content">
2397 <h3>MxChat Pro Feature</h3>
2398 <p>This action is available in the Pro version only.</p>
2399 <div class="mxchat-pro-notice-buttons">
2400 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
2401 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
2402 </div>
2403 </div>
2404 `;
2405
2406 // Append to body
2407 document.body.appendChild(noticeContainer);
2408
2409 // Add close functionality
2410 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
2411 closeButton.addEventListener('click', function() {
2412 noticeContainer.classList.remove('active');
2413 setTimeout(() => {
2414 noticeContainer.remove();
2415 }, 300);
2416 });
2417
2418 // Click outside to close (drag-safe)
2419 mxchatDragSafeOverlayClose(noticeContainer, function() {
2420 closeButton.click();
2421 });
2422
2423 // Show with animation
2424 setTimeout(() => {
2425 noticeContainer.classList.add('active');
2426 }, 10);
2427 } else {
2428 // If it already exists, just make it visible again
2429 noticeContainer.classList.add('active');
2430 }
2431 }
2432
2433 // Function to show notice for add-on requirements
2434 function showAddonRequiredNotice(addonName) {
2435 //console.log(`Showing add-on notice for: ${addonName}`);
2436 // Check if we already have a notification container
2437 let noticeContainer = document.querySelector('.mxchat-addon-notice');
2438
2439 if (!noticeContainer) {
2440 // Create the notice container
2441 noticeContainer = document.createElement('div');
2442 noticeContainer.className = 'mxchat-addon-notice';
2443
2444 // Create content
2445 noticeContainer.innerHTML = `
2446 <div class="mxchat-addon-notice-content">
2447 <span class="mxchat-addon-notice-icon">🧩</span>
2448 <h3>Add-on Required</h3>
2449 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
2450 <div class="mxchat-addon-notice-buttons">
2451 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
2452 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
2453 </div>
2454 </div>
2455 `;
2456
2457 // Append to body
2458 document.body.appendChild(noticeContainer);
2459
2460 // Add close functionality
2461 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
2462 closeButton.addEventListener('click', function() {
2463 noticeContainer.classList.remove('active');
2464 setTimeout(() => {
2465 noticeContainer.remove();
2466 }, 300);
2467 });
2468
2469 // Click outside to close (drag-safe)
2470 mxchatDragSafeOverlayClose(noticeContainer, function() {
2471 closeButton.click();
2472 });
2473
2474 // Show with animation
2475 setTimeout(() => {
2476 noticeContainer.classList.add('active');
2477 }, 10);
2478 } else {
2479 // If it already exists, update the content
2480 const addonNameElement = noticeContainer.querySelector('p strong');
2481 if (addonNameElement) {
2482 addonNameElement.textContent = addonName;
2483 }
2484
2485 // Make it visible again
2486 noticeContainer.classList.add('active');
2487 }
2488 }
2489
2490 // Form submission handling
2491 if (actionForm) {
2492 actionForm.addEventListener('submit', function() {
2493 //console.log('Form submitted');
2494 document.getElementById('mxchat-action-loading').style.display = 'flex';
2495 this.querySelector('button[type="submit"]').disabled = true;
2496 });
2497 }
2498 }
2499
2500 // Setup add action buttons (only if we're on the correct page)
2501 if (modal) {
2502 // Update the modal open function to support the step-based flow
2503 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '', enabledBots = null) {
2504 //console.log('Modal opening, edit mode:', isEdit);
2505
2506 // Get form fields
2507 const actionIdField = document.getElementById('edit_action_id');
2508 const labelField = document.getElementById('intent_label');
2509 const phrasesField = document.getElementById('action_phrases');
2510 const formActionType = document.getElementById('form_action_type');
2511 const callbackInput = document.getElementById('callback_function');
2512 const saveButton = document.getElementById('mxchat-save-action-btn');
2513 const nonceContainer = document.getElementById('action-nonce-container');
2514 const thresholdSlider = document.getElementById('similarity_threshold');
2515 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2516 const actionStep1 = document.getElementById('mxchat-action-step-1');
2517 const actionStep2 = document.getElementById('mxchat-action-step-2');
2518 const searchInput = document.getElementById('action-type-search');
2519
2520 // Set up modal for edit or create
2521 if (isEdit) {
2522 saveButton.textContent = 'Update Action';
2523 formActionType.value = 'mxchat_edit_intent';
2524 actionIdField.value = actionId;
2525 labelField.value = label;
2526 phrasesField.value = phrases;
2527 callbackInput.value = callbackFunction;
2528 thresholdSlider.value = threshold; // Set the current threshold value
2529 thresholdDisplay.textContent = threshold + '%'; // Update display
2530
2531 // NEW: Handle bot selection checkboxes for editing
2532 // First uncheck all bot checkboxes
2533 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2534 checkbox.checked = false;
2535 });
2536
2537 // Then check the ones that should be enabled
2538 if (enabledBots) {
2539 let botsArray;
2540 if (typeof enabledBots === 'string') {
2541 try {
2542 botsArray = JSON.parse(enabledBots);
2543 } catch (e) {
2544 console.warn('Failed to parse enabledBots:', enabledBots);
2545 botsArray = ['default']; // fallback
2546 }
2547 } else if (Array.isArray(enabledBots)) {
2548 botsArray = enabledBots;
2549 } else {
2550 botsArray = ['default']; // fallback
2551 }
2552
2553 botsArray.forEach(botId => {
2554 const checkbox = document.querySelector(`input[name="enabled_bots[]"][value="${botId}"]`);
2555 if (checkbox) {
2556 checkbox.checked = true;
2557 }
2558 });
2559 } else {
2560 // Fallback to default if no bot data
2561 const defaultCheckbox = document.querySelector('input[name="enabled_bots[]"][value="default"]');
2562 if (defaultCheckbox) {
2563 defaultCheckbox.checked = true;
2564 }
2565 }
2566
2567 // Update the nonce field for editing
2568 nonceContainer.innerHTML = ''; // Clear existing nonce
2569 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
2570 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
2571 }
2572
2573 // For editing, go directly to step 2 and update the selected action display
2574 actionStep1.classList.remove('active');
2575 actionStep2.classList.add('active');
2576
2577 // Find the matching action card to get its details
2578 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
2579 let foundCard = null;
2580
2581 actionCards.forEach(card => {
2582 if (card.dataset.value === callbackFunction) {
2583 foundCard = card;
2584 }
2585 });
2586
2587 if (foundCard) {
2588 //console.log('Found matching action card for:', callbackFunction);
2589 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
2590 const actionIconElement = foundCard.querySelector('.dashicons');
2591 const actionIcon = actionIconElement
2592 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
2593 : 'admin-generic';
2594 const actionDescription = foundCard.querySelector('p')?.textContent || '';
2595
2596 document.getElementById('selected-action-title').textContent = actionLabel;
2597 document.getElementById('selected-action-description').textContent = actionDescription;
2598 document.getElementById('selected-action-icon').innerHTML =
2599 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2600 } else {
2601 //console.log('No matching action card found for:', callbackFunction);
2602 // Fallback if we can't find the card
2603 document.getElementById('selected-action-title').textContent = label;
2604 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
2605 document.getElementById('selected-action-icon').innerHTML =
2606 `<span class="dashicons dashicons-admin-generic"></span>`;
2607 }
2608 } else {
2609 //console.log('Setting up create mode');
2610 saveButton.textContent = 'Save Action';
2611 formActionType.value = 'mxchat_add_intent';
2612 actionIdField.value = '';
2613 labelField.value = '';
2614 phrasesField.value = '';
2615 callbackInput.value = '';
2616 thresholdSlider.value = 85; // Default value for new actions
2617 thresholdDisplay.textContent = '85%'; // Default display
2618
2619 // For new actions, ensure default is checked and others are unchecked
2620 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2621 checkbox.checked = (checkbox.value === 'default');
2622 });
2623
2624 // Update the nonce field for adding
2625 nonceContainer.innerHTML = ''; // Clear existing nonce
2626 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
2627 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
2628 }
2629
2630 // For creating new, start at step 1
2631 actionStep1.classList.add('active');
2632 actionStep2.classList.remove('active');
2633 }
2634
2635 // Show modal with animation
2636 modal.style.display = 'flex';
2637 requestAnimationFrame(() => {
2638 modal.classList.add('active');
2639 });
2640
2641 // Set up close handlers
2642 let detachEsc = null;
2643 const closeModal = () => {
2644 //console.log('Closing modal');
2645 if (detachEsc) {
2646 detachEsc();
2647 detachEsc = null;
2648 }
2649 modal.classList.remove('active');
2650 setTimeout(() => {
2651 modal.style.display = 'none';
2652 }, 300); // Match the CSS transition time
2653 };
2654
2655 // This modal holds edits: close only on explicit controls, confirming when dirty.
2656 const guardedClose = mxchatGuardedClose(modal, closeModal);
2657
2658 // Close button handler
2659 const closeBtn = modal.querySelector('.mxchat-modal-close');
2660 if (closeBtn) {
2661 closeBtn.onclick = guardedClose;
2662 }
2663
2664 // Cancel button handler
2665 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
2666 if (cancelBtns) {
2667 cancelBtns.forEach(btn => {
2668 btn.onclick = guardedClose;
2669 });
2670 }
2671
2672 // Escape key to close modal
2673 detachEsc = mxchatBindEscClose(modal, guardedClose);
2674
2675 // Focus appropriate field based on current step
2676 if (isEdit || actionStep2.classList.contains('active')) {
2677 if (labelField) labelField.focus();
2678 } else {
2679 if (searchInput) searchInput.focus();
2680 }
2681
2682 return closeModal; // Return close function for external use
2683 };
2684 // Setup add action buttons
2685 const addActionBtn = document.getElementById('mxchat-add-action-btn');
2686 if (addActionBtn) {
2687 //console.log('Add action button found');
2688 addActionBtn.onclick = () => window.mxchatOpenActionModal();
2689 }
2690
2691 const createFirstAction = document.getElementById('mxchat-create-first-action');
2692 if (createFirstAction) {
2693 //console.log('Create first action button found');
2694 createFirstAction.onclick = () => window.mxchatOpenActionModal();
2695 }
2696
2697 // Setup edit buttons
2698 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
2699 //console.log('Edit buttons found:', editButtons.length);
2700 editButtons.forEach(button => {
2701 button.onclick = () => {
2702 const actionId = button.dataset.actionId;
2703 const phrases = button.dataset.phrases;
2704 const label = button.dataset.label;
2705 const threshold = button.dataset.threshold || 85;
2706 const callbackFunction = button.dataset.callbackFunction;
2707 const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
2708
2709 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
2710 };
2711 });
2712 }
2713 });
2714
2715 jQuery(document).ready(function($) {
2716 // Auto-expand custom post types container if any are checked
2717 // Note: Click handler is in admin-knowledge-page.php inline script (initCustomPostTypesToggle)
2718 function autoExpandIfNeeded() {
2719 const $container = $('#mxchat-custom-post-types-container');
2720 const $toggleBtn = $('#mxchat-custom-post-types-toggle');
2721
2722 if ($container.length === 0) return;
2723
2724 const hasCheckedItems = $container.find('input[type="checkbox"]:checked').length > 0;
2725
2726 if (hasCheckedItems) {
2727 $container.show();
2728 const $icon = $toggleBtn.find('span:last-child');
2729 if ($icon.length) {
2730 $icon.text('');
2731 }
2732 }
2733 }
2734
2735 // Run on page load
2736 autoExpandIfNeeded();
2737 });
2738
2739 document.addEventListener('DOMContentLoaded', function() {
2740 var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
2741 var modal = document.getElementById('mxchatSampleModal');
2742 var modalClose = document.getElementById('mxchatModalClose');
2743 var closeBtn = document.getElementById('mxchatCloseBtn');
2744 var copyBtn = document.getElementById('mxchatCopyBtn');
2745 var instructionsContent = document.querySelector('.mxchat-instructions-content');
2746 var modalContent = document.querySelector('.mxchat-instructions-modal-content');
2747
2748 if (!viewSampleBtn || !modal) {
2749 return;
2750 }
2751
2752 // Open modal
2753 viewSampleBtn.addEventListener('click', function(e) {
2754 e.preventDefault();
2755 e.stopPropagation();
2756 modal.classList.add('mxchat-instructions-show');
2757 });
2758
2759 // Close modal function
2760 function closeModal(e) {
2761 if (e) {
2762 e.preventDefault();
2763 e.stopPropagation();
2764 }
2765 modal.classList.remove('mxchat-instructions-show');
2766 }
2767
2768 // Close modal events
2769 if (modalClose) {
2770 modalClose.addEventListener('click', function(e) {
2771 closeModal(e);
2772 });
2773 }
2774
2775 if (closeBtn) {
2776 closeBtn.addEventListener('click', function(e) {
2777 closeModal(e);
2778 });
2779 }
2780
2781 // Close on backdrop click ONLY (not on hover), and only when the whole gesture
2782 // happened on the backdrop — selecting the sample text and releasing past the
2783 // dialog edge otherwise dispatches the click on the overlay and closes it.
2784 mxchatDragSafeOverlayClose(modal, closeModal);
2785
2786 // Prevent modal content clicks from closing the modal
2787 if (modalContent) {
2788 modalContent.addEventListener('click', function(e) {
2789 e.stopPropagation();
2790 });
2791 }
2792
2793 // Close on escape key
2794 document.addEventListener('keydown', function(e) {
2795 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2796 closeModal();
2797 }
2798 });
2799
2800 // Copy functionality
2801 if (copyBtn && instructionsContent) {
2802 copyBtn.addEventListener('click', function(e) {
2803 e.preventDefault();
2804 e.stopPropagation();
2805
2806 var text = instructionsContent.textContent;
2807
2808 if (navigator.clipboard) {
2809 navigator.clipboard.writeText(text).then(function() {
2810 showCopySuccess();
2811 }).catch(function() {
2812 fallbackCopy(text);
2813 });
2814 } else {
2815 fallbackCopy(text);
2816 }
2817 });
2818 }
2819
2820 function fallbackCopy(text) {
2821 var textArea = document.createElement('textarea');
2822 textArea.value = text;
2823 textArea.style.position = 'fixed';
2824 textArea.style.left = '-999999px';
2825 textArea.style.top = '-999999px';
2826 document.body.appendChild(textArea);
2827 textArea.select();
2828 try {
2829 document.execCommand('copy');
2830 showCopySuccess();
2831 } catch (err) {
2832 console.error('Copy failed');
2833 }
2834 document.body.removeChild(textArea);
2835 }
2836
2837 function showCopySuccess() {
2838 var originalText = copyBtn.innerHTML;
2839 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!';
2840
2841 setTimeout(function() {
2842 copyBtn.innerHTML = originalText;
2843 }, 2000);
2844 }
2845 });
2846
2847 jQuery(document).ready(function($) {
2848 var emailDependentFields = [
2849 '#email_blocker_header_content',
2850 '#email_blocker_button_text',
2851 '#enable_name_field',
2852 '#name_field_placeholder'
2853 ];
2854
2855 // Add visual indicators
2856 emailDependentFields.forEach(function(fieldId) {
2857 var $row = $(fieldId).closest('tr');
2858 $row.addClass('email-dependent-field');
2859
2860 // Add an icon to show it's dependent
2861 var $label = $row.find('th label, th');
2862 $label.prepend('<span class="email-dependent-icon" style="color: #0073aa; margin-right: 5px;">↳</span>');
2863 });
2864
2865 // Hide initially with opacity for smoother transition
2866 emailDependentFields.forEach(function(fieldId) {
2867 $(fieldId).closest('tr').hide().css('opacity', '0');
2868 });
2869
2870 function toggleEmailFields() {
2871 var emailEnabled = $('#enable_email_block').is(':checked');
2872
2873 emailDependentFields.forEach(function(fieldId) {
2874 var $row = $(fieldId).closest('tr');
2875 if (emailEnabled) {
2876 $row.slideDown(400).animate({opacity: 1}, 200);
2877 } else {
2878 $row.animate({opacity: 0}, 200).slideUp(400);
2879 }
2880 });
2881 }
2882
2883 toggleEmailFields();
2884 $('#enable_email_block').on('change', toggleEmailFields);
2885 });
2886
2887
2888
2889 //Handle role restriction changes
2890 jQuery(document).ready(function($) {
2891 //Handle role restriction changes for both data sources
2892 $(document).on('change', '.mxchat-role-select', function() {
2893 const $select = $(this);
2894 const entryId = $select.data('entry-id');
2895 const dataSource = $select.data('data-source');
2896 const roleRestriction = $select.val();
2897 const nonce = $select.data('nonce');
2898
2899 // Visual feedback
2900 $select.prop('disabled', true).addClass('updating');
2901
2902 $.ajax({
2903 url: ajaxurl,
2904 type: 'POST',
2905 data: {
2906 action: 'mxchat_update_role_restriction',
2907 nonce: nonce,
2908 entry_id: entryId,
2909 data_source: dataSource, // NEW: Include data source
2910 role_restriction: roleRestriction
2911 },
2912 success: function(response) {
2913 if (response.success) {
2914 // Show success feedback
2915 $select.removeClass('updating').addClass('updated');
2916 setTimeout(() => {
2917 $select.removeClass('updated');
2918 }, 2000);
2919 } else {
2920 alert('Failed to update role restriction: ' + response.data);
2921 // Revert selection
2922 $select.val($select.data('original-value'));
2923 }
2924 },
2925 error: function() {
2926 alert('Error updating role restriction');
2927 // Revert selection
2928 $select.val($select.data('original-value'));
2929 },
2930 complete: function() {
2931 $select.prop('disabled', false);
2932 }
2933 });
2934
2935 // Store original value for potential revert
2936 $select.data('original-value', roleRestriction);
2937 });
2938
2939 // Store initial values
2940 $('.mxchat-role-select').each(function() {
2941 $(this).data('original-value', $(this).val());
2942 });
2943 });
2944
2945 // Bot Selector Handler
2946 jQuery(document).ready(function($) {
2947 var saveTimer;
2948
2949 // Function to update bot_id in forms
2950 function updateBotIdInForm(formSelector) {
2951 var botId = $('#mxchat-bot-selector').val();
2952 var form = $(formSelector);
2953
2954 if (form.length > 0) {
2955 // Remove existing bot_id hidden input
2956 form.find('input[name="bot_id"]').remove();
2957
2958 // Add new bot_id hidden input if not default
2959 if (botId && botId !== 'default') {
2960 form.append('<input type="hidden" name="bot_id" value="' + botId + '">');
2961 }
2962 }
2963 }
2964
2965 $('#mxchat-bot-selector').on('change', function() {
2966 var botId = $(this).val();
2967
2968 // Clear any existing timer
2969 clearTimeout(saveTimer);
2970
2971 // Update all forms with new bot_id when bot selection changes
2972 updateBotIdInForm('#mxchat-url-form');
2973 updateBotIdInForm('#mxchat-content-form');
2974
2975 // Save the selection via AJAX
2976 $.ajax({
2977 url: ajaxurl,
2978 type: 'POST',
2979 data: {
2980 action: 'mxchat_save_selected_bot',
2981 bot_id: botId,
2982 nonce: mxchatAdmin.setting_nonce
2983 },
2984 success: function(response) {
2985 if (response.success) {
2986 // Show saved indicator
2987 $('#mxchat-bot-save-status').fadeIn().delay(2000).fadeOut();
2988
2989 // Reload the page after a short delay to refresh content
2990 saveTimer = setTimeout(function() {
2991 var currentUrl = new URL(window.location.href);
2992 currentUrl.searchParams.set('bot_id', botId);
2993 currentUrl.searchParams.set('page', 'mxchat-prompts');
2994 window.location.href = currentUrl.toString();
2995 }, 500);
2996 }
2997 },
2998 error: function() {
2999 console.error('Failed to save bot selection');
3000 }
3001 });
3002 });
3003
3004 // Initialize forms with current bot_id when the page loads
3005 setTimeout(function() {
3006 updateBotIdInForm('#mxchat-url-form');
3007 updateBotIdInForm('#mxchat-content-form');
3008 }, 100);
3009 });
3010
3011 // API Key Status Indicator for Chat Models
3012 jQuery(document).ready(function($) {
3013 function updateChatModelAPIStatus(apiKeyStatuses) {
3014 var selectedModel = $('#model').val();
3015
3016 // Hide all status messages
3017 $('.mxchat-api-status').hide();
3018
3019 // Return early if no model is selected
3020 if (!selectedModel) {
3021 return;
3022 }
3023
3024 // Map models to providers
3025 var provider = null;
3026
3027 if (selectedModel.startsWith('gpt-')) {
3028 provider = 'openai';
3029 } else if (selectedModel.startsWith('claude-')) {
3030 provider = 'claude';
3031 } else if (selectedModel.startsWith('grok-')) {
3032 provider = 'xai';
3033 } else if (selectedModel.startsWith('deepseek-')) {
3034 provider = 'deepseek';
3035 } else if (selectedModel.startsWith('gemini-')) {
3036 provider = 'gemini';
3037 } else if (selectedModel === 'openrouter') {
3038 provider = 'openrouter';
3039 }
3040
3041 // If we have fresh API key data, update the messages
3042 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
3043 var $statusElement = $('.mxchat-api-status[data-provider="' + provider + '"]');
3044 var hasKey = apiKeyStatuses[provider];
3045
3046 if (hasKey) {
3047 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
3048 } else {
3049 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
3050 }
3051 }
3052
3053 // Show the appropriate status message
3054 if (provider) {
3055 $('.mxchat-api-status[data-provider="' + provider + '"]').show();
3056 }
3057 }
3058
3059 function updateEmbeddingModelAPIStatus(apiKeyStatuses) {
3060 var selectedModel = $('#embedding_model').val();
3061
3062 // Hide all status messages
3063 $('.mxchat-embedding-api-status').hide();
3064
3065 // Return early if no model is selected
3066 if (!selectedModel) {
3067 return;
3068 }
3069
3070 // Map models to providers
3071 var provider = null;
3072
3073 if (selectedModel.startsWith('text-embedding-')) {
3074 provider = 'openai';
3075 } else if (selectedModel.startsWith('voyage-')) {
3076 provider = 'voyage';
3077 } else if (selectedModel.startsWith('gemini-embedding-')) {
3078 provider = 'gemini';
3079 }
3080
3081 // If we have fresh API key data, update the messages
3082 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
3083 var $statusElement = $('.mxchat-embedding-api-status[data-provider="' + provider + '"]');
3084 var hasKey = apiKeyStatuses[provider];
3085
3086 if (hasKey) {
3087 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
3088 } else {
3089 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
3090 }
3091 }
3092
3093 // Show the appropriate status message
3094 if (provider) {
3095 $('.mxchat-embedding-api-status[data-provider="' + provider + '"]').show();
3096 }
3097 }
3098
3099 function getProviderName(provider) {
3100 var names = {
3101 'openai': 'OpenAI',
3102 'claude': 'Anthropic (Claude)',
3103 'xai': 'X.AI (Grok)',
3104 'deepseek': 'DeepSeek',
3105 'gemini': 'Google Gemini',
3106 'openrouter': 'OpenRouter',
3107 'voyage': 'Voyage AI'
3108 };
3109 return names[provider] || provider;
3110 }
3111
3112 function refreshAPIKeyStatus() {
3113 $.ajax({
3114 url: ajaxurl,
3115 type: 'POST',
3116 data: {
3117 action: 'mxchat_check_api_keys',
3118 nonce: mxchatAdmin.setting_nonce
3119 },
3120 success: function(response) {
3121 if (response.success && response.data) {
3122 updateChatModelAPIStatus(response.data);
3123 updateEmbeddingModelAPIStatus(response.data);
3124 }
3125 }
3126 });
3127 }
3128
3129 // Expose refresh function globally so auto-save can call it
3130 window.mxchatRefreshAPIKeyStatus = refreshAPIKeyStatus;
3131
3132 // Run on page load
3133 updateChatModelAPIStatus();
3134 updateEmbeddingModelAPIStatus();
3135
3136 // Check if we just saved settings (WordPress redirects with ?settings-updated=true)
3137 var urlParams = new URLSearchParams(window.location.search);
3138 if (urlParams.get('settings-updated') === 'true') {
3139 // Page was just reloaded after save, fetch fresh API key status
3140 refreshAPIKeyStatus();
3141 }
3142
3143 // Run when model changes
3144 $('#model').on('change', function() {
3145 updateChatModelAPIStatus();
3146 updateWebSearchToggleVisibility();
3147 });
3148 $('#embedding_model').on('change', function() { updateEmbeddingModelAPIStatus(); });
3149
3150 // Web Search toggle visibility based on model
3151 function updateWebSearchToggleVisibility() {
3152 var selectedModel = $('#model').val();
3153 var $wrapper = $('#web-search-toggle-wrapper');
3154 var $unavailableMessage = $('#web-search-unavailable-message');
3155
3156 if (!$wrapper.length) return; // Element doesn't exist
3157
3158 // Get the list of web-search-capable models from data attributes (OpenAI + Gemini)
3159 var openaiModelsAttr = $wrapper.data('openai-models');
3160 var geminiModelsAttr = $wrapper.data('gemini-models');
3161 var unsupportedModelsAttr = $wrapper.data('unsupported-models');
3162
3163 var openaiModels = openaiModelsAttr ? String(openaiModelsAttr).split(',') : [];
3164 var geminiModels = geminiModelsAttr ? String(geminiModelsAttr).split(',') : [];
3165 var unsupportedModels = unsupportedModelsAttr ? String(unsupportedModelsAttr).split(',') : [];
3166
3167 // Check if selected model is an OpenAI or Gemini model that supports web search
3168 var isCapable = openaiModels.includes(selectedModel) || geminiModels.includes(selectedModel);
3169 var isSupported = isCapable && !unsupportedModels.includes(selectedModel);
3170
3171 if (isSupported) {
3172 $wrapper.show();
3173 $unavailableMessage.hide();
3174 } else {
3175 $wrapper.hide();
3176 $unavailableMessage.show();
3177 }
3178 }
3179
3180 // Run on page load
3181 updateWebSearchToggleVisibility();
3182 });
3183
3184 // Transcripts Metrics Dashboard
3185 jQuery(document).ready(function($) {
3186 // Tab switching functionality
3187 $('.mxchat-metrics-tab').on('click', function() {
3188 const tabName = $(this).data('tab');
3189
3190 // Update tab buttons
3191 $('.mxchat-metrics-tab').removeClass('active');
3192 $(this).addClass('active');
3193
3194 // Update panels
3195 $('.mxchat-metrics-panel').removeClass('active');
3196 $(`.mxchat-metrics-panel[data-panel="${tabName}"]`).addClass('active');
3197
3198 // Initialize chart if activity tab is shown
3199 if (tabName === 'activity' && typeof mxchatChartData !== 'undefined') {
3200 // Small delay to ensure the canvas is visible
3201 setTimeout(function() {
3202 initActivityChart();
3203 }, 50);
3204 }
3205 });
3206
3207 // Initialize chart function
3208 function initActivityChart() {
3209 const canvas = document.getElementById('mxchat-activity-chart');
3210 if (!canvas) return;
3211
3212 // Check if chart already exists and destroy it
3213 if (canvas.chartInstance) {
3214 canvas.chartInstance.destroy();
3215 }
3216
3217 const ctx = canvas.getContext('2d');
3218
3219 // Create gradient for chats line
3220 const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
3221 chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
3222 chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
3223
3224 // Create gradient for messages line
3225 const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
3226 messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
3227 messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
3228
3229 // Simple chart without external library
3230 canvas.chartInstance = new SimpleChart(canvas, {
3231 labels: mxchatChartData.labels,
3232 datasets: [
3233 {
3234 label: 'Chats',
3235 data: mxchatChartData.chats,
3236 borderColor: '#667eea',
3237 backgroundColor: chatsGradient,
3238 fill: true
3239 },
3240 {
3241 label: 'Messages',
3242 data: mxchatChartData.messages,
3243 borderColor: '#764ba2',
3244 backgroundColor: messagesGradient,
3245 fill: true
3246 }
3247 ]
3248 });
3249 }
3250
3251 // Simple chart implementation (no external dependencies)
3252 class SimpleChart {
3253 constructor(canvas, config) {
3254 this.canvas = canvas;
3255 this.ctx = canvas.getContext('2d');
3256 this.config = config;
3257 this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
3258 this.render();
3259 }
3260
3261 destroy() {
3262 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3263 }
3264
3265 render() {
3266 const dpr = window.devicePixelRatio || 1;
3267 const rect = this.canvas.getBoundingClientRect();
3268
3269 this.canvas.width = rect.width * dpr;
3270 this.canvas.height = rect.height * dpr;
3271 this.ctx.scale(dpr, dpr);
3272
3273 this.canvas.style.width = rect.width + 'px';
3274 this.canvas.style.height = rect.height + 'px';
3275
3276 const width = rect.width - this.padding.left - this.padding.right;
3277 const height = rect.height - this.padding.top - this.padding.bottom;
3278
3279 // Find max value
3280 let maxValue = 0;
3281 this.config.datasets.forEach(dataset => {
3282 const max = Math.max(...dataset.data);
3283 if (max > maxValue) maxValue = max;
3284 });
3285
3286 // Add some padding to max value
3287 maxValue = Math.ceil(maxValue * 1.1);
3288 if (maxValue === 0) maxValue = 10;
3289
3290 // Draw grid lines
3291 this.ctx.strokeStyle = '#e5e7eb';
3292 this.ctx.lineWidth = 1;
3293 const gridLines = 5;
3294
3295 for (let i = 0; i <= gridLines; i++) {
3296 const y = this.padding.top + (height / gridLines) * i;
3297 this.ctx.beginPath();
3298 this.ctx.moveTo(this.padding.left, y);
3299 this.ctx.lineTo(this.padding.left + width, y);
3300 this.ctx.stroke();
3301
3302 // Draw y-axis labels
3303 const value = maxValue - (maxValue / gridLines) * i;
3304 this.ctx.fillStyle = '#6b7280';
3305 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3306 this.ctx.textAlign = 'right';
3307 this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
3308 }
3309
3310 // Draw datasets
3311 this.config.datasets.forEach(dataset => {
3312 const points = [];
3313 const xStep = width / (this.config.labels.length - 1 || 1);
3314
3315 dataset.data.forEach((value, index) => {
3316 const x = this.padding.left + (xStep * index);
3317 const y = this.padding.top + height - (value / maxValue * height);
3318 points.push({ x, y, value });
3319 });
3320
3321 // Draw filled area
3322 if (dataset.fill && dataset.backgroundColor) {
3323 this.ctx.fillStyle = dataset.backgroundColor;
3324 this.ctx.beginPath();
3325 this.ctx.moveTo(points[0].x, this.padding.top + height);
3326 points.forEach(point => {
3327 this.ctx.lineTo(point.x, point.y);
3328 });
3329 this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
3330 this.ctx.closePath();
3331 this.ctx.fill();
3332 }
3333
3334 // Draw line
3335 this.ctx.strokeStyle = dataset.borderColor;
3336 this.ctx.lineWidth = 3;
3337 this.ctx.lineCap = 'round';
3338 this.ctx.lineJoin = 'round';
3339
3340 this.ctx.beginPath();
3341 points.forEach((point, index) => {
3342 if (index === 0) {
3343 this.ctx.moveTo(point.x, point.y);
3344 } else {
3345 this.ctx.lineTo(point.x, point.y);
3346 }
3347 });
3348 this.ctx.stroke();
3349
3350 // Draw points
3351 points.forEach(point => {
3352 this.ctx.fillStyle = '#ffffff';
3353 this.ctx.beginPath();
3354 this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
3355 this.ctx.fill();
3356 this.ctx.strokeStyle = dataset.borderColor;
3357 this.ctx.lineWidth = 2;
3358 this.ctx.stroke();
3359 });
3360 });
3361
3362 // Draw x-axis labels
3363 const xStep = width / (this.config.labels.length - 1 || 1);
3364 this.ctx.fillStyle = '#6b7280';
3365 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3366 this.ctx.textAlign = 'center';
3367
3368 this.config.labels.forEach((label, index) => {
3369 const x = this.padding.left + (xStep * index);
3370 this.ctx.fillText(label, x, this.padding.top + height + 20);
3371 });
3372
3373 // Draw legend
3374 let legendX = this.padding.left;
3375 const legendY = rect.height - 10;
3376
3377 this.config.datasets.forEach((dataset, index) => {
3378 // Color box
3379 this.ctx.fillStyle = dataset.borderColor;
3380 this.ctx.fillRect(legendX, legendY - 8, 12, 12);
3381
3382 // Label
3383 this.ctx.fillStyle = '#374151';
3384 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3385 this.ctx.textAlign = 'left';
3386 this.ctx.fillText(dataset.label, legendX + 18, legendY);
3387
3388 legendX += this.ctx.measureText(dataset.label).width + 40;
3389 });
3390 }
3391 }
3392
3393 // Initialize chart on page load if we're on the activity tab
3394 if ($('.mxchat-metrics-tab.active').data('tab') === 'activity' && typeof mxchatChartData !== 'undefined') {
3395 setTimeout(function() {
3396 initActivityChart();
3397 }, 100);
3398 }
3399
3400 // ========================================
3401 // SLACK TEST CONNECTION
3402 // ========================================
3403 $('#mxchat-test-slack-connection').on('click', function() {
3404 var $button = $(this);
3405 var $result = $('#mxchat-slack-test-result');
3406 var originalText = $button.html();
3407
3408 // Show loading state
3409 $button.prop('disabled', true).html(
3410 '<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>' +
3411 'Testing...'
3412 );
3413
3414 $.ajax({
3415 url: ajaxurl,
3416 type: 'POST',
3417 data: {
3418 action: 'mxchat_test_slack_connection',
3419 nonce: mxchatAdmin.nonce
3420 },
3421 success: function(response) {
3422 $result.show();
3423 if (response.success) {
3424 $result.html(
3425 '<div style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 16px; border-radius: 8px;">' +
3426 '<strong style="display: block; margin-bottom: 8px;">✓ Connection Successful!</strong>' +
3427 '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3428 '</div>'
3429 );
3430 } else {
3431 var bgColor = response.data.partial ? '#fff3cd' : '#f8d7da';
3432 var borderColor = response.data.partial ? '#ffeeba' : '#f5c6cb';
3433 var textColor = response.data.partial ? '#856404' : '#721c24';
3434 var icon = response.data.partial ? '' : '';
3435 var title = response.data.partial ? 'Partial Success - Missing Scopes' : 'Connection Failed';
3436
3437 $result.html(
3438 '<div style="background: ' + bgColor + '; border: 1px solid ' + borderColor + '; color: ' + textColor + '; padding: 16px; border-radius: 8px;">' +
3439 '<strong style="display: block; margin-bottom: 8px;">' + icon + ' ' + title + '</strong>' +
3440 '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3441 (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>' : '') +
3442 '</div>'
3443 );
3444 }
3445 },
3446 error: function() {
3447 $result.show().html(
3448 '<div style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 16px; border-radius: 8px;">' +
3449 '<strong>✗ Request Failed</strong><br>Could not connect to the server. Please try again.' +
3450 '</div>'
3451 );
3452 },
3453 complete: function() {
3454 $button.prop('disabled', false).html(originalText);
3455 }
3456 });
3457 });
3458
3459 // Helper function to escape HTML
3460 function escapeHtml(text) {
3461 var div = document.createElement('div');
3462 div.appendChild(document.createTextNode(text));
3463 return div.innerHTML;
3464 }
3465
3466 // ========================================
3467 // DEBUG & OPTIMIZATION TOOLS
3468 // ========================================
3469
3470 // Debug Mode Toggle
3471 $('#mxchat_debug_mode').on('change', function() {
3472 var $toggle = $(this);
3473 var enabled = $toggle.is(':checked') ? 'on' : 'off';
3474 var $label = $toggle.closest('label');
3475
3476 // Add loading indicator next to the toggle label
3477 var $indicator = $label.find('.mxchat-save-indicator');
3478 if ($indicator.length === 0) {
3479 $indicator = $('<span class="mxchat-save-indicator" style="margin-left: 10px;"></span>');
3480 $label.append($indicator);
3481 }
3482 $indicator.html('<span class="mxchat-saving-spinner"></span>').show();
3483
3484 $.ajax({
3485 url: ajaxurl,
3486 type: 'POST',
3487 data: {
3488 action: 'mxchat_toggle_debug_mode',
3489 enabled: enabled,
3490 _ajax_nonce: mxchatAdmin.setting_nonce
3491 },
3492 success: function(response) {
3493 if (response.success) {
3494 // Show success indicator
3495 $indicator.html('<span class="mxchat-save-success">✓</span>');
3496 setTimeout(function() {
3497 $indicator.fadeOut(300);
3498 }, 2000);
3499
3500 // Always refresh the log after toggling
3501 refreshDebugLog();
3502 } else {
3503 $indicator.html('<span class="mxchat-save-error">✗</span>');
3504 alert(response.data.message || 'Error toggling debug mode');
3505 $toggle.prop('checked', !$toggle.is(':checked'));
3506 }
3507 },
3508 error: function() {
3509 $indicator.html('<span class="mxchat-save-error">✗</span>');
3510 alert('Error toggling debug mode');
3511 $toggle.prop('checked', !$toggle.is(':checked'));
3512 }
3513 });
3514 });
3515
3516 // Refresh Debug Log
3517 function refreshDebugLog() {
3518 var $container = $('#mxchat-debug-log');
3519 var $countBadge = $('#mxchat-log-count');
3520
3521 $.ajax({
3522 url: ajaxurl,
3523 type: 'POST',
3524 data: {
3525 action: 'mxchat_get_debug_log',
3526 _ajax_nonce: mxchatAdmin.setting_nonce
3527 },
3528 success: function(response) {
3529 if (response.success) {
3530 var log = response.data.log;
3531 var count = response.data.count;
3532
3533 if (count > 0) {
3534 $countBadge.text(count + ' entries').show();
3535 var html = '<div class="mxchat-debug-log-entries">';
3536 log.forEach(function(entry) {
3537 var typeClass = 'mxchat-log-type-' + entry.type;
3538 var typeLabel = entry.type.replace(/_/g, ' ').toUpperCase();
3539 html += '<div class="mxchat-debug-log-entry ' + typeClass + '">';
3540 html += '<div class="mxchat-log-header">';
3541 html += '<span class="mxchat-log-type">' + escapeHtml(typeLabel) + '</span>';
3542 html += '<span class="mxchat-log-time">' + escapeHtml(entry.time) + '</span>';
3543 html += '</div>';
3544 html += '<div class="mxchat-log-message">' + escapeHtml(entry.message) + '</div>';
3545 if (entry.data) {
3546 html += '<div class="mxchat-log-data">' + escapeHtml(JSON.stringify(entry.data, null, 2)) + '</div>';
3547 }
3548 html += '</div>';
3549 });
3550 html += '</div>';
3551 $container.html(html);
3552 } else {
3553 $countBadge.hide();
3554 $container.html(
3555 '<div class="mxchat-debug-log-empty">' +
3556 '<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>' +
3557 '<p>No log entries yet. Enable debug mode to start logging.</p>' +
3558 '</div>'
3559 );
3560 }
3561 }
3562 }
3563 });
3564 }
3565
3566 // Load debug log on page load
3567 if ($('#mxchat-debug-log').length) {
3568 refreshDebugLog();
3569 }
3570
3571 // Refresh Log Button
3572 $('#mxchat-refresh-log').on('click', function() {
3573 var $btn = $(this);
3574 $btn.prop('disabled', true);
3575 refreshDebugLog();
3576 setTimeout(function() {
3577 $btn.prop('disabled', false);
3578 }, 500);
3579 });
3580
3581 // Clear Log Button
3582 $('#mxchat-clear-log').on('click', function() {
3583 if (!confirm('Are you sure you want to clear the debug log?')) {
3584 return;
3585 }
3586
3587 var $btn = $(this);
3588 $btn.prop('disabled', true);
3589
3590 $.ajax({
3591 url: ajaxurl,
3592 type: 'POST',
3593 data: {
3594 action: 'mxchat_clear_debug_log',
3595 _ajax_nonce: mxchatAdmin.setting_nonce
3596 },
3597 success: function(response) {
3598 if (response.success) {
3599 refreshDebugLog();
3600 } else {
3601 alert(response.data.message || 'Error clearing log');
3602 }
3603 },
3604 error: function() {
3605 alert('Error clearing log');
3606 },
3607 complete: function() {
3608 $btn.prop('disabled', false);
3609 }
3610 });
3611 });
3612
3613 // Export Settings Button
3614 $('#mxchat-export-settings').on('click', function() {
3615 var $btn = $(this);
3616 var originalHtml = $btn.html();
3617 $btn.prop('disabled', true).html(
3618 '<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...'
3619 );
3620
3621 $.ajax({
3622 url: ajaxurl,
3623 type: 'POST',
3624 data: {
3625 action: 'mxchat_export_settings',
3626 _ajax_nonce: mxchatAdmin.setting_nonce
3627 },
3628 success: function(response) {
3629 if (response.success) {
3630 // Create and download the JSON file
3631 var dataStr = JSON.stringify(response.data.settings, null, 2);
3632 var blob = new Blob([dataStr], { type: 'application/json' });
3633 var url = URL.createObjectURL(blob);
3634 var a = document.createElement('a');
3635 a.href = url;
3636 a.download = response.data.filename;
3637 document.body.appendChild(a);
3638 a.click();
3639 document.body.removeChild(a);
3640 URL.revokeObjectURL(url);
3641 } else {
3642 alert(response.data.message || 'Error exporting settings');
3643 }
3644 },
3645 error: function() {
3646 alert('Error exporting settings');
3647 },
3648 complete: function() {
3649 $btn.prop('disabled', false).html(originalHtml);
3650 }
3651 });
3652 });
3653
3654 // Reset Settings Modal
3655 var $resetModal = $('#mxchat-reset-modal');
3656 var $resetConfirmInput = $('#mxchat-reset-confirmation');
3657 var $resetConfirmBtn = $('#mxchat-reset-confirm');
3658
3659 $('#mxchat-reset-settings').on('click', function() {
3660 $resetModal.fadeIn(200);
3661 $resetConfirmInput.val('').focus();
3662 $resetConfirmBtn.prop('disabled', true);
3663 });
3664
3665 $('#mxchat-reset-modal-close, #mxchat-reset-cancel, .mxch-modal-backdrop').on('click', function() {
3666 $resetModal.fadeOut(200);
3667 });
3668
3669 $resetConfirmInput.on('input', function() {
3670 var value = $(this).val().toUpperCase();
3671 $resetConfirmBtn.prop('disabled', value !== 'RESET');
3672 });
3673
3674 $resetConfirmBtn.on('click', function() {
3675 var $btn = $(this);
3676 var confirmation = $resetConfirmInput.val();
3677
3678 $btn.prop('disabled', true).text('Resetting...');
3679
3680 $.ajax({
3681 url: ajaxurl,
3682 type: 'POST',
3683 data: {
3684 action: 'mxchat_reset_all_settings',
3685 confirmation: confirmation,
3686 _ajax_nonce: mxchatAdmin.setting_nonce
3687 },
3688 success: function(response) {
3689 if (response.success) {
3690 alert(response.data.message);
3691 window.location.reload();
3692 } else {
3693 alert(response.data.message || 'Error resetting settings');
3694 $btn.prop('disabled', false).text('Reset All Settings');
3695 }
3696 },
3697 error: function() {
3698 alert('Error resetting settings');
3699 $btn.prop('disabled', false).text('Reset All Settings');
3700 }
3701 });
3702 });
3703
3704 // Load debug log on page load if we're on the optimization section
3705 if ($('#optimization').hasClass('active') || window.location.hash === '#optimization') {
3706 refreshDebugLog();
3707 }
3708
3709 // Also refresh when switching to optimization tab
3710 $(document).on('click', '[data-section="optimization"]', function() {
3711 setTimeout(refreshDebugLog, 100);
3712 });
3713 });
3714
3715 // Global rate-limit usage: "Reset counter" button (plan-mxchat-20260603-e9b3f9)
3716 jQuery(document).ready(function($) {
3717 var $usage = $('#mxch-global-usage');
3718 if (!$usage.length) {
3719 return;
3720 }
3721
3722 $('#mxch-global-usage-reset').on('click', function() {
3723 var $btn = $(this);
3724 if (!window.confirm('Reset the global usage counter to zero now? This clears how many messages have been used in the current window.')) {
3725 return;
3726 }
3727
3728 var original = $btn.text();
3729 $btn.prop('disabled', true).text('Resetting…');
3730
3731 $.ajax({
3732 url: mxchatAdmin.ajax_url,
3733 type: 'POST',
3734 data: {
3735 action: 'mxchat_reset_global_rate_limit',
3736 bot_id: $usage.data('bot-id') || 'default',
3737 _ajax_nonce: $usage.data('reset-nonce')
3738 },
3739 success: function(response) {
3740 $btn.prop('disabled', false).text(original);
3741 if (response && response.success) {
3742 $('#mxch-global-usage-fill').css('width', (response.data.pct || 0) + '%');
3743 if (response.data.text) {
3744 $('#mxch-global-usage-text').text(response.data.text);
3745 }
3746 } else {
3747 window.alert((response && response.data && response.data.message) || 'Reset failed. Please try again.');
3748 }
3749 },
3750 error: function() {
3751 $btn.prop('disabled', false).text(original);
3752 window.alert('Reset failed. Please try again.');
3753 }
3754 });
3755 });
3756 });