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

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