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

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