PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.1
MxChat – AI Chatbot & Content Generation for WordPress v3.2.1
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / js / mxchat-admin.js

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

3,536 lines 163.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Simple debounce function implementation
2 function debounce(func, wait) {
3 let timeout;
4 return function executedFunction(...args) {
5 const later = () => {
6 clearTimeout(timeout);
7 func(...args);
8 };
9 clearTimeout(timeout);
10 timeout = setTimeout(later, wait);
11 };
12 }
13
14 // 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.4', label: 'GPT-5.4', description: 'Flagship reasoning and coding model' },
1032 { value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini', description: 'Fast and affordable with 400K context' },
1033 { value: 'gpt-5.4-nano', label: 'GPT-5.4 Nano', description: 'Fastest and cheapest for lightweight tasks' },
1034 { value: 'gpt-5.3-chat-latest', label: 'GPT-5.3 Chat', description: 'Optimized for natural conversations with reduced hallucinations' },
1035 { value: 'gpt-5.2', label: 'GPT-5.2', description: 'Best general-purpose & agentic model with fast responses' },
1036 { value: 'gpt-5.1-chat-latest', label: 'GPT-5.1 Chat Latest', description: 'Recommended for most use cases' },
1037 { value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1', description: 'Flagship for coding & agentic tasks with low reasoning (400K context)' },
1038 { value: 'gpt-5', label: 'GPT-5', description: 'Flagship for coding, reasoning, and agentic tasks across domains' },
1039 { value: 'gpt-5-mini', label: 'GPT-5 Mini', description: 'Fast and lightweight' },
1040 { value: 'gpt-5-nano', label: 'GPT-5 Nano', description: 'Fastest and cheapest; ideal for summarization and classification' },
1041 ],
1042 claude: [
1043 { value: 'claude-opus-4-6', label: 'Claude Opus 4.6', description: 'Most capable Claude model - recommended' },
1044 { value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', description: 'Latest Sonnet - excellent balance of speed and capability' },
1045 { value: 'claude-opus-4-5', label: 'Claude Opus 4.5', description: 'Highly capable for complex tasks' },
1046 { value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5', description: 'Best for complex agents and coding' },
1047 { value: 'claude-opus-4-1-20250805', label: 'Claude Opus 4.1', description: 'Exceptional for specialized complex tasks' },
1048 { value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5', description: 'Fastest and most intelligent Haiku' },
1049 { value: 'claude-opus-4-20250514', label: 'Claude 4 Opus', description: 'Complex tasks' },
1050 { value: 'claude-sonnet-4-20250514', label: 'Claude 4 Sonnet', description: 'High performance' },
1051 ],
1052 xai: [
1053 { value: 'grok-4-1-fast-reasoning', label: 'Grok 4.1 Fast (Reasoning)', description: '2M context window and reasoning' },
1054 { value: 'grok-4-1-fast-non-reasoning', label: 'Grok 4.1 Fast (Non-Reasoning)', description: '2M context window and faster responses' },
1055 { value: 'grok-4-0709', label: 'Grok 4', description: 'Latest flagship model - unparalleled performance in natural language, math and reasoning' },
1056 { value: 'grok-3-beta', label: 'Grok-3', description: 'Powerful model with 131K context' },
1057 { value: 'grok-3-fast-beta', label: 'Grok-3 Fast', description: 'High performance with faster responses' },
1058 { value: 'grok-3-mini-beta', label: 'Grok-3 Mini', description: 'Affordable model with good performance' },
1059 { value: 'grok-3-mini-fast-beta', label: 'Grok-3 Mini Fast', description: 'Quick and cost-effective' },
1060 { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' },
1061 ],
1062 deepseek: [
1063 { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
1064 ],
1065 };
1066
1067 let allModels = [];
1068 Object.keys(models).forEach(key => {
1069 if (category === 'all' || category === key) {
1070 allModels = allModels.concat(models[key]);
1071 }
1072 });
1073
1074 // Filter by search term if present
1075 if (filter) {
1076 const lowerFilter = filter.toLowerCase();
1077 allModels = allModels.filter(model =>
1078 model.label.toLowerCase().includes(lowerFilter) ||
1079 model.description.toLowerCase().includes(lowerFilter)
1080 );
1081 }
1082
1083 // Create model cards
1084 allModels.forEach(model => {
1085 const isSelected = $modelSelect.val() === model.value;
1086 const $modelCard = $(`
1087 <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
1088 <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
1089 <div class="mxchat-model-selector-info">
1090 <h4 class="mxchat-model-selector-title">${model.label}</h4>
1091 <p class="mxchat-model-selector-description">${model.description}</p>
1092 </div>
1093 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1094 </div>
1095 `);
1096 $grid.append($modelCard);
1097 });
1098 };
1099
1100 // Helper function to get icon for each model
1101 function getModelIcon(modelValue) {
1102 if (modelValue === 'openrouter') return '<span class="dashicons dashicons-networking" style="font-size: 24px; color: #6750A4;"></span>';
1103 if (modelValue.startsWith('gemini-')) return '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" class="mxchat-model-icon-gemini"><defs><path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"></path></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"></use></clipPath><path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"></path><path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"></path><path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"></path><path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"></path></svg>';
1104 if (modelValue.startsWith('gpt-')) return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" class="mxchat-model-icon-openai"><path fill="currentColor" d="M297 131a80.6 80.6 0 0 0-93.7-104.2 80.6 80.6 0 0 0-137 29A80.6 80.6 0 0 0 23 189a80.6 80.6 0 0 0 93.7 104.2 80.6 80.6 0 0 0 137-29A80.7 80.7 0 0 0 297.1 131zM176.9 299c-14 .1-27.6-4.8-38.4-13.8l1.9-1 63.7-36.9c3.3-1.8 5.3-5.3 5.2-9v-89.9l27 15.6c.3.1.4.4.5.7v74.4a60 60 0 0 1-60 60zM47.9 244a59.7 59.7 0 0 1-7.1-40.1l1.9 1.1 63.7 36.8c3.2 1.9 7.2 1.9 10.5 0l77.8-45V228c0 .3-.2.6-.4.8L129.9 266a60 60 0 0 1-82-22zM31.2 105c7-12.2 18-21.5 31.2-26.3v75.8c0 3.7 2 7.2 5.2 9l77.8 45-27 15.5a1 1 0 0 1-.9 0L53.1 187a60 60 0 0 1-22-82zm221.2 51.5-77.8-45 27-15.5a1 1 0 0 1 .9 0l64.4 37.1a60 60 0 0 1-9.3 108.2v-75.8c0-3.7-2-7.2-5.2-9zm26.8-40.4-1.9-1.1-63.7-36.8a10.4 10.4 0 0 0-10.5 0L125.4 123V92c0-.3 0-.6.3-.8L190.1 54a60 60 0 0 1 89.1 62.1zm-168.5 55.4-27-15.5a1 1 0 0 1-.4-.7V80.9a60 60 0 0 1 98.3-46.1l-1.9 1L116 72.8a10.3 10.3 0 0 0-5.2 9v89.8zm14.6-31.5 34.7-20 34.6 20v40L160 200l-34.7-20z"></path></svg>';
1105 if (modelValue.startsWith('claude-')) return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 176" fill="none" class="mxchat-model-icon-claude"><path fill="currentColor" d="m147.487 0l70.081 175.78H256L185.919 0zM66.183 106.221l23.98-61.774l23.98 61.774zM70.07 0L0 175.78h39.18l14.33-36.914h73.308l14.328 36.914h39.179L110.255 0z"></path></svg>';
1106 if (modelValue.startsWith('grok-')) return '<svg fill="currentColor" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="mxchat-model-icon-xai"><path d="M6.469 8.776L16.512 23h-4.464L2.005 8.776H6.47zm-.004 7.9l2.233 3.164L6.467 23H2l4.465-6.324zM22 2.582V23h-3.659V7.764L22 2.582zM22 1l-9.952 14.095-2.233-3.163L17.533 1H22z"></path></svg>';
1107 if (modelValue.startsWith('deepseek-')) return '<svg height="1em" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg" class="mxchat-model-icon-deepseek"><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="currentColor"></path></svg>';
1108 return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
1109 }
1110
1111 // Event handlers
1112 $modelSelectorButton.on('click', function() {
1113 $('#mxchat_model_selector_modal').show();
1114 window.populateModelsGrid('', 'all');
1115 });
1116
1117 $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
1118 $('#mxchat_model_selector_modal').hide();
1119 });
1120
1121 $('.mxchat-model-category-btn').on('click', function() {
1122 $('.mxchat-model-category-btn').removeClass('active');
1123 $(this).addClass('active');
1124 const category = $(this).data('category');
1125 const searchTerm = $('#mxchat_model_search_input').val();
1126 window.populateModelsGrid(searchTerm, category);
1127 });
1128
1129 $('#mxchat_model_search_input').on('input', function() {
1130 const searchTerm = $(this).val();
1131 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1132 window.populateModelsGrid(searchTerm, activeCategory);
1133 });
1134
1135 $(document).on('click', '.mxchat-model-selector-card', function() {
1136 const modelValue = $(this).data('value');
1137 const $modelSelect = $('#model');
1138 const $clickedCard = $(this);
1139
1140 // Check if OpenRouter was selected
1141 if (modelValue === 'openrouter') {
1142 // Load OpenRouter models instead of closing
1143 loadOpenRouterModels();
1144 } else {
1145 // Remove selection from all other cards
1146 $('.mxchat-model-selector-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1147
1148 // Add selection to clicked card immediately for instant feedback
1149 $clickedCard.addClass('mxchat-model-selected');
1150 if ($clickedCard.find('.mxchat-model-selector-checkmark').length === 0) {
1151 $clickedCard.append('<div class="mxchat-model-selector-checkmark">✓</div>');
1152 }
1153
1154 // Brief delay to show the selection, then start saving
1155 setTimeout(function() {
1156 // Normal model selection
1157 $modelSelect.val(modelValue).trigger('change');
1158
1159 // Show loading state on the card
1160 $clickedCard.css('pointer-events', 'none');
1161 const originalContent = $clickedCard.find('.mxchat-model-selector-title').html();
1162 $clickedCard.find('.mxchat-model-selector-title').html(
1163 '<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...'
1164 );
1165
1166 // Manually save the model via AJAX
1167 jQuery.ajax({
1168 url: mxchatAdmin.ajax_url,
1169 type: 'POST',
1170 data: {
1171 action: 'mxchat_save_setting',
1172 name: 'model',
1173 value: modelValue,
1174 _ajax_nonce: mxchatAdmin.setting_nonce
1175 },
1176 success: function(response) {
1177 if (response.success) {
1178 // Show success state
1179 $clickedCard.find('.mxchat-model-selector-title').html(
1180 '<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!'
1181 );
1182
1183 // Update button text after successful save
1184 const selectedModelText = $modelSelect.find('option:selected').text();
1185 $('#mxchat_model_selector_btn').text(selectedModelText);
1186
1187 // Close modal after a short delay to show the success message
1188 setTimeout(function() {
1189 $('#mxchat_model_selector_modal').hide();
1190 // Restore original content and remove selection for next time
1191 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1192 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1193 $clickedCard.css('pointer-events', 'auto');
1194 }, 600);
1195 } else {
1196 // Show error state
1197 $clickedCard.find('.mxchat-model-selector-title').html(
1198 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1199 );
1200
1201 setTimeout(function() {
1202 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1203 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1204 $clickedCard.css('pointer-events', 'auto');
1205 }, 1500);
1206 }
1207 },
1208 error: function() {
1209 // Show error state
1210 $clickedCard.find('.mxchat-model-selector-title').html(
1211 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1212 );
1213
1214 setTimeout(function() {
1215 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1216 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1217 $clickedCard.css('pointer-events', 'auto');
1218 }, 1500);
1219 }
1220 });
1221 }, 300); // 300ms delay to show the selection before starting to save
1222 }
1223 });
1224
1225 // Close modal when clicking outside
1226 $(window).on('click', function(event) {
1227 if ($(event.target).is('#mxchat_model_selector_modal')) {
1228 $('#mxchat_model_selector_modal').hide();
1229 }
1230 });
1231 }
1232
1233 function loadOpenRouterModels() {
1234 const apiKey = $('#openrouter_api_key').val(); // This line already re-checks the field
1235 const $modal = $('#mxchat_model_selector_modal');
1236 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1237
1238 if (!apiKey || apiKey.trim() === '') {
1239 // Show error message in modal
1240 $modalBody.html(`
1241 <div style="text-align: center; padding: 40px;">
1242 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1243 <h3>OpenRouter API Key Required</h3>
1244 <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>
1245 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1246 </div>
1247 `);
1248
1249 $('#mxchat_back_to_models').on('click', function(e) {
1250 e.preventDefault();
1251 // CHANGE THIS: Instead of reloading, restore the original modal content
1252 $modalBody.html(`
1253 <div class="mxchat-model-selector-search-container">
1254 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1255 </div>
1256 <div class="mxchat-model-selector-categories">
1257 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1258 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1259 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1260 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1261 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1262 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1263 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1264 </div>
1265 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1266 `);
1267
1268 // Re-populate the grid
1269 populateModelsGrid('', 'all');
1270
1271 // Re-bind event handlers
1272 rebindModalEventHandlers();
1273 });
1274 return;
1275 }
1276
1277 // Show loading state
1278 $modalBody.html(`
1279 <div style="text-align: center; padding: 60px 20px;">
1280 <div class="spinner is-active" style="float: none; margin: 0 auto 20px;"></div>
1281 <h3>Loading OpenRouter Models...</h3>
1282 <p>Fetching available models from OpenRouter</p>
1283 </div>
1284 `);
1285
1286 // Fetch models from OpenRouter
1287 jQuery.ajax({
1288 url: mxchatAdmin.ajax_url,
1289 type: 'POST',
1290 data: {
1291 action: 'mxchat_fetch_openrouter_models',
1292 api_key: apiKey,
1293 nonce: mxchatAdmin.fetch_openrouter_models_nonce
1294 },
1295 success: function(response) {
1296 if (response.success && response.data.models) {
1297 displayOpenRouterModels(response.data.models);
1298 } else {
1299 $modalBody.html(`
1300 <div style="text-align: center; padding: 40px;">
1301 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1302 <h3>Error Loading Models</h3>
1303 <p>${response.data.message || 'Failed to load models from OpenRouter'}</p>
1304 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1305 </div>
1306 `);
1307
1308 $('#mxchat_back_to_models').on('click', function(e) {
1309 e.preventDefault();
1310 // CHANGE THIS: Restore original content instead of reloading
1311 restoreOriginalModalContent();
1312 });
1313 }
1314 },
1315 error: function() {
1316 $modalBody.html(`
1317 <div style="text-align: center; padding: 40px;">
1318 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1319 <h3>Connection Error</h3>
1320 <p>Failed to connect to OpenRouter. Please check your API key and try again.</p>
1321 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1322 </div>
1323 `);
1324
1325 $('#mxchat_back_to_models').on('click', function(e) {
1326 e.preventDefault();
1327 // CHANGE THIS: Restore original content instead of reloading
1328 restoreOriginalModalContent();
1329 });
1330 }
1331 });
1332 }
1333 function restoreOriginalModalContent() {
1334 const $modalBody = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-body');
1335
1336 $modalBody.html(`
1337 <div class="mxchat-model-selector-search-container">
1338 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1339 </div>
1340 <div class="mxchat-model-selector-categories">
1341 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1342 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1343 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1344 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1345 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1346 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1347 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1348 </div>
1349 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1350 `);
1351
1352 // Re-populate the grid
1353 window.populateModelsGrid('', 'all');
1354
1355 // Re-bind event handlers
1356 rebindModalEventHandlers();
1357 }
1358 function rebindModalEventHandlers() {
1359 const $modal = $('#mxchat_model_selector_modal');
1360
1361 // Re-bind category button clicks
1362 $('.mxchat-model-category-btn').off('click').on('click', function() {
1363 $('.mxchat-model-category-btn').removeClass('active');
1364 $(this).addClass('active');
1365 const category = $(this).data('category');
1366 const searchTerm = $('#mxchat_model_search_input').val();
1367 window.populateModelsGrid(searchTerm, category);
1368 });
1369
1370 // Re-bind search input
1371 $('#mxchat_model_search_input').off('input').on('input', function() {
1372 const searchTerm = $(this).val();
1373 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1374 populateModelsGrid(searchTerm, activeCategory);
1375 });
1376 }
1377
1378 function restoreDefaultModalFooter() {
1379 const $modalFooter = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-footer');
1380
1381 // Restore default footer buttons
1382 $modalFooter.html(`
1383 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
1384 `);
1385
1386 // Re-bind cancel button
1387 $('#mxchat_cancel_model_selection').on('click', function() {
1388 $('#mxchat_model_selector_modal').hide();
1389 });
1390 }
1391
1392 function displayOpenRouterModels(models) {
1393 const $modal = $('#mxchat_model_selector_modal');
1394 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1395 const $modalFooter = $modal.find('.mxchat-model-selector-modal-footer');
1396 const currentSelected = $('#openrouter_selected_model').val();
1397
1398 // Variable to store the currently selected model (in the UI, not yet saved)
1399 let pendingSelection = {
1400 modelId: currentSelected || null,
1401 modelName: $('#openrouter_selected_model_name').val() || null
1402 };
1403
1404 // Build new modal content with search and models
1405 const newContent = `
1406 <div class="mxchat-model-selector-search-container">
1407 <input type="text" id="mxchat_openrouter_search" class="mxchat-model-search-input" placeholder="Search OpenRouter models...">
1408 <p style="margin: 10px 0; color: #666; font-size: 13px;">
1409 <strong>${models.length} models available</strong> ·
1410 <a href="#" id="mxchat_back_to_provider_select" style="color: #2271b1;">← Back to providers</a>
1411 </p>
1412 </div>
1413 <div class="mxchat-model-selector-grid" id="mxchat_openrouter_models_grid"></div>
1414 `;
1415
1416 // Update footer with Save button for OpenRouter
1417 const footerContent = `
1418 <button id="mxchat_back_to_models_footer" class="button mxchat-model-cancel-btn">Back to Providers</button>
1419 <button id="mxchat_save_openrouter_model" class="button button-primary" disabled>
1420 <span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model
1421 </button>
1422 `;
1423
1424 $modalBody.html(newContent);
1425 $modalFooter.html(footerContent);
1426
1427 // Function to render models
1428 function renderOpenRouterModels(filterText = '') {
1429 const $grid = $('#mxchat_openrouter_models_grid');
1430 $grid.empty();
1431
1432 let filteredModels = models;
1433 if (filterText) {
1434 const lowerFilter = filterText.toLowerCase();
1435 filteredModels = models.filter(m =>
1436 m.id.toLowerCase().includes(lowerFilter) ||
1437 m.name.toLowerCase().includes(lowerFilter) ||
1438 (m.description && m.description.toLowerCase().includes(lowerFilter))
1439 );
1440 }
1441
1442 filteredModels.forEach(model => {
1443 const isSelected = pendingSelection.modelId === model.id;
1444 const contextLength = model.context_length ? `${(model.context_length / 1000).toFixed(0)}K` : '';
1445 const promptPrice = model.pricing.prompt ? `$${(model.pricing.prompt * 1000000).toFixed(2)}/1M` : '';
1446
1447 const $card = jQuery(`
1448 <div class="mxchat-openrouter-card ${isSelected ? 'mxchat-model-selected' : ''}" data-model-id="${model.id}" data-model-name="${model.name}">
1449 <div class="mxchat-model-selector-icon">
1450 ${getOpenRouterIcon(model.id)}
1451 </div>
1452 <div class="mxchat-model-selector-info">
1453 <h4 class="mxchat-model-selector-title">${model.name}</h4>
1454 <div style="font-size: 12px; color: #666; margin-top: 5px;">
1455 ${contextLength ? '<span style="margin-right: 12px;">📄 ' + contextLength + '</span>' : ''}
1456 ${promptPrice ? '<span>💰 ' + promptPrice + '</span>' : ''}
1457 </div>
1458 </div>
1459 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1460 </div>
1461 `);
1462
1463 $grid.append($card);
1464 });
1465 }
1466
1467 // Helper to get icon
1468 function getOpenRouterIcon(modelId) {
1469 if (modelId.includes('gpt') || modelId.includes('openai')) {
1470 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>';
1471 } else if (modelId.includes('claude') || modelId.includes('anthropic')) {
1472 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>';
1473 } else if (modelId.includes('gemini') || modelId.includes('google')) {
1474 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>';
1475 }
1476 return '<span class="dashicons dashicons-cloud" style="font-size: 24px; color: #6750A4;"></span>';
1477 }
1478
1479 // Initial render
1480 renderOpenRouterModels();
1481
1482 // Search handler
1483 $('#mxchat_openrouter_search').on('input', function() {
1484 renderOpenRouterModels($(this).val());
1485 });
1486
1487 $('#mxchat_back_to_provider_select').on('click', function(e) {
1488 e.preventDefault();
1489 // Restore footer to default state
1490 restoreDefaultModalFooter();
1491 // Instead of location.reload(), restore original content
1492 restoreOriginalModalContent();
1493 });
1494
1495 // Back to providers footer button
1496 $('#mxchat_back_to_models_footer').on('click', function(e) {
1497 e.preventDefault();
1498 // Restore footer to default state
1499 restoreDefaultModalFooter();
1500 // Restore original content
1501 restoreOriginalModalContent();
1502 });
1503
1504 // Model selection - just highlight, don't save yet
1505 $(document).on('click', '.mxchat-openrouter-card', function(e) {
1506 e.stopPropagation(); // Prevent triggering the regular model card handler
1507
1508 const modelId = $(this).data('model-id');
1509 const modelName = $(this).data('model-name');
1510
1511 // Remove selection from all cards
1512 $('.mxchat-openrouter-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1513
1514 // Add selection to clicked card
1515 $(this).addClass('mxchat-model-selected');
1516 if ($(this).find('.mxchat-model-selector-checkmark').length === 0) {
1517 $(this).append('<div class="mxchat-model-selector-checkmark">✓</div>');
1518 }
1519
1520 // Update pending selection
1521 pendingSelection.modelId = modelId;
1522 pendingSelection.modelName = modelName;
1523
1524 // Enable the save button
1525 $('#mxchat_save_openrouter_model').prop('disabled', false);
1526 });
1527
1528 // Save button handler
1529 $('#mxchat_save_openrouter_model').on('click', function() {
1530 const $saveButton = $(this);
1531
1532 if (!pendingSelection.modelId) {
1533 return;
1534 }
1535
1536 // Disable button and show loading state
1537 $saveButton.prop('disabled', true).html('<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...');
1538
1539 // First, save that we're using OpenRouter
1540 jQuery.ajax({
1541 url: mxchatAdmin.ajax_url,
1542 type: 'POST',
1543 data: {
1544 action: 'mxchat_save_setting',
1545 name: 'model',
1546 value: 'openrouter',
1547 _ajax_nonce: mxchatAdmin.setting_nonce
1548 },
1549 success: function() {
1550 // After model is set, save the model ID
1551 jQuery.ajax({
1552 url: mxchatAdmin.ajax_url,
1553 type: 'POST',
1554 data: {
1555 action: 'mxchat_save_setting',
1556 name: 'openrouter_selected_model',
1557 value: pendingSelection.modelId,
1558 _ajax_nonce: mxchatAdmin.setting_nonce
1559 },
1560 success: function() {
1561 // Update DOM immediately
1562 $('#openrouter_selected_model').val(pendingSelection.modelId);
1563
1564 // After model ID is saved, save the display name
1565 jQuery.ajax({
1566 url: mxchatAdmin.ajax_url,
1567 type: 'POST',
1568 data: {
1569 action: 'mxchat_save_setting',
1570 name: 'openrouter_selected_model_name',
1571 value: pendingSelection.modelName,
1572 _ajax_nonce: mxchatAdmin.setting_nonce
1573 },
1574 success: function() {
1575 // Update DOM immediately
1576 $('#openrouter_selected_model_name').val(pendingSelection.modelName);
1577
1578 // Update button text
1579 $('#mxchat_model_selector_btn').text('OpenRouter: ' + pendingSelection.modelName);
1580
1581 // Update the "Currently using" message
1582 const $currentSelection = $('#openrouter-current-selection');
1583 if ($currentSelection.length) {
1584 $currentSelection.html(
1585 '<span class="dashicons dashicons-yes" style="font-size: 16px; vertical-align: middle;"></span> ' +
1586 'Currently using: <strong>' + pendingSelection.modelName + '</strong>'
1587 ).show();
1588 }
1589
1590 // Show success state briefly
1591 $saveButton.html('<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!');
1592
1593 // Close modal after short delay
1594 setTimeout(function() {
1595 // Restore footer to default state
1596 restoreDefaultModalFooter();
1597 $modal.hide();
1598 }, 800);
1599 },
1600 error: function() {
1601 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1602 alert('Failed to save model name. Please try again.');
1603 }
1604 });
1605 },
1606 error: function() {
1607 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1608 alert('Failed to save model ID. Please try again.');
1609 }
1610 });
1611 },
1612 error: function() {
1613 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1614 alert('Failed to save OpenRouter selection. Please try again.');
1615 }
1616 });
1617 });
1618
1619 }
1620
1621 // Embedding model selector - completely separate from chat model selector
1622 function setupMxChatEmbeddingModelSelector() {
1623 const $embeddingModelSelect = $('#embedding_model');
1624
1625 // Skip if the element doesn't exist on the page
1626 if ($embeddingModelSelect.length === 0) {
1627 return;
1628 }
1629
1630 const $embeddingModelSelectorButton = $('<button>', {
1631 type: 'button',
1632 id: 'mxchat_embedding_model_selector_btn',
1633 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
1634 text: 'Select Embedding Model'
1635 });
1636
1637 // Replace the select dropdown with a button
1638 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
1639
1640 // Update button text to show currently selected model
1641 function updateButtonText() {
1642 const selectedModel = $embeddingModelSelect.val();
1643 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
1644 $embeddingModelSelectorButton.text(selectedModelText);
1645 }
1646
1647 // Initialize button text
1648 updateButtonText();
1649
1650 // Create a unique ID for the modal to avoid conflicts
1651 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
1652
1653 // Create and append modal HTML with unique IDs
1654 const embeddingModelSelectorModal = `
1655 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
1656 <div class="mxchat-embedding-model-selector-modal-content">
1657 <div class="mxchat-embedding-model-selector-modal-header">
1658 <h3>Select Embedding Model</h3>
1659 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
1660 </div>
1661 <div class="mxchat-embedding-model-selector-modal-body">
1662 <div class="mxchat-embedding-model-selector-search-container">
1663 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
1664 </div>
1665 <div class="mxchat-embedding-model-selector-categories">
1666 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
1667 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
1668 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
1669 <button class="mxchat-embedding-model-category-btn" data-category="gemini">Google Gemini</button>
1670 </div>
1671 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
1672 </div>
1673 <div class="mxchat-embedding-model-selector-modal-footer">
1674 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
1675 </div>
1676 </div>
1677 </div>
1678 `;
1679
1680 // Use jQuery's append to ensure it doesn't clash with existing modals
1681 $('body').append(embeddingModelSelectorModal);
1682
1683 // Populate models grid
1684 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1685 const $grid = $('#mxchat_embedding_models_grid');
1686 $grid.empty();
1687
1688 // Define embedding models with descriptions and context lengths
1689 const models = {
1690 openai: [
1691 {
1692 value: 'text-embedding-3-small',
1693 label: 'TE3 Small',
1694 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1695 },
1696 {
1697 value: 'text-embedding-ada-002',
1698 label: 'Ada 2',
1699 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1700 },
1701 {
1702 value: 'text-embedding-3-large',
1703 label: 'TE3 Large',
1704 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1705 }
1706 ],
1707 voyage: [
1708 {
1709 value: 'voyage-3-large',
1710 label: 'Voyage-3 Large',
1711 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1712 }
1713 ],
1714 gemini: [
1715 {
1716 value: 'gemini-embedding-001',
1717 label: 'Gemini Embedding',
1718 description: 'Stable SOTA embeddings (1536 dimensions, 8K context)'
1719 }
1720 ]
1721 };
1722
1723 let allModels = [];
1724 Object.keys(models).forEach(key => {
1725 if (category === 'all' || category === key) {
1726 allModels = allModels.concat(models[key]);
1727 }
1728 });
1729
1730 // Filter by search term if present
1731 if (filter) {
1732 const lowerFilter = filter.toLowerCase();
1733 allModels = allModels.filter(model =>
1734 model.label.toLowerCase().includes(lowerFilter) ||
1735 model.description.toLowerCase().includes(lowerFilter)
1736 );
1737 }
1738
1739 // Create model cards
1740 allModels.forEach(model => {
1741 const isSelected = $embeddingModelSelect.val() === model.value;
1742 let providerClass = 'mxchat-embedding-model-provider-openai';
1743
1744 if (model.value.startsWith('voyage-')) {
1745 providerClass = 'mxchat-embedding-model-provider-voyage';
1746 } else if (model.value.startsWith('gemini-embedding-')) {
1747 providerClass = 'mxchat-embedding-model-provider-gemini';
1748 }
1749
1750 let iconHTML = '';
1751 if (model.value.startsWith('voyage-')) {
1752 iconHTML = '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>';
1753 } else if (model.value.startsWith('gemini-embedding-')) {
1754 iconHTML = '<span class="dashicons dashicons-google mxchat-embedding-model-icon-gemini"></span>';
1755 } else {
1756 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>';
1757 }
1758
1759 const $modelCard = $(`
1760 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1761 <div class="mxchat-embedding-model-selector-icon">
1762 ${iconHTML}
1763 </div>
1764 <div class="mxchat-embedding-model-selector-info">
1765 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1766 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1767 </div>
1768 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1769 </div>
1770 `);
1771
1772 $grid.append($modelCard);
1773 });
1774 }
1775
1776 // Event handlers - use namespaced events to avoid conflicts
1777 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1778 e.stopPropagation(); // Prevent event bubbling
1779 $('#' + embeddingModalId).show();
1780 populateEmbeddingModelsGrid('', 'all');
1781 });
1782
1783 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1784 e.stopPropagation(); // Prevent event bubbling
1785 $('#' + embeddingModalId).hide();
1786 });
1787
1788 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1789 e.stopPropagation(); // Prevent event bubbling
1790 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1791 $(this).addClass('active');
1792 const category = $(this).data('category');
1793 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1794 populateEmbeddingModelsGrid(searchTerm, category);
1795 });
1796
1797 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1798 const searchTerm = $(this).val();
1799 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1800 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1801 });
1802
1803 // Use a direct selector to avoid conflicts with other card elements
1804 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1805 e.stopPropagation(); // Prevent event bubbling
1806 const modelValue = $(this).data('value');
1807
1808 // Important: Only update this specific select element
1809 $embeddingModelSelect.val(modelValue);
1810
1811 // Manually trigger change only on this element
1812 const changeEvent = new Event('change', { bubbles: true });
1813 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1814
1815 // Update button text
1816 updateButtonText();
1817
1818 // Hide modal
1819 $('#' + embeddingModalId).hide();
1820 });
1821
1822 // Close modal when clicking outside - use namespaced events
1823 $(window).on('click.embeddingModelSelector', function(event) {
1824 if ($(event.target).is('#' + embeddingModalId)) {
1825 $('#' + embeddingModalId).hide();
1826 }
1827 });
1828 }
1829
1830 // Call this function after the DOM is fully loaded
1831 $(document).ready(function() {
1832 setupMxChatModelSelector();
1833 setupMxChatEmbeddingModelSelector();
1834 });
1835
1836 // Add Intent Form Submission
1837 $('#mxchat-add-intent-form').on('submit', function(event) {
1838 $('#mxchat-intent-loading').show();
1839 $('#mxchat-intent-loading-text').show();
1840 $(this).find('button[type="submit"]').hide();
1841 });
1842
1843 // Inline Edit Functionality
1844 $('.edit-button').on('click', function() {
1845 var row = $(this).closest('tr');
1846
1847 // Clear URL field if it's a manual content URL (mxchat:// protocol)
1848 var urlEdit = row.find('.url-edit');
1849 if (urlEdit.length && urlEdit.val().indexOf('mxchat://') === 0) {
1850 urlEdit.val('');
1851 }
1852
1853 // Expand the accordion to show the edit textarea (fixes short content editing)
1854 var contentFull = row.find('.mxchat-content-full');
1855 if (contentFull.length && contentFull.is(':hidden')) {
1856 contentFull.show();
1857 row.find('.mxchat-content-preview').hide();
1858 }
1859
1860 row.find('.content-view, .url-view').hide();
1861 row.find('.content-edit, .url-edit').show();
1862 row.find('.edit-button').hide();
1863 row.find('.save-button').show();
1864 });
1865
1866 // Save button handler
1867 // Save button handler
1868 $('.save-button').on('click', function() {
1869 var button = $(this);
1870 var row = button.closest('tr');
1871 var id = button.data('id');
1872 var nonce = button.data('nonce'); // Get nonce from button data attribute
1873 var newContent = row.find('.content-edit').val();
1874 var newUrl = row.find('.url-edit').val();
1875
1876 //console.log('Nonce from button:', nonce); // Debug
1877
1878 button.prop('disabled', true);
1879 button.text('Saving...');
1880
1881 $.ajax({
1882 url: mxchatAdmin.ajax_url,
1883 type: 'POST',
1884 data: {
1885 action: 'mxchat_save_inline_prompt',
1886 id: id,
1887 article_content: newContent,
1888 article_url: newUrl,
1889 _ajax_nonce: nonce // Use nonce from button
1890 },
1891 success: function(response) {
1892 button.prop('disabled', false);
1893 button.text('Save');
1894
1895 if (response.success) {
1896 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1897 if (newUrl) {
1898 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank"><span class="dashicons dashicons-external"></span> View Source</a>');
1899 } else {
1900 row.find('.url-view').html('<span class="mxchat-na">Manual Content</span>');
1901 }
1902
1903 row.find('.content-edit, .url-edit').hide();
1904 row.find('.content-view, .url-view').show();
1905 row.find('.save-button').hide();
1906 row.find('.edit-button').show();
1907
1908 // Restore accordion state - show preview, hide full content
1909 row.find('.mxchat-content-preview').show();
1910 row.find('.mxchat-content-full').hide();
1911 } else {
1912 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1913 }
1914 },
1915 error: function() {
1916 button.prop('disabled', false);
1917 button.text('Save');
1918 alert('An error occurred while saving.');
1919 }
1920 });
1921 });
1922
1923
1924 // Questions handling
1925 $('.mxchat-add-question').on('click', function () {
1926 const container = $('#mxchat-additional-questions-container');
1927 const questionCount = container.find('.mxchat-question-row').length + 4;
1928 const questionIndex = container.find('.mxchat-question-row').length;
1929
1930 const newQuestion = `
1931 <div class="mxchat-question-row">
1932 <input type="text"
1933 name="additional_popular_questions[]"
1934 placeholder="Enter Additional Popular Question ${questionCount}"
1935 class="regular-text mxchat-question-input"
1936 data-question-index="${questionIndex}" />
1937 <button type="button" class="button mxchat-remove-question"
1938 aria-label="Remove question">Remove</button>
1939 </div>
1940 `;
1941 container.append(newQuestion);
1942 });
1943
1944 $(document).on('click', '.mxchat-remove-question', function () {
1945 $(this).closest('.mxchat-question-row').remove();
1946 saveQuestions();
1947 });
1948
1949 $(document).on('change', '.mxchat-question-input', function() {
1950 saveQuestions();
1951 });
1952
1953 function saveQuestions() {
1954 const questions = [];
1955 $('.mxchat-question-input').each(function() {
1956 const value = $(this).val().trim();
1957 if (value) {
1958 questions.push(value);
1959 }
1960 });
1961
1962 const feedbackContainer = $('<div class="feedback-container"></div>');
1963 const spinner = $('<div class="saving-spinner"></div>');
1964 const successIcon = $('<div class="success-icon">✔</div>');
1965
1966 // Append feedback after the add button
1967 $('.mxchat-add-question').after(feedbackContainer);
1968 feedbackContainer.append(spinner);
1969
1970 // Save via AJAX
1971 $.ajax({
1972 url: mxchatAdmin.ajax_url,
1973 type: 'POST',
1974 data: {
1975 action: 'mxchat_save_setting',
1976 name: 'additional_popular_questions',
1977 value: JSON.stringify(questions),
1978 _ajax_nonce: mxchatAdmin.setting_nonce
1979 },
1980 success: function(response) {
1981 if (response.success) {
1982 spinner.fadeOut(200, function() {
1983 feedbackContainer.append(successIcon);
1984 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
1985 feedbackContainer.remove();
1986 });
1987 });
1988 } else {
1989 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
1990 feedbackContainer.remove();
1991 }
1992 },
1993 error: function() {
1994 alert('An error occurred while saving questions.');
1995 feedbackContainer.remove();
1996 }
1997 });
1998 }
1999
2000 // Live agent status handler
2001 const statusToggle = document.getElementById('live_agent_status');
2002 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
2003 if (statusToggle && statusText) {
2004 statusToggle.addEventListener('change', function() {
2005 // Update display text
2006 statusText.textContent = this.checked ? 'Online' : 'Offline';
2007
2008 // Send the correct on/off value to the server
2009 if (window.mxchatSaveSetting) {
2010 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
2011 }
2012 });
2013 }
2014
2015 // Function to adjust the textarea height to content
2016 function adjustTextareaHeight() {
2017 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
2018 this.style.height = this.scrollHeight + 'px'; // Expand to content height
2019 }
2020
2021 // Function to reset the textarea height to initial
2022 function resetTextareaHeight() {
2023 this.style.height = ''; // Remove inline height, reverting to CSS default
2024 }
2025
2026 // Target the specific textarea by ID
2027 var $textarea = $('#system_prompt_instructions');
2028
2029 // Bind events
2030 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
2031 .on('blur', resetTextareaHeight); // Reset on blur
2032 });
2033
2034
2035
2036 document.addEventListener('DOMContentLoaded', function() {
2037 // Check if we're on the correct page before initializing
2038 const modal = document.getElementById('mxchat-action-modal');
2039
2040 // Only initialize if the modal exists on this page
2041 if (modal) {
2042 //console.log('MXChat Action Modal JS Loaded');
2043
2044 // Initialize the action modal functionality
2045 initStepBasedActionModal();
2046 }
2047
2048 // Function to initialize the step-based action modal
2049 function initStepBasedActionModal() {
2050 // We already checked for modal existence above, so no need to check again
2051
2052 const actionStep1 = document.getElementById('mxchat-action-step-1');
2053 const actionStep2 = document.getElementById('mxchat-action-step-2');
2054 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
2055 const searchInput = document.getElementById('action-type-search');
2056 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
2057 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
2058 const actionForm = document.getElementById('mxchat-action-form');
2059 const callbackInput = document.getElementById('callback_function');
2060 const actionIdField = document.getElementById('edit_action_id');
2061 const labelField = document.getElementById('intent_label');
2062 const phrasesField = document.getElementById('action_phrases');
2063 const formActionType = document.getElementById('form_action_type');
2064 const nonceContainer = document.getElementById('action-nonce-container');
2065 const thresholdSlider = document.getElementById('similarity_threshold');
2066 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2067
2068 // Rest of your initialization code remains the same...
2069
2070 // Log the structure of one action card for debugging
2071 if (actionCards.length > 0) {
2072 //console.log('First action card data attributes:', actionCards[0].dataset);
2073 //console.log('First action card HTML:', actionCards[0].outerHTML);
2074 }
2075
2076 // Add click event listeners to category buttons
2077 categoryButtons.forEach(button => {
2078 button.addEventListener('click', function() {
2079 //console.log('Category button clicked:', this.dataset.category);
2080
2081 // Remove active class from all buttons
2082 categoryButtons.forEach(btn => btn.classList.remove('active'));
2083
2084 // Add active class to clicked button
2085 this.classList.add('active');
2086
2087 // Get selected category
2088 const category = this.dataset.category;
2089
2090 // Filter action cards
2091 filterActionCards(category, searchInput.value);
2092 });
2093 });
2094
2095 // Add search functionality
2096 if (searchInput) {
2097 searchInput.addEventListener('input', function() {
2098 // Get active category
2099 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
2100 //console.log('Search input changed, active category:', activeCategory);
2101
2102 // Filter action cards
2103 filterActionCards(activeCategory, this.value);
2104 });
2105 }
2106
2107 // Add click event listeners to action cards
2108 actionCards.forEach(card => {
2109 card.addEventListener('click', function() {
2110 // Get the action data
2111 const isPro = this.dataset.pro === 'true';
2112 const isInstalled = this.dataset.installed === 'true';
2113 const addonName = this.dataset.addon || '';
2114 const actionValue = this.dataset.value;
2115 const actionLabel = this.dataset.label;
2116 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
2117 const actionDescription = this.querySelector('p').textContent;
2118
2119 // Check if this is a promotional card (add-on not installed)
2120 const isPromo = this.dataset.promo === 'true';
2121
2122 if (isPromo || (addonName && !isInstalled)) {
2123 // Add-on required but not installed — show informational notice
2124 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '').replace('— Get Add-on', '').trim() || addonName + ' Add-on';
2125 showAddonRequiredNotice(addonDisplayName);
2126 return;
2127 }
2128
2129 // If we get here, the action is available - proceed as normal
2130 callbackInput.value = actionValue;
2131
2132 // Update the selected action display in step 2
2133 document.getElementById('selected-action-title').textContent = actionLabel;
2134 document.getElementById('selected-action-description').textContent = actionDescription;
2135 document.getElementById('selected-action-icon').innerHTML =
2136 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2137
2138 // Set a default label based on the action type (user can change it)
2139 if (!labelField.value) {
2140 labelField.value = actionLabel;
2141 }
2142
2143 // Move to step 2
2144 actionStep1.classList.remove('active');
2145 actionStep2.classList.add('active');
2146
2147 // Update modal title
2148 });
2149 });
2150
2151 // Back button functionality
2152 if (backToStep1Btn) {
2153 backToStep1Btn.addEventListener('click', function() {
2154 //console.log('Back button clicked');
2155 actionStep2.classList.remove('active');
2156 actionStep1.classList.add('active');
2157 });
2158 }
2159
2160 // Function to filter action cards by category and search term
2161 function filterActionCards(category, searchTerm) {
2162 searchTerm = searchTerm.toLowerCase().trim();
2163 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
2164
2165 let visibleCount = 0;
2166
2167 // Show all cards initially with animation
2168 actionCards.forEach((card, index) => {
2169 // Reset animation
2170 card.style.animation = 'none';
2171 // Trigger reflow
2172 void card.offsetWidth;
2173
2174 // Determine if card should be visible based on category and search term
2175 const cardCategory = card.dataset.category || '';
2176 const matchesCategory = category === 'all' || cardCategory === category;
2177
2178 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
2179 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
2180 const matchesSearch = searchTerm === '' ||
2181 cardTitle.includes(searchTerm) ||
2182 cardDesc.includes(searchTerm);
2183
2184 // Show/hide card with animation
2185 if (matchesCategory && matchesSearch) {
2186 card.style.display = 'flex';
2187 // Staggered animation for cards
2188 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
2189 visibleCount++;
2190 } else {
2191 card.style.display = 'none';
2192 }
2193 });
2194
2195 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
2196 }
2197
2198 // Function to show notice for Pro features
2199 function showProFeatureNotice() {
2200 //console.log('Showing Pro feature notice');
2201 // Check if we already have a notification container
2202 let noticeContainer = document.querySelector('.mxchat-pro-notice');
2203
2204 if (!noticeContainer) {
2205 // Create the notice container
2206 noticeContainer = document.createElement('div');
2207 noticeContainer.className = 'mxchat-pro-notice';
2208
2209 // Create content
2210 noticeContainer.innerHTML = `
2211 <div class="mxchat-pro-notice-content">
2212 <h3>MxChat Pro Feature</h3>
2213 <p>This action is available in the Pro version only.</p>
2214 <div class="mxchat-pro-notice-buttons">
2215 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
2216 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
2217 </div>
2218 </div>
2219 `;
2220
2221 // Append to body
2222 document.body.appendChild(noticeContainer);
2223
2224 // Add close functionality
2225 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
2226 closeButton.addEventListener('click', function() {
2227 noticeContainer.classList.remove('active');
2228 setTimeout(() => {
2229 noticeContainer.remove();
2230 }, 300);
2231 });
2232
2233 // Click outside to close
2234 noticeContainer.addEventListener('click', function(e) {
2235 if (e.target === noticeContainer) {
2236 closeButton.click();
2237 }
2238 });
2239
2240 // Show with animation
2241 setTimeout(() => {
2242 noticeContainer.classList.add('active');
2243 }, 10);
2244 } else {
2245 // If it already exists, just make it visible again
2246 noticeContainer.classList.add('active');
2247 }
2248 }
2249
2250 // Function to show notice for add-on requirements
2251 function showAddonRequiredNotice(addonName) {
2252 //console.log(`Showing add-on notice for: ${addonName}`);
2253 // Check if we already have a notification container
2254 let noticeContainer = document.querySelector('.mxchat-addon-notice');
2255
2256 if (!noticeContainer) {
2257 // Create the notice container
2258 noticeContainer = document.createElement('div');
2259 noticeContainer.className = 'mxchat-addon-notice';
2260
2261 // Create content
2262 noticeContainer.innerHTML = `
2263 <div class="mxchat-addon-notice-content">
2264 <span class="mxchat-addon-notice-icon">🧩</span>
2265 <h3>Add-on Required</h3>
2266 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
2267 <div class="mxchat-addon-notice-buttons">
2268 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
2269 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
2270 </div>
2271 </div>
2272 `;
2273
2274 // Append to body
2275 document.body.appendChild(noticeContainer);
2276
2277 // Add close functionality
2278 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
2279 closeButton.addEventListener('click', function() {
2280 noticeContainer.classList.remove('active');
2281 setTimeout(() => {
2282 noticeContainer.remove();
2283 }, 300);
2284 });
2285
2286 // Click outside to close
2287 noticeContainer.addEventListener('click', function(e) {
2288 if (e.target === noticeContainer) {
2289 closeButton.click();
2290 }
2291 });
2292
2293 // Show with animation
2294 setTimeout(() => {
2295 noticeContainer.classList.add('active');
2296 }, 10);
2297 } else {
2298 // If it already exists, update the content
2299 const addonNameElement = noticeContainer.querySelector('p strong');
2300 if (addonNameElement) {
2301 addonNameElement.textContent = addonName;
2302 }
2303
2304 // Make it visible again
2305 noticeContainer.classList.add('active');
2306 }
2307 }
2308
2309 // Form submission handling
2310 if (actionForm) {
2311 actionForm.addEventListener('submit', function() {
2312 //console.log('Form submitted');
2313 document.getElementById('mxchat-action-loading').style.display = 'flex';
2314 this.querySelector('button[type="submit"]').disabled = true;
2315 });
2316 }
2317 }
2318
2319 // Setup add action buttons (only if we're on the correct page)
2320 if (modal) {
2321 // Update the modal open function to support the step-based flow
2322 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '', enabledBots = null) {
2323 //console.log('Modal opening, edit mode:', isEdit);
2324
2325 // Get form fields
2326 const actionIdField = document.getElementById('edit_action_id');
2327 const labelField = document.getElementById('intent_label');
2328 const phrasesField = document.getElementById('action_phrases');
2329 const formActionType = document.getElementById('form_action_type');
2330 const callbackInput = document.getElementById('callback_function');
2331 const saveButton = document.getElementById('mxchat-save-action-btn');
2332 const nonceContainer = document.getElementById('action-nonce-container');
2333 const thresholdSlider = document.getElementById('similarity_threshold');
2334 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2335 const actionStep1 = document.getElementById('mxchat-action-step-1');
2336 const actionStep2 = document.getElementById('mxchat-action-step-2');
2337 const searchInput = document.getElementById('action-type-search');
2338
2339 // Set up modal for edit or create
2340 if (isEdit) {
2341 saveButton.textContent = 'Update Action';
2342 formActionType.value = 'mxchat_edit_intent';
2343 actionIdField.value = actionId;
2344 labelField.value = label;
2345 phrasesField.value = phrases;
2346 callbackInput.value = callbackFunction;
2347 thresholdSlider.value = threshold; // Set the current threshold value
2348 thresholdDisplay.textContent = threshold + '%'; // Update display
2349
2350 // NEW: Handle bot selection checkboxes for editing
2351 // First uncheck all bot checkboxes
2352 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2353 checkbox.checked = false;
2354 });
2355
2356 // Then check the ones that should be enabled
2357 if (enabledBots) {
2358 let botsArray;
2359 if (typeof enabledBots === 'string') {
2360 try {
2361 botsArray = JSON.parse(enabledBots);
2362 } catch (e) {
2363 console.warn('Failed to parse enabledBots:', enabledBots);
2364 botsArray = ['default']; // fallback
2365 }
2366 } else if (Array.isArray(enabledBots)) {
2367 botsArray = enabledBots;
2368 } else {
2369 botsArray = ['default']; // fallback
2370 }
2371
2372 botsArray.forEach(botId => {
2373 const checkbox = document.querySelector(`input[name="enabled_bots[]"][value="${botId}"]`);
2374 if (checkbox) {
2375 checkbox.checked = true;
2376 }
2377 });
2378 } else {
2379 // Fallback to default if no bot data
2380 const defaultCheckbox = document.querySelector('input[name="enabled_bots[]"][value="default"]');
2381 if (defaultCheckbox) {
2382 defaultCheckbox.checked = true;
2383 }
2384 }
2385
2386 // Update the nonce field for editing
2387 nonceContainer.innerHTML = ''; // Clear existing nonce
2388 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
2389 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
2390 }
2391
2392 // For editing, go directly to step 2 and update the selected action display
2393 actionStep1.classList.remove('active');
2394 actionStep2.classList.add('active');
2395
2396 // Find the matching action card to get its details
2397 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
2398 let foundCard = null;
2399
2400 actionCards.forEach(card => {
2401 if (card.dataset.value === callbackFunction) {
2402 foundCard = card;
2403 }
2404 });
2405
2406 if (foundCard) {
2407 //console.log('Found matching action card for:', callbackFunction);
2408 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
2409 const actionIconElement = foundCard.querySelector('.dashicons');
2410 const actionIcon = actionIconElement
2411 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
2412 : 'admin-generic';
2413 const actionDescription = foundCard.querySelector('p')?.textContent || '';
2414
2415 document.getElementById('selected-action-title').textContent = actionLabel;
2416 document.getElementById('selected-action-description').textContent = actionDescription;
2417 document.getElementById('selected-action-icon').innerHTML =
2418 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2419 } else {
2420 //console.log('No matching action card found for:', callbackFunction);
2421 // Fallback if we can't find the card
2422 document.getElementById('selected-action-title').textContent = label;
2423 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
2424 document.getElementById('selected-action-icon').innerHTML =
2425 `<span class="dashicons dashicons-admin-generic"></span>`;
2426 }
2427 } else {
2428 //console.log('Setting up create mode');
2429 saveButton.textContent = 'Save Action';
2430 formActionType.value = 'mxchat_add_intent';
2431 actionIdField.value = '';
2432 labelField.value = '';
2433 phrasesField.value = '';
2434 callbackInput.value = '';
2435 thresholdSlider.value = 85; // Default value for new actions
2436 thresholdDisplay.textContent = '85%'; // Default display
2437
2438 // For new actions, ensure default is checked and others are unchecked
2439 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2440 checkbox.checked = (checkbox.value === 'default');
2441 });
2442
2443 // Update the nonce field for adding
2444 nonceContainer.innerHTML = ''; // Clear existing nonce
2445 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
2446 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
2447 }
2448
2449 // For creating new, start at step 1
2450 actionStep1.classList.add('active');
2451 actionStep2.classList.remove('active');
2452 }
2453
2454 // Show modal with animation
2455 modal.style.display = 'flex';
2456 requestAnimationFrame(() => {
2457 modal.classList.add('active');
2458 });
2459
2460 // Set up close handlers
2461 const closeModal = () => {
2462 //console.log('Closing modal');
2463 modal.classList.remove('active');
2464 setTimeout(() => {
2465 modal.style.display = 'none';
2466 }, 300); // Match the CSS transition time
2467 };
2468
2469 // Close button handler
2470 const closeBtn = modal.querySelector('.mxchat-modal-close');
2471 if (closeBtn) {
2472 closeBtn.onclick = closeModal;
2473 }
2474
2475 // Cancel button handler
2476 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
2477 if (cancelBtns) {
2478 cancelBtns.forEach(btn => {
2479 btn.onclick = closeModal;
2480 });
2481 }
2482
2483 // Click outside modal to close
2484 modal.onclick = (e) => {
2485 if (e.target === modal) {
2486 closeModal();
2487 }
2488 };
2489
2490 // Escape key to close modal
2491 document.addEventListener('keydown', function(e) {
2492 if (e.key === 'Escape' && modal.classList.contains('active')) {
2493 closeModal();
2494 }
2495 }, { once: true });
2496
2497 // Focus appropriate field based on current step
2498 if (isEdit || actionStep2.classList.contains('active')) {
2499 if (labelField) labelField.focus();
2500 } else {
2501 if (searchInput) searchInput.focus();
2502 }
2503
2504 return closeModal; // Return close function for external use
2505 };
2506 // Setup add action buttons
2507 const addActionBtn = document.getElementById('mxchat-add-action-btn');
2508 if (addActionBtn) {
2509 //console.log('Add action button found');
2510 addActionBtn.onclick = () => window.mxchatOpenActionModal();
2511 }
2512
2513 const createFirstAction = document.getElementById('mxchat-create-first-action');
2514 if (createFirstAction) {
2515 //console.log('Create first action button found');
2516 createFirstAction.onclick = () => window.mxchatOpenActionModal();
2517 }
2518
2519 // Setup edit buttons
2520 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
2521 //console.log('Edit buttons found:', editButtons.length);
2522 editButtons.forEach(button => {
2523 button.onclick = () => {
2524 const actionId = button.dataset.actionId;
2525 const phrases = button.dataset.phrases;
2526 const label = button.dataset.label;
2527 const threshold = button.dataset.threshold || 85;
2528 const callbackFunction = button.dataset.callbackFunction;
2529 const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
2530
2531 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
2532 };
2533 });
2534 }
2535 });
2536
2537 jQuery(document).ready(function($) {
2538 // Auto-expand custom post types container if any are checked
2539 // Note: Click handler is in admin-knowledge-page.php inline script (initCustomPostTypesToggle)
2540 function autoExpandIfNeeded() {
2541 const $container = $('#mxchat-custom-post-types-container');
2542 const $toggleBtn = $('#mxchat-custom-post-types-toggle');
2543
2544 if ($container.length === 0) return;
2545
2546 const hasCheckedItems = $container.find('input[type="checkbox"]:checked').length > 0;
2547
2548 if (hasCheckedItems) {
2549 $container.show();
2550 const $icon = $toggleBtn.find('span:last-child');
2551 if ($icon.length) {
2552 $icon.text('');
2553 }
2554 }
2555 }
2556
2557 // Run on page load
2558 autoExpandIfNeeded();
2559 });
2560
2561 document.addEventListener('DOMContentLoaded', function() {
2562 var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
2563 var modal = document.getElementById('mxchatSampleModal');
2564 var modalClose = document.getElementById('mxchatModalClose');
2565 var closeBtn = document.getElementById('mxchatCloseBtn');
2566 var copyBtn = document.getElementById('mxchatCopyBtn');
2567 var instructionsContent = document.querySelector('.mxchat-instructions-content');
2568 var modalContent = document.querySelector('.mxchat-instructions-modal-content');
2569
2570 if (!viewSampleBtn || !modal) {
2571 return;
2572 }
2573
2574 // Open modal
2575 viewSampleBtn.addEventListener('click', function(e) {
2576 e.preventDefault();
2577 e.stopPropagation();
2578 modal.classList.add('mxchat-instructions-show');
2579 });
2580
2581 // Close modal function
2582 function closeModal(e) {
2583 if (e) {
2584 e.preventDefault();
2585 e.stopPropagation();
2586 }
2587 modal.classList.remove('mxchat-instructions-show');
2588 }
2589
2590 // Close modal events
2591 if (modalClose) {
2592 modalClose.addEventListener('click', function(e) {
2593 closeModal(e);
2594 });
2595 }
2596
2597 if (closeBtn) {
2598 closeBtn.addEventListener('click', function(e) {
2599 closeModal(e);
2600 });
2601 }
2602
2603 // Close on backdrop click ONLY (not on hover)
2604 modal.addEventListener('click', function(e) {
2605 // Only close if clicking directly on the overlay, not on child elements
2606 if (e.target === modal) {
2607 closeModal(e);
2608 }
2609 });
2610
2611 // Prevent modal content clicks from closing the modal
2612 if (modalContent) {
2613 modalContent.addEventListener('click', function(e) {
2614 e.stopPropagation();
2615 });
2616 }
2617
2618 // Close on escape key
2619 document.addEventListener('keydown', function(e) {
2620 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2621 closeModal();
2622 }
2623 });
2624
2625 // Copy functionality
2626 if (copyBtn && instructionsContent) {
2627 copyBtn.addEventListener('click', function(e) {
2628 e.preventDefault();
2629 e.stopPropagation();
2630
2631 var text = instructionsContent.textContent;
2632
2633 if (navigator.clipboard) {
2634 navigator.clipboard.writeText(text).then(function() {
2635 showCopySuccess();
2636 }).catch(function() {
2637 fallbackCopy(text);
2638 });
2639 } else {
2640 fallbackCopy(text);
2641 }
2642 });
2643 }
2644
2645 function fallbackCopy(text) {
2646 var textArea = document.createElement('textarea');
2647 textArea.value = text;
2648 textArea.style.position = 'fixed';
2649 textArea.style.left = '-999999px';
2650 textArea.style.top = '-999999px';
2651 document.body.appendChild(textArea);
2652 textArea.select();
2653 try {
2654 document.execCommand('copy');
2655 showCopySuccess();
2656 } catch (err) {
2657 console.error('Copy failed');
2658 }
2659 document.body.removeChild(textArea);
2660 }
2661
2662 function showCopySuccess() {
2663 var originalText = copyBtn.innerHTML;
2664 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!';
2665
2666 setTimeout(function() {
2667 copyBtn.innerHTML = originalText;
2668 }, 2000);
2669 }
2670 });
2671
2672 jQuery(document).ready(function($) {
2673 var emailDependentFields = [
2674 '#email_blocker_header_content',
2675 '#email_blocker_button_text',
2676 '#enable_name_field',
2677 '#name_field_placeholder'
2678 ];
2679
2680 // Add visual indicators
2681 emailDependentFields.forEach(function(fieldId) {
2682 var $row = $(fieldId).closest('tr');
2683 $row.addClass('email-dependent-field');
2684
2685 // Add an icon to show it's dependent
2686 var $label = $row.find('th label, th');
2687 $label.prepend('<span class="email-dependent-icon" style="color: #0073aa; margin-right: 5px;">↳</span>');
2688 });
2689
2690 // Hide initially with opacity for smoother transition
2691 emailDependentFields.forEach(function(fieldId) {
2692 $(fieldId).closest('tr').hide().css('opacity', '0');
2693 });
2694
2695 function toggleEmailFields() {
2696 var emailEnabled = $('#enable_email_block').is(':checked');
2697
2698 emailDependentFields.forEach(function(fieldId) {
2699 var $row = $(fieldId).closest('tr');
2700 if (emailEnabled) {
2701 $row.slideDown(400).animate({opacity: 1}, 200);
2702 } else {
2703 $row.animate({opacity: 0}, 200).slideUp(400);
2704 }
2705 });
2706 }
2707
2708 toggleEmailFields();
2709 $('#enable_email_block').on('change', toggleEmailFields);
2710 });
2711
2712
2713
2714 //Handle role restriction changes
2715 jQuery(document).ready(function($) {
2716 //Handle role restriction changes for both data sources
2717 $(document).on('change', '.mxchat-role-select', function() {
2718 const $select = $(this);
2719 const entryId = $select.data('entry-id');
2720 const dataSource = $select.data('data-source');
2721 const roleRestriction = $select.val();
2722 const nonce = $select.data('nonce');
2723
2724 // Visual feedback
2725 $select.prop('disabled', true).addClass('updating');
2726
2727 $.ajax({
2728 url: ajaxurl,
2729 type: 'POST',
2730 data: {
2731 action: 'mxchat_update_role_restriction',
2732 nonce: nonce,
2733 entry_id: entryId,
2734 data_source: dataSource, // NEW: Include data source
2735 role_restriction: roleRestriction
2736 },
2737 success: function(response) {
2738 if (response.success) {
2739 // Show success feedback
2740 $select.removeClass('updating').addClass('updated');
2741 setTimeout(() => {
2742 $select.removeClass('updated');
2743 }, 2000);
2744 } else {
2745 alert('Failed to update role restriction: ' + response.data);
2746 // Revert selection
2747 $select.val($select.data('original-value'));
2748 }
2749 },
2750 error: function() {
2751 alert('Error updating role restriction');
2752 // Revert selection
2753 $select.val($select.data('original-value'));
2754 },
2755 complete: function() {
2756 $select.prop('disabled', false);
2757 }
2758 });
2759
2760 // Store original value for potential revert
2761 $select.data('original-value', roleRestriction);
2762 });
2763
2764 // Store initial values
2765 $('.mxchat-role-select').each(function() {
2766 $(this).data('original-value', $(this).val());
2767 });
2768 });
2769
2770 // Bot Selector Handler
2771 jQuery(document).ready(function($) {
2772 var saveTimer;
2773
2774 // Function to update bot_id in forms
2775 function updateBotIdInForm(formSelector) {
2776 var botId = $('#mxchat-bot-selector').val();
2777 var form = $(formSelector);
2778
2779 if (form.length > 0) {
2780 // Remove existing bot_id hidden input
2781 form.find('input[name="bot_id"]').remove();
2782
2783 // Add new bot_id hidden input if not default
2784 if (botId && botId !== 'default') {
2785 form.append('<input type="hidden" name="bot_id" value="' + botId + '">');
2786 }
2787 }
2788 }
2789
2790 $('#mxchat-bot-selector').on('change', function() {
2791 var botId = $(this).val();
2792
2793 // Clear any existing timer
2794 clearTimeout(saveTimer);
2795
2796 // Update all forms with new bot_id when bot selection changes
2797 updateBotIdInForm('#mxchat-url-form');
2798 updateBotIdInForm('#mxchat-content-form');
2799
2800 // Save the selection via AJAX
2801 $.ajax({
2802 url: ajaxurl,
2803 type: 'POST',
2804 data: {
2805 action: 'mxchat_save_selected_bot',
2806 bot_id: botId,
2807 nonce: mxchatAdmin.setting_nonce
2808 },
2809 success: function(response) {
2810 if (response.success) {
2811 // Show saved indicator
2812 $('#mxchat-bot-save-status').fadeIn().delay(2000).fadeOut();
2813
2814 // Reload the page after a short delay to refresh content
2815 saveTimer = setTimeout(function() {
2816 var currentUrl = new URL(window.location.href);
2817 currentUrl.searchParams.set('bot_id', botId);
2818 currentUrl.searchParams.set('page', 'mxchat-prompts');
2819 window.location.href = currentUrl.toString();
2820 }, 500);
2821 }
2822 },
2823 error: function() {
2824 console.error('Failed to save bot selection');
2825 }
2826 });
2827 });
2828
2829 // Initialize forms with current bot_id when the page loads
2830 setTimeout(function() {
2831 updateBotIdInForm('#mxchat-url-form');
2832 updateBotIdInForm('#mxchat-content-form');
2833 }, 100);
2834 });
2835
2836 // API Key Status Indicator for Chat Models
2837 jQuery(document).ready(function($) {
2838 function updateChatModelAPIStatus(apiKeyStatuses) {
2839 var selectedModel = $('#model').val();
2840
2841 // Hide all status messages
2842 $('.mxchat-api-status').hide();
2843
2844 // Return early if no model is selected
2845 if (!selectedModel) {
2846 return;
2847 }
2848
2849 // Map models to providers
2850 var provider = null;
2851
2852 if (selectedModel.startsWith('gpt-')) {
2853 provider = 'openai';
2854 } else if (selectedModel.startsWith('claude-')) {
2855 provider = 'claude';
2856 } else if (selectedModel.startsWith('grok-')) {
2857 provider = 'xai';
2858 } else if (selectedModel.startsWith('deepseek-')) {
2859 provider = 'deepseek';
2860 } else if (selectedModel.startsWith('gemini-')) {
2861 provider = 'gemini';
2862 } else if (selectedModel === 'openrouter') {
2863 provider = 'openrouter';
2864 }
2865
2866 // If we have fresh API key data, update the messages
2867 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
2868 var $statusElement = $('.mxchat-api-status[data-provider="' + provider + '"]');
2869 var hasKey = apiKeyStatuses[provider];
2870
2871 if (hasKey) {
2872 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
2873 } else {
2874 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
2875 }
2876 }
2877
2878 // Show the appropriate status message
2879 if (provider) {
2880 $('.mxchat-api-status[data-provider="' + provider + '"]').show();
2881 }
2882 }
2883
2884 function updateEmbeddingModelAPIStatus(apiKeyStatuses) {
2885 var selectedModel = $('#embedding_model').val();
2886
2887 // Hide all status messages
2888 $('.mxchat-embedding-api-status').hide();
2889
2890 // Return early if no model is selected
2891 if (!selectedModel) {
2892 return;
2893 }
2894
2895 // Map models to providers
2896 var provider = null;
2897
2898 if (selectedModel.startsWith('text-embedding-')) {
2899 provider = 'openai';
2900 } else if (selectedModel.startsWith('voyage-')) {
2901 provider = 'voyage';
2902 } else if (selectedModel.startsWith('gemini-embedding-')) {
2903 provider = 'gemini';
2904 }
2905
2906 // If we have fresh API key data, update the messages
2907 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
2908 var $statusElement = $('.mxchat-embedding-api-status[data-provider="' + provider + '"]');
2909 var hasKey = apiKeyStatuses[provider];
2910
2911 if (hasKey) {
2912 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
2913 } else {
2914 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
2915 }
2916 }
2917
2918 // Show the appropriate status message
2919 if (provider) {
2920 $('.mxchat-embedding-api-status[data-provider="' + provider + '"]').show();
2921 }
2922 }
2923
2924 function getProviderName(provider) {
2925 var names = {
2926 'openai': 'OpenAI',
2927 'claude': 'Anthropic (Claude)',
2928 'xai': 'X.AI (Grok)',
2929 'deepseek': 'DeepSeek',
2930 'gemini': 'Google Gemini',
2931 'openrouter': 'OpenRouter',
2932 'voyage': 'Voyage AI'
2933 };
2934 return names[provider] || provider;
2935 }
2936
2937 function refreshAPIKeyStatus() {
2938 $.ajax({
2939 url: ajaxurl,
2940 type: 'POST',
2941 data: {
2942 action: 'mxchat_check_api_keys',
2943 nonce: mxchatAdmin.setting_nonce
2944 },
2945 success: function(response) {
2946 if (response.success && response.data) {
2947 updateChatModelAPIStatus(response.data);
2948 updateEmbeddingModelAPIStatus(response.data);
2949 }
2950 }
2951 });
2952 }
2953
2954 // Expose refresh function globally so auto-save can call it
2955 window.mxchatRefreshAPIKeyStatus = refreshAPIKeyStatus;
2956
2957 // Run on page load
2958 updateChatModelAPIStatus();
2959 updateEmbeddingModelAPIStatus();
2960
2961 // Check if we just saved settings (WordPress redirects with ?settings-updated=true)
2962 var urlParams = new URLSearchParams(window.location.search);
2963 if (urlParams.get('settings-updated') === 'true') {
2964 // Page was just reloaded after save, fetch fresh API key status
2965 refreshAPIKeyStatus();
2966 }
2967
2968 // Run when model changes
2969 $('#model').on('change', function() {
2970 updateChatModelAPIStatus();
2971 updateWebSearchToggleVisibility();
2972 });
2973 $('#embedding_model').on('change', function() { updateEmbeddingModelAPIStatus(); });
2974
2975 // Web Search toggle visibility based on model
2976 function updateWebSearchToggleVisibility() {
2977 var selectedModel = $('#model').val();
2978 var $wrapper = $('#web-search-toggle-wrapper');
2979 var $unavailableMessage = $('#web-search-unavailable-message');
2980
2981 if (!$wrapper.length) return; // Element doesn't exist
2982
2983 // Get the list of supported OpenAI models from data attribute
2984 var openaiModelsAttr = $wrapper.data('openai-models');
2985 var unsupportedModelsAttr = $wrapper.data('unsupported-models');
2986
2987 var openaiModels = openaiModelsAttr ? openaiModelsAttr.split(',') : [];
2988 var unsupportedModels = unsupportedModelsAttr ? unsupportedModelsAttr.split(',') : [];
2989
2990 // Check if selected model is an OpenAI model that supports web search
2991 var isOpenAI = openaiModels.includes(selectedModel);
2992 var isSupported = isOpenAI && !unsupportedModels.includes(selectedModel);
2993
2994 if (isSupported) {
2995 $wrapper.show();
2996 $unavailableMessage.hide();
2997 } else {
2998 $wrapper.hide();
2999 $unavailableMessage.show();
3000 }
3001 }
3002
3003 // Run on page load
3004 updateWebSearchToggleVisibility();
3005 });
3006
3007 // Transcripts Metrics Dashboard
3008 jQuery(document).ready(function($) {
3009 // Tab switching functionality
3010 $('.mxchat-metrics-tab').on('click', function() {
3011 const tabName = $(this).data('tab');
3012
3013 // Update tab buttons
3014 $('.mxchat-metrics-tab').removeClass('active');
3015 $(this).addClass('active');
3016
3017 // Update panels
3018 $('.mxchat-metrics-panel').removeClass('active');
3019 $(`.mxchat-metrics-panel[data-panel="${tabName}"]`).addClass('active');
3020
3021 // Initialize chart if activity tab is shown
3022 if (tabName === 'activity' && typeof mxchatChartData !== 'undefined') {
3023 // Small delay to ensure the canvas is visible
3024 setTimeout(function() {
3025 initActivityChart();
3026 }, 50);
3027 }
3028 });
3029
3030 // Initialize chart function
3031 function initActivityChart() {
3032 const canvas = document.getElementById('mxchat-activity-chart');
3033 if (!canvas) return;
3034
3035 // Check if chart already exists and destroy it
3036 if (canvas.chartInstance) {
3037 canvas.chartInstance.destroy();
3038 }
3039
3040 const ctx = canvas.getContext('2d');
3041
3042 // Create gradient for chats line
3043 const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
3044 chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
3045 chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
3046
3047 // Create gradient for messages line
3048 const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
3049 messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
3050 messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
3051
3052 // Simple chart without external library
3053 canvas.chartInstance = new SimpleChart(canvas, {
3054 labels: mxchatChartData.labels,
3055 datasets: [
3056 {
3057 label: 'Chats',
3058 data: mxchatChartData.chats,
3059 borderColor: '#667eea',
3060 backgroundColor: chatsGradient,
3061 fill: true
3062 },
3063 {
3064 label: 'Messages',
3065 data: mxchatChartData.messages,
3066 borderColor: '#764ba2',
3067 backgroundColor: messagesGradient,
3068 fill: true
3069 }
3070 ]
3071 });
3072 }
3073
3074 // Simple chart implementation (no external dependencies)
3075 class SimpleChart {
3076 constructor(canvas, config) {
3077 this.canvas = canvas;
3078 this.ctx = canvas.getContext('2d');
3079 this.config = config;
3080 this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
3081 this.render();
3082 }
3083
3084 destroy() {
3085 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3086 }
3087
3088 render() {
3089 const dpr = window.devicePixelRatio || 1;
3090 const rect = this.canvas.getBoundingClientRect();
3091
3092 this.canvas.width = rect.width * dpr;
3093 this.canvas.height = rect.height * dpr;
3094 this.ctx.scale(dpr, dpr);
3095
3096 this.canvas.style.width = rect.width + 'px';
3097 this.canvas.style.height = rect.height + 'px';
3098
3099 const width = rect.width - this.padding.left - this.padding.right;
3100 const height = rect.height - this.padding.top - this.padding.bottom;
3101
3102 // Find max value
3103 let maxValue = 0;
3104 this.config.datasets.forEach(dataset => {
3105 const max = Math.max(...dataset.data);
3106 if (max > maxValue) maxValue = max;
3107 });
3108
3109 // Add some padding to max value
3110 maxValue = Math.ceil(maxValue * 1.1);
3111 if (maxValue === 0) maxValue = 10;
3112
3113 // Draw grid lines
3114 this.ctx.strokeStyle = '#e5e7eb';
3115 this.ctx.lineWidth = 1;
3116 const gridLines = 5;
3117
3118 for (let i = 0; i <= gridLines; i++) {
3119 const y = this.padding.top + (height / gridLines) * i;
3120 this.ctx.beginPath();
3121 this.ctx.moveTo(this.padding.left, y);
3122 this.ctx.lineTo(this.padding.left + width, y);
3123 this.ctx.stroke();
3124
3125 // Draw y-axis labels
3126 const value = maxValue - (maxValue / gridLines) * i;
3127 this.ctx.fillStyle = '#6b7280';
3128 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3129 this.ctx.textAlign = 'right';
3130 this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
3131 }
3132
3133 // Draw datasets
3134 this.config.datasets.forEach(dataset => {
3135 const points = [];
3136 const xStep = width / (this.config.labels.length - 1 || 1);
3137
3138 dataset.data.forEach((value, index) => {
3139 const x = this.padding.left + (xStep * index);
3140 const y = this.padding.top + height - (value / maxValue * height);
3141 points.push({ x, y, value });
3142 });
3143
3144 // Draw filled area
3145 if (dataset.fill && dataset.backgroundColor) {
3146 this.ctx.fillStyle = dataset.backgroundColor;
3147 this.ctx.beginPath();
3148 this.ctx.moveTo(points[0].x, this.padding.top + height);
3149 points.forEach(point => {
3150 this.ctx.lineTo(point.x, point.y);
3151 });
3152 this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
3153 this.ctx.closePath();
3154 this.ctx.fill();
3155 }
3156
3157 // Draw line
3158 this.ctx.strokeStyle = dataset.borderColor;
3159 this.ctx.lineWidth = 3;
3160 this.ctx.lineCap = 'round';
3161 this.ctx.lineJoin = 'round';
3162
3163 this.ctx.beginPath();
3164 points.forEach((point, index) => {
3165 if (index === 0) {
3166 this.ctx.moveTo(point.x, point.y);
3167 } else {
3168 this.ctx.lineTo(point.x, point.y);
3169 }
3170 });
3171 this.ctx.stroke();
3172
3173 // Draw points
3174 points.forEach(point => {
3175 this.ctx.fillStyle = '#ffffff';
3176 this.ctx.beginPath();
3177 this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
3178 this.ctx.fill();
3179 this.ctx.strokeStyle = dataset.borderColor;
3180 this.ctx.lineWidth = 2;
3181 this.ctx.stroke();
3182 });
3183 });
3184
3185 // Draw x-axis labels
3186 const xStep = width / (this.config.labels.length - 1 || 1);
3187 this.ctx.fillStyle = '#6b7280';
3188 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3189 this.ctx.textAlign = 'center';
3190
3191 this.config.labels.forEach((label, index) => {
3192 const x = this.padding.left + (xStep * index);
3193 this.ctx.fillText(label, x, this.padding.top + height + 20);
3194 });
3195
3196 // Draw legend
3197 let legendX = this.padding.left;
3198 const legendY = rect.height - 10;
3199
3200 this.config.datasets.forEach((dataset, index) => {
3201 // Color box
3202 this.ctx.fillStyle = dataset.borderColor;
3203 this.ctx.fillRect(legendX, legendY - 8, 12, 12);
3204
3205 // Label
3206 this.ctx.fillStyle = '#374151';
3207 this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3208 this.ctx.textAlign = 'left';
3209 this.ctx.fillText(dataset.label, legendX + 18, legendY);
3210
3211 legendX += this.ctx.measureText(dataset.label).width + 40;
3212 });
3213 }
3214 }
3215
3216 // Initialize chart on page load if we're on the activity tab
3217 if ($('.mxchat-metrics-tab.active').data('tab') === 'activity' && typeof mxchatChartData !== 'undefined') {
3218 setTimeout(function() {
3219 initActivityChart();
3220 }, 100);
3221 }
3222
3223 // ========================================
3224 // SLACK TEST CONNECTION
3225 // ========================================
3226 $('#mxchat-test-slack-connection').on('click', function() {
3227 var $button = $(this);
3228 var $result = $('#mxchat-slack-test-result');
3229 var originalText = $button.html();
3230
3231 // Show loading state
3232 $button.prop('disabled', true).html(
3233 '<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>' +
3234 'Testing...'
3235 );
3236
3237 $.ajax({
3238 url: ajaxurl,
3239 type: 'POST',
3240 data: {
3241 action: 'mxchat_test_slack_connection',
3242 nonce: mxchatAdmin.nonce
3243 },
3244 success: function(response) {
3245 $result.show();
3246 if (response.success) {
3247 $result.html(
3248 '<div style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 16px; border-radius: 8px;">' +
3249 '<strong style="display: block; margin-bottom: 8px;">✓ Connection Successful!</strong>' +
3250 '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3251 '</div>'
3252 );
3253 } else {
3254 var bgColor = response.data.partial ? '#fff3cd' : '#f8d7da';
3255 var borderColor = response.data.partial ? '#ffeeba' : '#f5c6cb';
3256 var textColor = response.data.partial ? '#856404' : '#721c24';
3257 var icon = response.data.partial ? '' : '';
3258 var title = response.data.partial ? 'Partial Success - Missing Scopes' : 'Connection Failed';
3259
3260 $result.html(
3261 '<div style="background: ' + bgColor + '; border: 1px solid ' + borderColor + '; color: ' + textColor + '; padding: 16px; border-radius: 8px;">' +
3262 '<strong style="display: block; margin-bottom: 8px;">' + icon + ' ' + title + '</strong>' +
3263 '<pre style="margin: 0; white-space: pre-wrap; font-size: 13px;">' + escapeHtml(response.data.message) + '</pre>' +
3264 (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>' : '') +
3265 '</div>'
3266 );
3267 }
3268 },
3269 error: function() {
3270 $result.show().html(
3271 '<div style="background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 16px; border-radius: 8px;">' +
3272 '<strong>✗ Request Failed</strong><br>Could not connect to the server. Please try again.' +
3273 '</div>'
3274 );
3275 },
3276 complete: function() {
3277 $button.prop('disabled', false).html(originalText);
3278 }
3279 });
3280 });
3281
3282 // Helper function to escape HTML
3283 function escapeHtml(text) {
3284 var div = document.createElement('div');
3285 div.appendChild(document.createTextNode(text));
3286 return div.innerHTML;
3287 }
3288
3289 // ========================================
3290 // DEBUG & OPTIMIZATION TOOLS
3291 // ========================================
3292
3293 // Debug Mode Toggle
3294 $('#mxchat_debug_mode').on('change', function() {
3295 var $toggle = $(this);
3296 var enabled = $toggle.is(':checked') ? 'on' : 'off';
3297 var $label = $toggle.closest('label');
3298
3299 // Add loading indicator next to the toggle label
3300 var $indicator = $label.find('.mxchat-save-indicator');
3301 if ($indicator.length === 0) {
3302 $indicator = $('<span class="mxchat-save-indicator" style="margin-left: 10px;"></span>');
3303 $label.append($indicator);
3304 }
3305 $indicator.html('<span class="mxchat-saving-spinner"></span>').show();
3306
3307 $.ajax({
3308 url: ajaxurl,
3309 type: 'POST',
3310 data: {
3311 action: 'mxchat_toggle_debug_mode',
3312 enabled: enabled,
3313 _ajax_nonce: mxchatAdmin.setting_nonce
3314 },
3315 success: function(response) {
3316 if (response.success) {
3317 // Show success indicator
3318 $indicator.html('<span class="mxchat-save-success">✓</span>');
3319 setTimeout(function() {
3320 $indicator.fadeOut(300);
3321 }, 2000);
3322
3323 // Always refresh the log after toggling
3324 refreshDebugLog();
3325 } else {
3326 $indicator.html('<span class="mxchat-save-error">✗</span>');
3327 alert(response.data.message || 'Error toggling debug mode');
3328 $toggle.prop('checked', !$toggle.is(':checked'));
3329 }
3330 },
3331 error: function() {
3332 $indicator.html('<span class="mxchat-save-error">✗</span>');
3333 alert('Error toggling debug mode');
3334 $toggle.prop('checked', !$toggle.is(':checked'));
3335 }
3336 });
3337 });
3338
3339 // Refresh Debug Log
3340 function refreshDebugLog() {
3341 var $container = $('#mxchat-debug-log');
3342 var $countBadge = $('#mxchat-log-count');
3343
3344 $.ajax({
3345 url: ajaxurl,
3346 type: 'POST',
3347 data: {
3348 action: 'mxchat_get_debug_log',
3349 _ajax_nonce: mxchatAdmin.setting_nonce
3350 },
3351 success: function(response) {
3352 if (response.success) {
3353 var log = response.data.log;
3354 var count = response.data.count;
3355
3356 if (count > 0) {
3357 $countBadge.text(count + ' entries').show();
3358 var html = '<div class="mxchat-debug-log-entries">';
3359 log.forEach(function(entry) {
3360 var typeClass = 'mxchat-log-type-' + entry.type;
3361 var typeLabel = entry.type.replace(/_/g, ' ').toUpperCase();
3362 html += '<div class="mxchat-debug-log-entry ' + typeClass + '">';
3363 html += '<div class="mxchat-log-header">';
3364 html += '<span class="mxchat-log-type">' + escapeHtml(typeLabel) + '</span>';
3365 html += '<span class="mxchat-log-time">' + escapeHtml(entry.time) + '</span>';
3366 html += '</div>';
3367 html += '<div class="mxchat-log-message">' + escapeHtml(entry.message) + '</div>';
3368 if (entry.data) {
3369 html += '<div class="mxchat-log-data">' + escapeHtml(JSON.stringify(entry.data, null, 2)) + '</div>';
3370 }
3371 html += '</div>';
3372 });
3373 html += '</div>';
3374 $container.html(html);
3375 } else {
3376 $countBadge.hide();
3377 $container.html(
3378 '<div class="mxchat-debug-log-empty">' +
3379 '<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>' +
3380 '<p>No log entries yet. Enable debug mode to start logging.</p>' +
3381 '</div>'
3382 );
3383 }
3384 }
3385 }
3386 });
3387 }
3388
3389 // Load debug log on page load
3390 if ($('#mxchat-debug-log').length) {
3391 refreshDebugLog();
3392 }
3393
3394 // Refresh Log Button
3395 $('#mxchat-refresh-log').on('click', function() {
3396 var $btn = $(this);
3397 $btn.prop('disabled', true);
3398 refreshDebugLog();
3399 setTimeout(function() {
3400 $btn.prop('disabled', false);
3401 }, 500);
3402 });
3403
3404 // Clear Log Button
3405 $('#mxchat-clear-log').on('click', function() {
3406 if (!confirm('Are you sure you want to clear the debug log?')) {
3407 return;
3408 }
3409
3410 var $btn = $(this);
3411 $btn.prop('disabled', true);
3412
3413 $.ajax({
3414 url: ajaxurl,
3415 type: 'POST',
3416 data: {
3417 action: 'mxchat_clear_debug_log',
3418 _ajax_nonce: mxchatAdmin.setting_nonce
3419 },
3420 success: function(response) {
3421 if (response.success) {
3422 refreshDebugLog();
3423 } else {
3424 alert(response.data.message || 'Error clearing log');
3425 }
3426 },
3427 error: function() {
3428 alert('Error clearing log');
3429 },
3430 complete: function() {
3431 $btn.prop('disabled', false);
3432 }
3433 });
3434 });
3435
3436 // Export Settings Button
3437 $('#mxchat-export-settings').on('click', function() {
3438 var $btn = $(this);
3439 var originalHtml = $btn.html();
3440 $btn.prop('disabled', true).html(
3441 '<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...'
3442 );
3443
3444 $.ajax({
3445 url: ajaxurl,
3446 type: 'POST',
3447 data: {
3448 action: 'mxchat_export_settings',
3449 _ajax_nonce: mxchatAdmin.setting_nonce
3450 },
3451 success: function(response) {
3452 if (response.success) {
3453 // Create and download the JSON file
3454 var dataStr = JSON.stringify(response.data.settings, null, 2);
3455 var blob = new Blob([dataStr], { type: 'application/json' });
3456 var url = URL.createObjectURL(blob);
3457 var a = document.createElement('a');
3458 a.href = url;
3459 a.download = response.data.filename;
3460 document.body.appendChild(a);
3461 a.click();
3462 document.body.removeChild(a);
3463 URL.revokeObjectURL(url);
3464 } else {
3465 alert(response.data.message || 'Error exporting settings');
3466 }
3467 },
3468 error: function() {
3469 alert('Error exporting settings');
3470 },
3471 complete: function() {
3472 $btn.prop('disabled', false).html(originalHtml);
3473 }
3474 });
3475 });
3476
3477 // Reset Settings Modal
3478 var $resetModal = $('#mxchat-reset-modal');
3479 var $resetConfirmInput = $('#mxchat-reset-confirmation');
3480 var $resetConfirmBtn = $('#mxchat-reset-confirm');
3481
3482 $('#mxchat-reset-settings').on('click', function() {
3483 $resetModal.fadeIn(200);
3484 $resetConfirmInput.val('').focus();
3485 $resetConfirmBtn.prop('disabled', true);
3486 });
3487
3488 $('#mxchat-reset-modal-close, #mxchat-reset-cancel, .mxch-modal-backdrop').on('click', function() {
3489 $resetModal.fadeOut(200);
3490 });
3491
3492 $resetConfirmInput.on('input', function() {
3493 var value = $(this).val().toUpperCase();
3494 $resetConfirmBtn.prop('disabled', value !== 'RESET');
3495 });
3496
3497 $resetConfirmBtn.on('click', function() {
3498 var $btn = $(this);
3499 var confirmation = $resetConfirmInput.val();
3500
3501 $btn.prop('disabled', true).text('Resetting...');
3502
3503 $.ajax({
3504 url: ajaxurl,
3505 type: 'POST',
3506 data: {
3507 action: 'mxchat_reset_all_settings',
3508 confirmation: confirmation,
3509 _ajax_nonce: mxchatAdmin.setting_nonce
3510 },
3511 success: function(response) {
3512 if (response.success) {
3513 alert(response.data.message);
3514 window.location.reload();
3515 } else {
3516 alert(response.data.message || 'Error resetting settings');
3517 $btn.prop('disabled', false).text('Reset All Settings');
3518 }
3519 },
3520 error: function() {
3521 alert('Error resetting settings');
3522 $btn.prop('disabled', false).text('Reset All Settings');
3523 }
3524 });
3525 });
3526
3527 // Load debug log on page load if we're on the optimization section
3528 if ($('#optimization').hasClass('active') || window.location.hash === '#optimization') {
3529 refreshDebugLog();
3530 }
3531
3532 // Also refresh when switching to optimization tab
3533 $(document).on('click', '[data-section="optimization"]', function() {
3534 setTimeout(refreshDebugLog, 100);
3535 });
3536 });