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

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