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

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