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

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