PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.5.5
MxChat – AI Chatbot & Content Generation for WordPress v2.5.5
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 2.5.5, at js/mxchat-admin.js

2,786 lines 131.3 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 // Updated mxchatOpenActionModal function to integrate with the new selector
87 function mxchatOpenActionModal(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
88 const modal = document.getElementById('mxchat-action-modal');
89 if (!modal) return;
90
91 // Get form fields
92 const actionIdField = document.getElementById('edit_action_id');
93 const labelField = document.getElementById('intent_label');
94 const phrasesField = document.getElementById('action_phrases');
95 const formActionType = document.getElementById('form_action_type');
96 const callbackGroup = document.getElementById('callback_selection_group');
97 const callbackSelect = document.getElementById('callback_function');
98 const saveButton = document.getElementById('mxchat-save-action-btn');
99 const nonceContainer = document.getElementById('action-nonce-container');
100 const thresholdSlider = document.getElementById('similarity_threshold');
101 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
102
103 // Set up modal for edit or create
104 if (isEdit) {
105 saveButton.textContent = 'Update Action';
106 formActionType.value = 'mxchat_edit_intent';
107 actionIdField.value = actionId;
108 labelField.value = label;
109 phrasesField.value = phrases;
110 callbackGroup.style.display = 'none'; // Hide callback selection when editing
111 thresholdSlider.value = threshold; // Set the current threshold value
112 thresholdDisplay.textContent = threshold + '%'; // Update display
113
114 // Remove the required attribute when editing
115 callbackSelect.removeAttribute('required');
116
117 // Update the nonce field for editing
118 nonceContainer.innerHTML = ''; // Clear existing nonce
119 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
120 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
121 }
122 } else {
123 saveButton.textContent = 'Save Action';
124 formActionType.value = 'mxchat_add_intent';
125 actionIdField.value = '';
126 labelField.value = '';
127 phrasesField.value = '';
128 callbackGroup.style.display = 'block'; // Show callback selection when creating
129 thresholdSlider.value = 85; // Default value for new actions
130 thresholdDisplay.textContent = '85%'; // Default display
131
132 // Ensure the required attribute is present when adding
133 callbackSelect.setAttribute('required', 'required');
134
135 // Update the nonce field for adding
136 nonceContainer.innerHTML = ''; // Clear existing nonce
137 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
138 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
139 }
140 }
141
142 // Show modal with animation
143 modal.style.display = 'flex';
144 requestAnimationFrame(() => {
145 modal.classList.add('active');
146 });
147
148 // Set up close handlers
149 const closeModal = () => {
150 modal.classList.remove('active');
151 setTimeout(() => {
152 modal.style.display = 'none';
153 }, 300); // Match the CSS transition time
154 };
155
156 // Close button handler
157 const closeBtn = modal.querySelector('.mxchat-modal-close');
158 if (closeBtn) {
159 closeBtn.onclick = closeModal;
160 }
161
162 // Cancel button handler
163 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
164 if (cancelBtn) {
165 cancelBtn.onclick = closeModal;
166 }
167
168 // Click outside modal to close
169 modal.onclick = (e) => {
170 if (e.target === modal) {
171 closeModal();
172 }
173 };
174
175 // Escape key to close modal
176 document.addEventListener('keydown', function(e) {
177 if (e.key === 'Escape' && modal.classList.contains('active')) {
178 closeModal();
179 }
180 }, { once: true });
181
182 // Focus the first field
183 labelField.focus();
184
185 // Dispatch an event for the action type selector to catch
186 const event = new CustomEvent('mxchatModalOpened', {
187 detail: {
188 isEdit: isEdit,
189 callbackFunction: callbackFunction || (isEdit ? callbackSelect.value : '')
190 }
191 });
192 document.dispatchEvent(event);
193
194 return closeModal; // Return close function for external use
195 }
196
197 // Initialize event listeners
198 document.addEventListener('DOMContentLoaded', () => {
199 // Set up edit button handlers for intents
200 document.querySelectorAll('.mxchat-edit-button').forEach(button => {
201 button.onclick = () => {
202 const intentId = button.dataset.intentId;
203 const phrases = button.dataset.phrases;
204 mxchatOpenEditModal(intentId, phrases);
205 };
206 });
207
208 document.querySelectorAll('.mxchat-action-card .mxchat-edit-button').forEach(button => {
209 button.onclick = () => {
210 const actionId = button.dataset.actionId;
211 const phrases = button.dataset.phrases;
212 const label = button.dataset.label;
213 const threshold = button.dataset.threshold || 85;
214 const callbackFunction = button.dataset.callbackFunction;
215 const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
216
217 mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
218 };
219 });
220
221 // Set up add new action buttons (new functionality)
222 const addActionBtn = document.getElementById('mxchat-add-action-btn');
223 if (addActionBtn) {
224 addActionBtn.onclick = () => mxchatOpenActionModal();
225 }
226
227 const createFirstAction = document.getElementById('mxchat-create-first-action');
228 if (createFirstAction) {
229 createFirstAction.onclick = () => mxchatOpenActionModal();
230 }
231
232 // Setup category-specific new action buttons (new functionality)
233 document.querySelectorAll('.mxchat-new-action-button').forEach(button => {
234 button.onclick = () => {
235 const category = button.closest('.mxchat-new-action-card').dataset.category;
236 const closeModal = mxchatOpenActionModal();
237
238 // Pre-select the appropriate callback based on category
239 if (category) {
240 const callbackSelect = document.getElementById('callback_function');
241 if (callbackSelect) {
242 setTimeout(() => {
243 // Map categories to default callbacks
244 const categoryToCallback = {
245 'data_collection': 'mxchat_handle_form_collection',
246 'integrations': 'mxchat_handle_slack_message',
247 'custom_actions': 'mxchat_handle_custom_action',
248 'recommendations': 'mxchat_handle_product_recommendations'
249 // Add more mappings as needed
250 };
251
252 if (categoryToCallback[category]) {
253 callbackSelect.value = categoryToCallback[category];
254 }
255 }, 100);
256 }
257 }
258 };
259 });
260
261 // Handle action toggle switches (new functionality)
262 document.querySelectorAll('.mxchat-action-toggle').forEach(toggle => {
263 toggle.onchange = function() {
264 const actionId = this.dataset.actionId;
265 const isEnabled = this.checked;
266
267 // Show loading indicator
268 const loadingEl = document.getElementById('mxchat-action-loading');
269 if (loadingEl) loadingEl.style.display = 'flex';
270
271 // Send AJAX request to update status
272 fetch(ajaxurl, {
273 method: 'POST',
274 headers: {
275 'Content-Type': 'application/x-www-form-urlencoded',
276 },
277 body: new URLSearchParams({
278 action: 'mxchat_toggle_action',
279 intent_id: actionId,
280 enabled: isEnabled ? 1 : 0,
281 nonce: mxchatAdmin.toggle_action_nonce // Use the correct nonce
282 })
283 })
284 .then(response => response.json())
285 .then(data => {
286 if (!data.success) {
287 alert('Failed to update action status: ' + (data.data?.message || 'Unknown error'));
288 this.checked = !isEnabled; // Revert the toggle
289 }
290 })
291 .catch(error => {
292 //console.error('Error:', error);
293 alert('Server error. Please try again.');
294 this.checked = !isEnabled; // Revert the toggle
295 })
296 .finally(() => {
297 if (loadingEl) loadingEl.style.display = 'none';
298 });
299 };
300 });
301
302 // Handle threshold sliders in action cards (new functionality)
303 document.querySelectorAll('.mxchat-threshold-slider').forEach(slider => {
304 slider.oninput = function() {
305 const actionId = this.id.replace('intent_threshold_', '');
306 document.getElementById('threshold_output_' + actionId).textContent = this.value + '%';
307 };
308 });
309
310 // Handle threshold save buttons in action cards (new functionality)
311 document.querySelectorAll('.mxchat-threshold-save').forEach(button => {
312 button.onclick = function(e) {
313 e.preventDefault();
314 const form = this.closest('form');
315 const intentId = form.querySelector('input[name="intent_id"]').value;
316 const threshold = form.querySelector('input[name="intent_threshold"]').value;
317 const nonce = form.querySelector('input[name="_wpnonce"]').value;
318
319 // Show loading indicator
320 const loadingEl = document.getElementById('mxchat-action-loading');
321 if (loadingEl) loadingEl.style.display = 'flex';
322
323 // Send AJAX request
324 fetch(ajaxurl, {
325 method: 'POST',
326 headers: {
327 'Content-Type': 'application/x-www-form-urlencoded',
328 },
329 body: new URLSearchParams({
330 action: 'mxchat_update_intent_threshold',
331 intent_id: intentId,
332 intent_threshold: threshold,
333 _wpnonce: nonce
334 })
335 })
336 .then(response => response.json())
337 .then(data => {
338 if (data.success) {
339 // Visual feedback of success
340 const card = this.closest('.mxchat-action-card');
341 card.style.background = 'rgba(120, 115, 245, 0.1)';
342 setTimeout(() => {
343 card.style.background = 'white';
344 }, 300);
345 } else {
346 alert('Failed to update threshold: ' + (data.data?.message || 'Unknown error'));
347 }
348 })
349 .catch(error => {
350 //console.error('Error:', error);
351 alert('Server error. Please try again.');
352 })
353 .finally(() => {
354 if (loadingEl) loadingEl.style.display = 'none';
355 });
356 };
357 });
358 });
359
360 jQuery(document).ready(function($) {
361 // Ensure we have a debounce function (use lodash if available, otherwise use our implementation)
362 const useDebounce = (window._ && window._.debounce) ? window._.debounce : debounce;
363
364 // --- AJAX Auto-Save ---
365 let $autosaveSections = $('.mxchat-autosave-section');
366
367 // *** ADD THIS: Extend auto-save sections to include Pinecone settings ***
368 const $pineconeAutosaveSection = $('#mxchat-kb-tab-pinecone');
369 if ($pineconeAutosaveSection.length) {
370 $autosaveSections = $autosaveSections.add($pineconeAutosaveSection);
371 //console.log('Added Pinecone section to auto-save monitoring');
372 }
373
374 // Track whether fields have been modified by user
375 const userModifiedFields = new Set();
376
377 if ($autosaveSections.length) {
378 // Track user interactions with input fields to determine if changes are user-initiated
379 $autosaveSections.find('input, textarea, select').on('focus keydown paste', function() {
380 const fieldName = $(this).attr('name');
381 if (fieldName) {
382 userModifiedFields.add(fieldName);
383 }
384 });
385
386 // Handle real-time range slider value updates
387 $autosaveSections.find('input[type="range"]').on('input', function() {
388 const value = $(this).val();
389 $('#threshold_value').text(value);
390 });
391
392 // Handle all input changes (including range slider)
393 $autosaveSections.find('input, textarea, select').not('#model, #openrouter_selected_model').on('change', function() {
394 const $field = $(this);
395 const name = $field.attr('name');
396
397 // Skip saving for API key fields that haven't been interacted with and are empty
398 const isApiKeyField = name && (
399 name === 'loops_api_key' ||
400 name === 'api_key' ||
401 name === 'xai_api_key' ||
402 name === 'claude_api_key' ||
403 name === 'voyage_api_key' ||
404 name === 'gemini_api_key' ||
405 name === 'deepseek_api_key' ||
406 name.indexOf('_api_key') !== -1
407 );
408
409 // Skip processing if:
410 // 1. It's an API key field
411 // 2. The user hasn't interacted with it
412 // 3. The field is empty
413 if (isApiKeyField && !userModifiedFields.has(name) && (!$field.val() || $field.val().trim() === '')) {
414 //console.log('Skipping auto-save for untouched API key field:', name);
415 return;
416 }
417
418 let value;
419
420 // Handle different input types
421 if ($field.attr('type') === 'checkbox') {
422 // *** UPDATED: Handle Pinecone checkboxes differently ***
423 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
424 value = $field.is(':checked') ? '1' : '0';
425 } else {
426 value = $field.is(':checked') ? 'on' : 'off';
427 }
428 } else {
429 value = $field.val();
430 }
431
432 // Create feedback container
433 const feedbackContainer = $('<div class="feedback-container"></div>');
434 const spinner = $('<div class="saving-spinner"></div>');
435 const successIcon = $('<div class="success-icon">✔</div>');
436
437 // Position feedback container based on input type
438 if ($field.closest('.toggle-switch').length) {
439 $field.closest('td').append(feedbackContainer);
440 } else if ($field.closest('.mxchat-toggle-switch').length) {
441 $field.closest('.mxchat-toggle-container').append(feedbackContainer);
442 } else if ($field.closest('.slider-container').length) {
443 $field.closest('.slider-container').after(feedbackContainer);
444 } else {
445 $field.after(feedbackContainer);
446 }
447 feedbackContainer.append(spinner);
448
449 // Determine which AJAX action and nonce to use:
450 var ajaxAction, nonce;
451 // *** UPDATED: Add Pinecone fields to prompts action ***
452 if (name.indexOf('mxchat_prompts_options') !== -1 ||
453 name === 'mxchat_auto_sync_posts' ||
454 name === 'mxchat_auto_sync_pages' ||
455 name.indexOf('mxchat_auto_sync_') === 0 ||
456 name.indexOf('mxchat_pinecone_addon_options') !== -1) { // *** ADD THIS LINE ***
457 ajaxAction = 'mxchat_save_prompts_setting';
458 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
459 } else {
460 // Otherwise, use the existing AJAX action.
461 ajaxAction = 'mxchat_save_setting';
462 nonce = mxchatAdmin.setting_nonce;
463 }
464
465 // *** ADD THIS: Debug logging for Pinecone fields ***
466 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
467 //console.log('Saving Pinecone field:', name, '=', value);
468 }
469
470 // AJAX save request
471 $.ajax({
472 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
473 type: 'POST',
474 data: {
475 action: ajaxAction,
476 name: name,
477 value: value,
478 _ajax_nonce: nonce
479 },
480 success: function(response) {
481 if (response.success) {
482 spinner.fadeOut(200, function() {
483 feedbackContainer.append(successIcon);
484 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
485 feedbackContainer.remove();
486 });
487 });
488
489 // *** ADD THIS: Refresh API key status after saving an API key ***
490 const isApiKeyField = name && (
491 name === 'api_key' ||
492 name === 'xai_api_key' ||
493 name === 'claude_api_key' ||
494 name === 'voyage_api_key' ||
495 name === 'gemini_api_key' ||
496 name === 'deepseek_api_key' ||
497 name === 'openrouter_api_key' ||
498 name.indexOf('_api_key') !== -1
499 );
500
501 if (isApiKeyField && typeof window.mxchatRefreshAPIKeyStatus === 'function') {
502 window.mxchatRefreshAPIKeyStatus();
503 }
504
505 // *** ADD THIS: Update Pinecone checkbox state after successful save ***
506 if (name && name.indexOf('mxchat_pinecone_addon_options[mxchat_use_pinecone]') !== -1) {
507 //console.log('Pinecone toggle saved successfully, value:', value);
508
509 // The checkbox state is already updated by the user interaction
510 // But let's make sure the UI state matches the saved value
511 var $checkbox = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');
512 var settingsDiv = $('.mxchat-pinecone-settings');
513
514 // Double-check the UI state matches what was saved
515 if (value === '1' && !$checkbox.is(':checked')) {
516 $checkbox.prop('checked', true);
517 settingsDiv.slideDown(300);
518 } else if (value === '0' && $checkbox.is(':checked')) {
519 $checkbox.prop('checked', false);
520 settingsDiv.slideUp(300);
521 }
522
523 //console.log('Pinecone UI state synchronized');
524
525 // Check if Knowledge Import tab is currently active
526 if ($('.mxchat-kb-tab-button[data-tab="import"]').hasClass('active')) {
527 // Show a notice that we need to refresh
528 var $knowledgeCard = $('#mxchat-kb-tab-import .mxchat-card').eq(1);
529 if ($knowledgeCard.length > 0) {
530 // Add a refresh notice at the top of the knowledge base card
531 var refreshNotice = $('<div class="notice notice-warning" style="margin: 15px 0; padding: 10px 15px;">' +
532 '<p style="margin: 0;">' +
533 '<span class="dashicons dashicons-info" style="color: #f0ad4e; margin-right: 5px;"></span>' +
534 'Database settings have changed. ' +
535 '<a href="#" onclick="location.reload(); return false;" style="font-weight: bold;">Click here to refresh</a> to see the updated knowledge base.' +
536 '</p></div>');
537
538 $knowledgeCard.prepend(refreshNotice);
539 }
540 } else {
541 // If not on import tab, set a flag to refresh when they go there
542 sessionStorage.setItem('mxchat_pinecone_changed', 'true');
543 }
544 }
545
546 // *** ADD THIS: Debug logging for successful saves ***
547 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
548 //console.log('Pinecone field saved successfully:', name, '=', value);
549 }
550
551 // Check if the response contains a "no changes" message and log it
552 if (response.data && response.data.message === 'No changes detected') {
553 //console.log('No changes detected for field:', name);
554 }
555 } else {
556 // Only show alert for actual errors, not for "no changes"
557 let errorMessage = response.data?.message || 'Unknown error';
558
559 // Don't display an alert for "no changes" message
560 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
561 alert('Error saving: ' + errorMessage);
562 } else {
563 // Still provide visual feedback that no changes were needed
564 spinner.fadeOut(200, function() {
565 feedbackContainer.append(successIcon);
566 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
567 feedbackContainer.remove();
568 });
569 });
570 //console.log('No changes detected for field:', name);
571 return;
572 }
573
574 // Only revert checkbox state if it was an actual error
575 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
576 if ($field.attr('type') === 'checkbox') {
577 $field.prop('checked', !$field.is(':checked'));
578 }
579 }
580
581 // Always clean up the feedback container
582 feedbackContainer.remove();
583 }
584 },
585 error: function(xhr, textStatus, error) {
586 //console.error('AJAX Error:', textStatus, error);
587 alert('An error occurred while saving. Please try again.');
588
589 // Revert checkbox state on error
590 if ($field.attr('type') === 'checkbox') {
591 $field.prop('checked', !$field.is(':checked'));
592 }
593
594 feedbackContainer.remove();
595 }
596 });
597 });
598
599 // Initialize color pickers with debouncing
600 $autosaveSections.find('.my-color-field').each(function() {
601 const $colorField = $(this);
602
603 $(this).wpColorPicker({
604 change: useDebounce(function(event, ui) {
605 // Safety check - ensure we have a valid field and value
606 if (!$colorField || !$colorField.val()) {
607 //console.warn('Color picker not ready');
608 return;
609 }
610
611 const name = $colorField.attr('name');
612 const value = $colorField.val();
613
614 if (!name || !value) {
615 //console.warn('Missing required color picker values');
616 return;
617 }
618
619 // Create feedback container
620 const feedbackContainer = $('<div class="feedback-container"></div>');
621 const spinner = $('<div class="saving-spinner"></div>');
622 const successIcon = $('<div class="success-icon">✔</div>');
623
624 // Position feedback container
625 $colorField.closest('.wp-picker-container').after(feedbackContainer);
626 feedbackContainer.append(spinner);
627
628 // Determine which AJAX action and nonce to use:
629 var ajaxAction, nonce;
630 // Use the new AJAX action for submenu fields:
631 if (name.indexOf('mxchat_prompts_options') !== -1 ||
632 name === 'mxchat_auto_sync_posts' ||
633 name === 'mxchat_auto_sync_pages' ||
634 name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields
635 ajaxAction = 'mxchat_save_prompts_setting';
636 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
637 } else {
638 // Otherwise, use the existing AJAX action.
639 ajaxAction = 'mxchat_save_setting';
640 nonce = mxchatAdmin.setting_nonce;
641 }
642 // AJAX save request
643 $.ajax({
644 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
645 type: 'POST',
646 data: {
647 action: ajaxAction,
648 name: name,
649 value: value,
650 _ajax_nonce: nonce
651 },
652 success: function(response) {
653 if (response.success) {
654 spinner.fadeOut(200, function() {
655 feedbackContainer.append(successIcon);
656 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
657 feedbackContainer.remove();
658 });
659 });
660 } else {
661 alert('Error saving: ' + (response.data?.message || 'Unknown error'));
662 feedbackContainer.remove();
663 }
664 },
665 error: function() {
666 alert('An error occurred while saving.');
667 feedbackContainer.remove();
668 }
669 });
670 }, 500)
671 });
672 });
673
674 // Reinitialize color pickers when switching tabs
675 $('.mxchat-tab-button').on('click.mxchat', function() {
676 setTimeout(function() {
677 $('.my-color-field:visible').wpColorPicker('close');
678 }, 100);
679 });
680 }
681
682 // Initialize tabs system
683 function initTabs() {
684 // Remove any existing handlers first
685 $('.mxchat-tab-button').off('click.mxchat');
686
687 // Add new click handlers
688 $('.mxchat-tab-button').on('click.mxchat', function(e) {
689 e.preventDefault();
690 e.stopPropagation();
691
692 var $this = $(this);
693
694 // Get tab ID from data-tab attribute
695 var tabId = $this.data('tab') || 'chatbot';
696
697 // Safety check for empty tabId
698 if (!tabId) {
699 //console.warn('No tab identifier found');
700 return;
701 }
702
703 // Update tab buttons
704 $('.mxchat-tab-button').removeClass('active');
705 $this.addClass('active');
706
707 // Update content areas - with safety check
708 $('.mxchat-tab-content').removeClass('active');
709 var $targetTab = $('#' + tabId);
710 if ($targetTab.length) {
711 $targetTab.addClass('active');
712 // Removed localStorage saving functionality
713 } else {
714 //console.warn('Tab content #' + tabId + ' not found');
715 }
716 });
717 }
718
719 // Initialize tabs and handle events
720 initTabs();
721 $(document).on('widget-added widget-updated postbox-toggled', initTabs);
722
723 // Always activate the first tab (Chatbot)
724 $('.mxchat-tab-button').first().trigger('click.mxchat');
725
726
727 // Toggle visibility handlers
728 function toggleVisibility(selector) {
729 $(selector).on('click', function() {
730 var inputField = $(this).prev('input');
731 if (inputField.attr('type') === 'password') {
732 inputField.attr('type', 'text');
733 $(this).text('Hide');
734 } else {
735 inputField.attr('type', 'password');
736 $(this).text('Show');
737 }
738 });
739 }
740
741 // Initialize all toggle visibility buttons
742 [
743 '#toggleApiKeyVisibility',
744 '#toggleWooCommerceSecretVisibility',
745 '#toggleVoyageAPIKeyVisibility',
746 '#toggleLoopsApiKeyVisibility',
747 '#toggleXaiApiKeyVisibility',
748 '#toggleClaudeApiKeyVisibility',
749 '#toggleBraveApiKeyVisibility',
750 '#toggleWebhookUrlVisibility',
751 '#toggleSecretKeyVisibility',
752 '#toggleBotTokenVisibility',
753 '#toggleDeepSeekApiKeyVisibility',
754 '#toggleGeminiApiKeyVisibility',
755 '#toggleOpenRouterApiKeyVisibility'
756 ].forEach(toggleVisibility);
757
758 function setupMxChatModelSelector() {
759 const $modelSelect = $('#model');
760 const $modelSelectorButton = $('<button>', {
761 type: 'button',
762 id: 'mxchat_model_selector_btn',
763 class: 'button-primary mxchat-model-selector-btn',
764 text: 'Select AI Model'
765 });
766
767 // Replace the select dropdown with a button
768 $modelSelect.hide().after($modelSelectorButton);
769
770 // Update button text to show currently selected model
771 function updateButtonText() {
772 const selectedModel = $modelSelect.val();
773
774 // Check if OpenRouter is selected
775 if (selectedModel === 'openrouter') {
776 const openrouterModelId = $('#openrouter_selected_model').val();
777 const openrouterModelName = $('#openrouter_selected_model_name').val();
778
779 if (openrouterModelName && openrouterModelName.trim() !== '') {
780 $modelSelectorButton.text('OpenRouter: ' + openrouterModelName);
781 } else if (openrouterModelId && openrouterModelId.trim() !== '') {
782 $modelSelectorButton.text('OpenRouter: ' + openrouterModelId);
783 } else {
784 $modelSelectorButton.text('OpenRouter - Select Model');
785 }
786 } else {
787 const selectedModelText = $modelSelect.find('option:selected').text();
788 $modelSelectorButton.text(selectedModelText);
789 }
790 }
791
792 // Initialize button text
793 updateButtonText();
794
795 // Create and append modal HTML
796 const modelSelectorModal = `
797 <div id="mxchat_model_selector_modal" class="mxchat-model-selector-modal">
798 <div class="mxchat-model-selector-modal-content">
799 <div class="mxchat-model-selector-modal-header">
800 <h3>Select AI Model</h3>
801 <span class="mxchat-model-selector-modal-close">&times;</span>
802 </div>
803 <div class="mxchat-model-selector-modal-body">
804 <div class="mxchat-model-selector-search-container">
805 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
806 </div>
807 <div class="mxchat-model-selector-categories">
808 <button class="mxchat-model-category-btn active" data-category="all">All</button>
809 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
810 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
811 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
812 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
813 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
814 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
815 </div>
816 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
817 </div>
818 <div class="mxchat-model-selector-modal-footer">
819 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
820 </div>
821 </div>
822 </div>
823 `;
824
825 $('body').append(modelSelectorModal);
826
827 // MOVE THIS OUTSIDE - Make it a property of the window object so it's accessible globally
828 window.populateModelsGrid = function(filter = '', category = 'all') {
829 const $grid = $('#mxchat_models_grid');
830 $grid.empty();
831
832 const models = {
833 openrouter: [
834 { value: 'openrouter', label: 'OpenRouter', description: 'Access 100+ models from multiple providers (add API key to browse)' }
835 ],
836 gemini: [
837 { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Next-Gen features, speed & multimodal generation' },
838 { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Cost-efficient with low latency' },
839 { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Complex reasoning tasks requiring more intelligence' },
840 { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Fast and versatile performance' },
841 ],
842 openai: [
843 { value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1', description: 'Flagship for coding & agentic tasks with low reasoning (400K context)' },
844 { value: 'gpt-5', label: 'GPT-5', description: 'Flagship for coding, reasoning, and agentic tasks across domains' },
845 { value: 'gpt-5-mini', label: 'GPT-5 Mini', description: 'Faster, more cost-efficient for well-defined tasks and precise prompts' },
846 { value: 'gpt-5-nano', label: 'GPT-5 Nano', description: 'Fastest and cheapest; ideal for summarization and classification' },
847 { value: 'gpt-4.1-2025-04-14', label: 'GPT-4.1', description: 'Flagship model for complex tasks' },
848 { value: 'gpt-4o', label: 'GPT-4o', description: 'Recommended for most use cases' },
849 { value: 'gpt-4o-mini', label: 'GPT-4o Mini', description: 'Fast and lightweight' },
850 { value: 'gpt-4-turbo', label: 'GPT-4 Turbo', description: 'High-performance model' },
851 { value: 'gpt-4', label: 'GPT-4', description: 'High intelligence model' },
852 { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', description: 'Affordable and fast' },
853 ],
854 claude: [
855 { value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5', description: 'Best for complex agents and coding' },
856 { value: 'claude-opus-4-1-20250805', label: 'Claude Opus 4.1', description: 'Exceptional for specialized complex tasks' },
857 { value: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5', description: 'Fastest and most intelligent Haiku' },
858 { value: 'claude-opus-4-20250514', label: 'Claude 4 Opus', description: 'Most capable Claude model' },
859 { value: 'claude-sonnet-4-20250514', label: 'Claude 4 Sonnet', description: 'High performance' },
860 { value: 'claude-3-7-sonnet-20250219', label: 'Claude 3.7 Sonnet', description: 'High intelligence' },
861 { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus', description: 'Highly complex tasks' },
862 { value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', description: 'Balanced performance' },
863 { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', description: 'Fastest Claude model' },
864 ],
865 xai: [
866 { value: 'grok-4-1-fast-reasoning', label: 'Grok 4.1 Fast (Reasoning)', description: '2M context window and reasoning' },
867 { value: 'grok-4-1-fast-non-reasoning', label: 'Grok 4.1 Fast (Non-Reasoning)', description: '2M context window and faster responses' },
868 { value: 'grok-4-0709', label: 'Grok 4', description: 'Latest flagship model - unparalleled performance in natural language, math and reasoning' },
869 { value: 'grok-3-beta', label: 'Grok-3', description: 'Powerful model with 131K context' },
870 { value: 'grok-3-fast-beta', label: 'Grok-3 Fast', description: 'High performance with faster responses' },
871 { value: 'grok-3-mini-beta', label: 'Grok-3 Mini', description: 'Affordable model with good performance' },
872 { value: 'grok-3-mini-fast-beta', label: 'Grok-3 Mini Fast', description: 'Quick and cost-effective' },
873 { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' },
874 ],
875 deepseek: [
876 { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
877 ],
878 };
879
880 let allModels = [];
881 Object.keys(models).forEach(key => {
882 if (category === 'all' || category === key) {
883 allModels = allModels.concat(models[key]);
884 }
885 });
886
887 // Filter by search term if present
888 if (filter) {
889 const lowerFilter = filter.toLowerCase();
890 allModels = allModels.filter(model =>
891 model.label.toLowerCase().includes(lowerFilter) ||
892 model.description.toLowerCase().includes(lowerFilter)
893 );
894 }
895
896 // Create model cards
897 allModels.forEach(model => {
898 const isSelected = $modelSelect.val() === model.value;
899 const $modelCard = $(`
900 <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
901 <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
902 <div class="mxchat-model-selector-info">
903 <h4 class="mxchat-model-selector-title">${model.label}</h4>
904 <p class="mxchat-model-selector-description">${model.description}</p>
905 </div>
906 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
907 </div>
908 `);
909 $grid.append($modelCard);
910 });
911 };
912
913 // Helper function to get icon for each model
914 function getModelIcon(modelValue) {
915 if (modelValue === 'openrouter') return '<span class="dashicons dashicons-networking" style="font-size: 24px; color: #6750A4;"></span>';
916 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>';
917 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>';
918 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>';
919 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>';
920 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>';
921 return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
922 }
923
924 // Event handlers
925 $modelSelectorButton.on('click', function() {
926 $('#mxchat_model_selector_modal').show();
927 window.populateModelsGrid('', 'all');
928 });
929
930 $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
931 $('#mxchat_model_selector_modal').hide();
932 });
933
934 $('.mxchat-model-category-btn').on('click', function() {
935 $('.mxchat-model-category-btn').removeClass('active');
936 $(this).addClass('active');
937 const category = $(this).data('category');
938 const searchTerm = $('#mxchat_model_search_input').val();
939 window.populateModelsGrid(searchTerm, category);
940 });
941
942 $('#mxchat_model_search_input').on('input', function() {
943 const searchTerm = $(this).val();
944 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
945 window.populateModelsGrid(searchTerm, activeCategory);
946 });
947
948 $(document).on('click', '.mxchat-model-selector-card', function() {
949 const modelValue = $(this).data('value');
950 const $modelSelect = $('#model');
951 const $clickedCard = $(this);
952
953 // Check if OpenRouter was selected
954 if (modelValue === 'openrouter') {
955 // Load OpenRouter models instead of closing
956 loadOpenRouterModels();
957 } else {
958 // Remove selection from all other cards
959 $('.mxchat-model-selector-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
960
961 // Add selection to clicked card immediately for instant feedback
962 $clickedCard.addClass('mxchat-model-selected');
963 if ($clickedCard.find('.mxchat-model-selector-checkmark').length === 0) {
964 $clickedCard.append('<div class="mxchat-model-selector-checkmark">✓</div>');
965 }
966
967 // Brief delay to show the selection, then start saving
968 setTimeout(function() {
969 // Normal model selection
970 $modelSelect.val(modelValue).trigger('change');
971
972 // Show loading state on the card
973 $clickedCard.css('pointer-events', 'none');
974 const originalContent = $clickedCard.find('.mxchat-model-selector-title').html();
975 $clickedCard.find('.mxchat-model-selector-title').html(
976 '<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...'
977 );
978
979 // Manually save the model via AJAX
980 jQuery.ajax({
981 url: mxchatAdmin.ajax_url,
982 type: 'POST',
983 data: {
984 action: 'mxchat_save_setting',
985 name: 'model',
986 value: modelValue,
987 _ajax_nonce: mxchatAdmin.setting_nonce
988 },
989 success: function(response) {
990 if (response.success) {
991 // Show success state
992 $clickedCard.find('.mxchat-model-selector-title').html(
993 '<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!'
994 );
995
996 // Update button text after successful save
997 const selectedModelText = $modelSelect.find('option:selected').text();
998 $('#mxchat_model_selector_btn').text(selectedModelText);
999
1000 // Close modal after a short delay to show the success message
1001 setTimeout(function() {
1002 $('#mxchat_model_selector_modal').hide();
1003 // Restore original content and remove selection for next time
1004 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1005 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1006 $clickedCard.css('pointer-events', 'auto');
1007 }, 600);
1008 } else {
1009 // Show error state
1010 $clickedCard.find('.mxchat-model-selector-title').html(
1011 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1012 );
1013
1014 setTimeout(function() {
1015 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1016 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1017 $clickedCard.css('pointer-events', 'auto');
1018 }, 1500);
1019 }
1020 },
1021 error: function() {
1022 // Show error state
1023 $clickedCard.find('.mxchat-model-selector-title').html(
1024 '<span class="dashicons dashicons-no" style="color: #dc3232;"></span> Error!'
1025 );
1026
1027 setTimeout(function() {
1028 $clickedCard.find('.mxchat-model-selector-title').html(originalContent);
1029 $clickedCard.removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1030 $clickedCard.css('pointer-events', 'auto');
1031 }, 1500);
1032 }
1033 });
1034 }, 300); // 300ms delay to show the selection before starting to save
1035 }
1036 });
1037
1038 // Close modal when clicking outside
1039 $(window).on('click', function(event) {
1040 if ($(event.target).is('#mxchat_model_selector_modal')) {
1041 $('#mxchat_model_selector_modal').hide();
1042 }
1043 });
1044 }
1045
1046 function loadOpenRouterModels() {
1047 const apiKey = $('#openrouter_api_key').val(); // This line already re-checks the field
1048 const $modal = $('#mxchat_model_selector_modal');
1049 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1050
1051 if (!apiKey || apiKey.trim() === '') {
1052 // Show error message in modal
1053 $modalBody.html(`
1054 <div style="text-align: center; padding: 40px;">
1055 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1056 <h3>OpenRouter API Key Required</h3>
1057 <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>
1058 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1059 </div>
1060 `);
1061
1062 $('#mxchat_back_to_models').on('click', function(e) {
1063 e.preventDefault();
1064 // CHANGE THIS: Instead of reloading, restore the original modal content
1065 $modalBody.html(`
1066 <div class="mxchat-model-selector-search-container">
1067 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1068 </div>
1069 <div class="mxchat-model-selector-categories">
1070 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1071 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1072 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1073 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1074 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1075 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1076 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1077 </div>
1078 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1079 `);
1080
1081 // Re-populate the grid
1082 populateModelsGrid('', 'all');
1083
1084 // Re-bind event handlers
1085 rebindModalEventHandlers();
1086 });
1087 return;
1088 }
1089
1090 // Show loading state
1091 $modalBody.html(`
1092 <div style="text-align: center; padding: 60px 20px;">
1093 <div class="spinner is-active" style="float: none; margin: 0 auto 20px;"></div>
1094 <h3>Loading OpenRouter Models...</h3>
1095 <p>Fetching available models from OpenRouter</p>
1096 </div>
1097 `);
1098
1099 // Fetch models from OpenRouter
1100 jQuery.ajax({
1101 url: mxchatAdmin.ajax_url,
1102 type: 'POST',
1103 data: {
1104 action: 'mxchat_fetch_openrouter_models',
1105 api_key: apiKey,
1106 nonce: mxchatAdmin.fetch_openrouter_models_nonce
1107 },
1108 success: function(response) {
1109 if (response.success && response.data.models) {
1110 displayOpenRouterModels(response.data.models);
1111 } else {
1112 $modalBody.html(`
1113 <div style="text-align: center; padding: 40px;">
1114 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1115 <h3>Error Loading Models</h3>
1116 <p>${response.data.message || 'Failed to load models from OpenRouter'}</p>
1117 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1118 </div>
1119 `);
1120
1121 $('#mxchat_back_to_models').on('click', function(e) {
1122 e.preventDefault();
1123 // CHANGE THIS: Restore original content instead of reloading
1124 restoreOriginalModalContent();
1125 });
1126 }
1127 },
1128 error: function() {
1129 $modalBody.html(`
1130 <div style="text-align: center; padding: 40px;">
1131 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1132 <h3>Connection Error</h3>
1133 <p>Failed to connect to OpenRouter. Please check your API key and try again.</p>
1134 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1135 </div>
1136 `);
1137
1138 $('#mxchat_back_to_models').on('click', function(e) {
1139 e.preventDefault();
1140 // CHANGE THIS: Restore original content instead of reloading
1141 restoreOriginalModalContent();
1142 });
1143 }
1144 });
1145 }
1146 function restoreOriginalModalContent() {
1147 const $modalBody = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-body');
1148
1149 $modalBody.html(`
1150 <div class="mxchat-model-selector-search-container">
1151 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1152 </div>
1153 <div class="mxchat-model-selector-categories">
1154 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1155 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1156 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1157 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1158 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1159 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1160 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1161 </div>
1162 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1163 `);
1164
1165 // Re-populate the grid
1166 window.populateModelsGrid('', 'all');
1167
1168 // Re-bind event handlers
1169 rebindModalEventHandlers();
1170 }
1171 function rebindModalEventHandlers() {
1172 const $modal = $('#mxchat_model_selector_modal');
1173
1174 // Re-bind category button clicks
1175 $('.mxchat-model-category-btn').off('click').on('click', function() {
1176 $('.mxchat-model-category-btn').removeClass('active');
1177 $(this).addClass('active');
1178 const category = $(this).data('category');
1179 const searchTerm = $('#mxchat_model_search_input').val();
1180 window.populateModelsGrid(searchTerm, category);
1181 });
1182
1183 // Re-bind search input
1184 $('#mxchat_model_search_input').off('input').on('input', function() {
1185 const searchTerm = $(this).val();
1186 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1187 populateModelsGrid(searchTerm, activeCategory);
1188 });
1189 }
1190
1191 function restoreDefaultModalFooter() {
1192 const $modalFooter = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-footer');
1193
1194 // Restore default footer buttons
1195 $modalFooter.html(`
1196 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
1197 `);
1198
1199 // Re-bind cancel button
1200 $('#mxchat_cancel_model_selection').on('click', function() {
1201 $('#mxchat_model_selector_modal').hide();
1202 });
1203 }
1204
1205 function displayOpenRouterModels(models) {
1206 const $modal = $('#mxchat_model_selector_modal');
1207 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1208 const $modalFooter = $modal.find('.mxchat-model-selector-modal-footer');
1209 const currentSelected = $('#openrouter_selected_model').val();
1210
1211 // Variable to store the currently selected model (in the UI, not yet saved)
1212 let pendingSelection = {
1213 modelId: currentSelected || null,
1214 modelName: $('#openrouter_selected_model_name').val() || null
1215 };
1216
1217 // Build new modal content with search and models
1218 const newContent = `
1219 <div class="mxchat-model-selector-search-container">
1220 <input type="text" id="mxchat_openrouter_search" class="mxchat-model-search-input" placeholder="Search OpenRouter models...">
1221 <p style="margin: 10px 0; color: #666; font-size: 13px;">
1222 <strong>${models.length} models available</strong> ·
1223 <a href="#" id="mxchat_back_to_provider_select" style="color: #2271b1;">← Back to providers</a>
1224 </p>
1225 </div>
1226 <div class="mxchat-model-selector-grid" id="mxchat_openrouter_models_grid"></div>
1227 `;
1228
1229 // Update footer with Save button for OpenRouter
1230 const footerContent = `
1231 <button id="mxchat_back_to_models_footer" class="button mxchat-model-cancel-btn">Back to Providers</button>
1232 <button id="mxchat_save_openrouter_model" class="button button-primary" disabled>
1233 <span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model
1234 </button>
1235 `;
1236
1237 $modalBody.html(newContent);
1238 $modalFooter.html(footerContent);
1239
1240 // Function to render models
1241 function renderOpenRouterModels(filterText = '') {
1242 const $grid = $('#mxchat_openrouter_models_grid');
1243 $grid.empty();
1244
1245 let filteredModels = models;
1246 if (filterText) {
1247 const lowerFilter = filterText.toLowerCase();
1248 filteredModels = models.filter(m =>
1249 m.id.toLowerCase().includes(lowerFilter) ||
1250 m.name.toLowerCase().includes(lowerFilter) ||
1251 (m.description && m.description.toLowerCase().includes(lowerFilter))
1252 );
1253 }
1254
1255 filteredModels.forEach(model => {
1256 const isSelected = pendingSelection.modelId === model.id;
1257 const contextLength = model.context_length ? `${(model.context_length / 1000).toFixed(0)}K` : '';
1258 const promptPrice = model.pricing.prompt ? `$${(model.pricing.prompt * 1000000).toFixed(2)}/1M` : '';
1259
1260 const $card = jQuery(`
1261 <div class="mxchat-openrouter-card ${isSelected ? 'mxchat-model-selected' : ''}" data-model-id="${model.id}" data-model-name="${model.name}">
1262 <div class="mxchat-model-selector-icon">
1263 ${getOpenRouterIcon(model.id)}
1264 </div>
1265 <div class="mxchat-model-selector-info">
1266 <h4 class="mxchat-model-selector-title">${model.name}</h4>
1267 <div style="font-size: 12px; color: #666; margin-top: 5px;">
1268 ${contextLength ? '<span style="margin-right: 12px;">📄 ' + contextLength + '</span>' : ''}
1269 ${promptPrice ? '<span>💰 ' + promptPrice + '</span>' : ''}
1270 </div>
1271 </div>
1272 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1273 </div>
1274 `);
1275
1276 $grid.append($card);
1277 });
1278 }
1279
1280 // Helper to get icon
1281 function getOpenRouterIcon(modelId) {
1282 if (modelId.includes('gpt') || modelId.includes('openai')) {
1283 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>';
1284 } else if (modelId.includes('claude') || modelId.includes('anthropic')) {
1285 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>';
1286 } else if (modelId.includes('gemini') || modelId.includes('google')) {
1287 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>';
1288 }
1289 return '<span class="dashicons dashicons-cloud" style="font-size: 24px; color: #6750A4;"></span>';
1290 }
1291
1292 // Initial render
1293 renderOpenRouterModels();
1294
1295 // Search handler
1296 $('#mxchat_openrouter_search').on('input', function() {
1297 renderOpenRouterModels($(this).val());
1298 });
1299
1300 $('#mxchat_back_to_provider_select').on('click', function(e) {
1301 e.preventDefault();
1302 // Restore footer to default state
1303 restoreDefaultModalFooter();
1304 // Instead of location.reload(), restore original content
1305 restoreOriginalModalContent();
1306 });
1307
1308 // Back to providers footer button
1309 $('#mxchat_back_to_models_footer').on('click', function(e) {
1310 e.preventDefault();
1311 // Restore footer to default state
1312 restoreDefaultModalFooter();
1313 // Restore original content
1314 restoreOriginalModalContent();
1315 });
1316
1317 // Model selection - just highlight, don't save yet
1318 $(document).on('click', '.mxchat-openrouter-card', function(e) {
1319 e.stopPropagation(); // Prevent triggering the regular model card handler
1320
1321 const modelId = $(this).data('model-id');
1322 const modelName = $(this).data('model-name');
1323
1324 // Remove selection from all cards
1325 $('.mxchat-openrouter-card').removeClass('mxchat-model-selected').find('.mxchat-model-selector-checkmark').remove();
1326
1327 // Add selection to clicked card
1328 $(this).addClass('mxchat-model-selected');
1329 if ($(this).find('.mxchat-model-selector-checkmark').length === 0) {
1330 $(this).append('<div class="mxchat-model-selector-checkmark">✓</div>');
1331 }
1332
1333 // Update pending selection
1334 pendingSelection.modelId = modelId;
1335 pendingSelection.modelName = modelName;
1336
1337 // Enable the save button
1338 $('#mxchat_save_openrouter_model').prop('disabled', false);
1339 });
1340
1341 // Save button handler
1342 $('#mxchat_save_openrouter_model').on('click', function() {
1343 const $saveButton = $(this);
1344
1345 if (!pendingSelection.modelId) {
1346 return;
1347 }
1348
1349 // Disable button and show loading state
1350 $saveButton.prop('disabled', true).html('<span class="spinner is-active" style="float: none; margin: 0 5px 0 0;"></span> Saving...');
1351
1352 // First, save that we're using OpenRouter
1353 jQuery.ajax({
1354 url: mxchatAdmin.ajax_url,
1355 type: 'POST',
1356 data: {
1357 action: 'mxchat_save_setting',
1358 name: 'model',
1359 value: 'openrouter',
1360 _ajax_nonce: mxchatAdmin.setting_nonce
1361 },
1362 success: function() {
1363 // After model is set, save the model ID
1364 jQuery.ajax({
1365 url: mxchatAdmin.ajax_url,
1366 type: 'POST',
1367 data: {
1368 action: 'mxchat_save_setting',
1369 name: 'openrouter_selected_model',
1370 value: pendingSelection.modelId,
1371 _ajax_nonce: mxchatAdmin.setting_nonce
1372 },
1373 success: function() {
1374 // Update DOM immediately
1375 $('#openrouter_selected_model').val(pendingSelection.modelId);
1376
1377 // After model ID is saved, save the display name
1378 jQuery.ajax({
1379 url: mxchatAdmin.ajax_url,
1380 type: 'POST',
1381 data: {
1382 action: 'mxchat_save_setting',
1383 name: 'openrouter_selected_model_name',
1384 value: pendingSelection.modelName,
1385 _ajax_nonce: mxchatAdmin.setting_nonce
1386 },
1387 success: function() {
1388 // Update DOM immediately
1389 $('#openrouter_selected_model_name').val(pendingSelection.modelName);
1390
1391 // Update button text
1392 $('#mxchat_model_selector_btn').text('OpenRouter: ' + pendingSelection.modelName);
1393
1394 // Update the "Currently using" message
1395 const $currentSelection = $('#openrouter-current-selection');
1396 if ($currentSelection.length) {
1397 $currentSelection.html(
1398 '<span class="dashicons dashicons-yes" style="font-size: 16px; vertical-align: middle;"></span> ' +
1399 'Currently using: <strong>' + pendingSelection.modelName + '</strong>'
1400 ).show();
1401 }
1402
1403 // Show success state briefly
1404 $saveButton.html('<span class="dashicons dashicons-yes" style="color: #46b450; margin-top: 3px;"></span> Saved!');
1405
1406 // Close modal after short delay
1407 setTimeout(function() {
1408 // Restore footer to default state
1409 restoreDefaultModalFooter();
1410 $modal.hide();
1411 }, 800);
1412 },
1413 error: function() {
1414 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1415 alert('Failed to save model name. Please try again.');
1416 }
1417 });
1418 },
1419 error: function() {
1420 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1421 alert('Failed to save model ID. Please try again.');
1422 }
1423 });
1424 },
1425 error: function() {
1426 $saveButton.prop('disabled', false).html('<span class="dashicons dashicons-saved" style="margin-top: 3px;"></span> Save Selected Model');
1427 alert('Failed to save OpenRouter selection. Please try again.');
1428 }
1429 });
1430 });
1431
1432 }
1433
1434 // Embedding model selector - completely separate from chat model selector
1435 function setupMxChatEmbeddingModelSelector() {
1436 const $embeddingModelSelect = $('#embedding_model');
1437
1438 // Skip if the element doesn't exist on the page
1439 if ($embeddingModelSelect.length === 0) {
1440 return;
1441 }
1442
1443 const $embeddingModelSelectorButton = $('<button>', {
1444 type: 'button',
1445 id: 'mxchat_embedding_model_selector_btn',
1446 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
1447 text: 'Select Embedding Model'
1448 });
1449
1450 // Replace the select dropdown with a button
1451 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
1452
1453 // Update button text to show currently selected model
1454 function updateButtonText() {
1455 const selectedModel = $embeddingModelSelect.val();
1456 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
1457 $embeddingModelSelectorButton.text(selectedModelText);
1458 }
1459
1460 // Initialize button text
1461 updateButtonText();
1462
1463 // Create a unique ID for the modal to avoid conflicts
1464 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
1465
1466 // Create and append modal HTML with unique IDs
1467 const embeddingModelSelectorModal = `
1468 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
1469 <div class="mxchat-embedding-model-selector-modal-content">
1470 <div class="mxchat-embedding-model-selector-modal-header">
1471 <h3>Select Embedding Model</h3>
1472 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
1473 </div>
1474 <div class="mxchat-embedding-model-selector-modal-body">
1475 <div class="mxchat-embedding-model-selector-search-container">
1476 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
1477 </div>
1478 <div class="mxchat-embedding-model-selector-categories">
1479 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
1480 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
1481 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
1482 <button class="mxchat-embedding-model-category-btn" data-category="gemini">Google Gemini</button>
1483 </div>
1484 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
1485 </div>
1486 <div class="mxchat-embedding-model-selector-modal-footer">
1487 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
1488 </div>
1489 </div>
1490 </div>
1491 `;
1492
1493 // Use jQuery's append to ensure it doesn't clash with existing modals
1494 $('body').append(embeddingModelSelectorModal);
1495
1496 // Populate models grid
1497 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1498 const $grid = $('#mxchat_embedding_models_grid');
1499 $grid.empty();
1500
1501 // Define embedding models with descriptions and context lengths
1502 const models = {
1503 openai: [
1504 {
1505 value: 'text-embedding-3-small',
1506 label: 'TE3 Small',
1507 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1508 },
1509 {
1510 value: 'text-embedding-ada-002',
1511 label: 'Ada 2',
1512 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1513 },
1514 {
1515 value: 'text-embedding-3-large',
1516 label: 'TE3 Large',
1517 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1518 }
1519 ],
1520 voyage: [
1521 {
1522 value: 'voyage-3-large',
1523 label: 'Voyage-3 Large',
1524 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1525 }
1526 ],
1527 gemini: [
1528 {
1529 value: 'gemini-embedding-exp-03-07',
1530 label: 'Gemini Embedding',
1531 description: 'Experimental SOTA embeddings (1536 dimensions, 8K context)'
1532 }
1533 ]
1534 };
1535
1536 let allModels = [];
1537 Object.keys(models).forEach(key => {
1538 if (category === 'all' || category === key) {
1539 allModels = allModels.concat(models[key]);
1540 }
1541 });
1542
1543 // Filter by search term if present
1544 if (filter) {
1545 const lowerFilter = filter.toLowerCase();
1546 allModels = allModels.filter(model =>
1547 model.label.toLowerCase().includes(lowerFilter) ||
1548 model.description.toLowerCase().includes(lowerFilter)
1549 );
1550 }
1551
1552 // Create model cards
1553 allModels.forEach(model => {
1554 const isSelected = $embeddingModelSelect.val() === model.value;
1555 let providerClass = 'mxchat-embedding-model-provider-openai';
1556
1557 if (model.value.startsWith('voyage-')) {
1558 providerClass = 'mxchat-embedding-model-provider-voyage';
1559 } else if (model.value.startsWith('gemini-embedding-')) {
1560 providerClass = 'mxchat-embedding-model-provider-gemini';
1561 }
1562
1563 let iconHTML = '';
1564 if (model.value.startsWith('voyage-')) {
1565 iconHTML = '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>';
1566 } else if (model.value.startsWith('gemini-embedding-')) {
1567 iconHTML = '<span class="dashicons dashicons-google mxchat-embedding-model-icon-gemini"></span>';
1568 } else {
1569 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>';
1570 }
1571
1572 const $modelCard = $(`
1573 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1574 <div class="mxchat-embedding-model-selector-icon">
1575 ${iconHTML}
1576 </div>
1577 <div class="mxchat-embedding-model-selector-info">
1578 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1579 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1580 </div>
1581 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1582 </div>
1583 `);
1584
1585 $grid.append($modelCard);
1586 });
1587 }
1588
1589 // Event handlers - use namespaced events to avoid conflicts
1590 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1591 e.stopPropagation(); // Prevent event bubbling
1592 $('#' + embeddingModalId).show();
1593 populateEmbeddingModelsGrid('', 'all');
1594 });
1595
1596 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1597 e.stopPropagation(); // Prevent event bubbling
1598 $('#' + embeddingModalId).hide();
1599 });
1600
1601 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1602 e.stopPropagation(); // Prevent event bubbling
1603 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1604 $(this).addClass('active');
1605 const category = $(this).data('category');
1606 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1607 populateEmbeddingModelsGrid(searchTerm, category);
1608 });
1609
1610 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1611 const searchTerm = $(this).val();
1612 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1613 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1614 });
1615
1616 // Use a direct selector to avoid conflicts with other card elements
1617 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1618 e.stopPropagation(); // Prevent event bubbling
1619 const modelValue = $(this).data('value');
1620
1621 // Important: Only update this specific select element
1622 $embeddingModelSelect.val(modelValue);
1623
1624 // Manually trigger change only on this element
1625 const changeEvent = new Event('change', { bubbles: true });
1626 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1627
1628 // Update button text
1629 updateButtonText();
1630
1631 // Hide modal
1632 $('#' + embeddingModalId).hide();
1633 });
1634
1635 // Close modal when clicking outside - use namespaced events
1636 $(window).on('click.embeddingModelSelector', function(event) {
1637 if ($(event.target).is('#' + embeddingModalId)) {
1638 $('#' + embeddingModalId).hide();
1639 }
1640 });
1641 }
1642
1643 // Call this function after the DOM is fully loaded
1644 $(document).ready(function() {
1645 setupMxChatModelSelector();
1646 setupMxChatEmbeddingModelSelector();
1647 });
1648
1649 // Add Intent Form Submission
1650 $('#mxchat-add-intent-form').on('submit', function(event) {
1651 $('#mxchat-intent-loading').show();
1652 $('#mxchat-intent-loading-text').show();
1653 $(this).find('button[type="submit"]').hide();
1654 });
1655
1656 // Inline Edit Functionality
1657 $('.edit-button').on('click', function() {
1658 var row = $(this).closest('tr');
1659 row.find('.content-view, .url-view').hide();
1660 row.find('.content-edit, .url-edit').show();
1661 row.find('.edit-button').hide();
1662 row.find('.save-button').show();
1663 });
1664
1665 // Save button handler
1666 // Save button handler
1667 $('.save-button').on('click', function() {
1668 var button = $(this);
1669 var row = button.closest('tr');
1670 var id = button.data('id');
1671 var nonce = button.data('nonce'); // Get nonce from button data attribute
1672 var newContent = row.find('.content-edit').val();
1673 var newUrl = row.find('.url-edit').val();
1674
1675 //console.log('Nonce from button:', nonce); // Debug
1676
1677 button.prop('disabled', true);
1678 button.text('Saving...');
1679
1680 $.ajax({
1681 url: mxchatAdmin.ajax_url,
1682 type: 'POST',
1683 data: {
1684 action: 'mxchat_save_inline_prompt',
1685 id: id,
1686 article_content: newContent,
1687 article_url: newUrl,
1688 _ajax_nonce: nonce // Use nonce from button
1689 },
1690 success: function(response) {
1691 button.prop('disabled', false);
1692 button.text('Save');
1693
1694 if (response.success) {
1695 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1696 if (newUrl) {
1697 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank">' + newUrl + '</a>');
1698 } else {
1699 row.find('.url-view').html('N/A');
1700 }
1701
1702 row.find('.content-edit, .url-edit').hide();
1703 row.find('.content-view, .url-view').show();
1704 row.find('.save-button').hide();
1705 row.find('.edit-button').show();
1706 } else {
1707 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1708 }
1709 },
1710 error: function() {
1711 button.prop('disabled', false);
1712 button.text('Save');
1713 alert('An error occurred while saving.');
1714 }
1715 });
1716 });
1717
1718
1719 // Questions handling
1720 $('.mxchat-add-question').on('click', function () {
1721 const container = $('#mxchat-additional-questions-container');
1722 const questionCount = container.find('.mxchat-question-row').length + 4;
1723 const questionIndex = container.find('.mxchat-question-row').length;
1724
1725 const newQuestion = `
1726 <div class="mxchat-question-row">
1727 <input type="text"
1728 name="additional_popular_questions[]"
1729 placeholder="Enter Additional Popular Question ${questionCount}"
1730 class="regular-text mxchat-question-input"
1731 data-question-index="${questionIndex}" />
1732 <button type="button" class="button mxchat-remove-question"
1733 aria-label="Remove question">Remove</button>
1734 </div>
1735 `;
1736 container.append(newQuestion);
1737 });
1738
1739 $(document).on('click', '.mxchat-remove-question', function () {
1740 $(this).closest('.mxchat-question-row').remove();
1741 saveQuestions();
1742 });
1743
1744 $(document).on('change', '.mxchat-question-input', function() {
1745 saveQuestions();
1746 });
1747
1748 function saveQuestions() {
1749 const questions = [];
1750 $('.mxchat-question-input').each(function() {
1751 const value = $(this).val().trim();
1752 if (value) {
1753 questions.push(value);
1754 }
1755 });
1756
1757 const feedbackContainer = $('<div class="feedback-container"></div>');
1758 const spinner = $('<div class="saving-spinner"></div>');
1759 const successIcon = $('<div class="success-icon">✔</div>');
1760
1761 // Append feedback after the add button
1762 $('.mxchat-add-question').after(feedbackContainer);
1763 feedbackContainer.append(spinner);
1764
1765 // Save via AJAX
1766 $.ajax({
1767 url: mxchatAdmin.ajax_url,
1768 type: 'POST',
1769 data: {
1770 action: 'mxchat_save_setting',
1771 name: 'additional_popular_questions',
1772 value: JSON.stringify(questions),
1773 _ajax_nonce: mxchatAdmin.setting_nonce
1774 },
1775 success: function(response) {
1776 if (response.success) {
1777 spinner.fadeOut(200, function() {
1778 feedbackContainer.append(successIcon);
1779 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
1780 feedbackContainer.remove();
1781 });
1782 });
1783 } else {
1784 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
1785 feedbackContainer.remove();
1786 }
1787 },
1788 error: function() {
1789 alert('An error occurred while saving questions.');
1790 feedbackContainer.remove();
1791 }
1792 });
1793 }
1794
1795 // Live agent status handler
1796 const statusToggle = document.getElementById('live_agent_status');
1797 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
1798 if (statusToggle && statusText) {
1799 statusToggle.addEventListener('change', function() {
1800 // Update display text
1801 statusText.textContent = this.checked ? 'Online' : 'Offline';
1802
1803 // Send the correct on/off value to the server
1804 if (window.mxchatSaveSetting) {
1805 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
1806 }
1807 });
1808 }
1809
1810 // Function to adjust the textarea height to content
1811 function adjustTextareaHeight() {
1812 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
1813 this.style.height = this.scrollHeight + 'px'; // Expand to content height
1814 }
1815
1816 // Function to reset the textarea height to initial
1817 function resetTextareaHeight() {
1818 this.style.height = ''; // Remove inline height, reverting to CSS default
1819 }
1820
1821 // Target the specific textarea by ID
1822 var $textarea = $('#system_prompt_instructions');
1823
1824 // Bind events
1825 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
1826 .on('blur', resetTextareaHeight); // Reset on blur
1827 });
1828
1829
1830
1831 document.addEventListener('DOMContentLoaded', function() {
1832 // Check if we're on the correct page before initializing
1833 const modal = document.getElementById('mxchat-action-modal');
1834
1835 // Only initialize if the modal exists on this page
1836 if (modal) {
1837 //console.log('MXChat Action Modal JS Loaded');
1838
1839 // Initialize the action modal functionality
1840 initStepBasedActionModal();
1841 }
1842
1843 // Function to initialize the step-based action modal
1844 function initStepBasedActionModal() {
1845 // We already checked for modal existence above, so no need to check again
1846
1847 const actionStep1 = document.getElementById('mxchat-action-step-1');
1848 const actionStep2 = document.getElementById('mxchat-action-step-2');
1849 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
1850 const searchInput = document.getElementById('action-type-search');
1851 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
1852 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
1853 const actionForm = document.getElementById('mxchat-action-form');
1854 const callbackInput = document.getElementById('callback_function');
1855 const actionIdField = document.getElementById('edit_action_id');
1856 const labelField = document.getElementById('intent_label');
1857 const phrasesField = document.getElementById('action_phrases');
1858 const formActionType = document.getElementById('form_action_type');
1859 const nonceContainer = document.getElementById('action-nonce-container');
1860 const thresholdSlider = document.getElementById('similarity_threshold');
1861 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1862
1863 // Rest of your initialization code remains the same...
1864
1865 // Log the structure of one action card for debugging
1866 if (actionCards.length > 0) {
1867 //console.log('First action card data attributes:', actionCards[0].dataset);
1868 //console.log('First action card HTML:', actionCards[0].outerHTML);
1869 }
1870
1871 // Add click event listeners to category buttons
1872 categoryButtons.forEach(button => {
1873 button.addEventListener('click', function() {
1874 //console.log('Category button clicked:', this.dataset.category);
1875
1876 // Remove active class from all buttons
1877 categoryButtons.forEach(btn => btn.classList.remove('active'));
1878
1879 // Add active class to clicked button
1880 this.classList.add('active');
1881
1882 // Get selected category
1883 const category = this.dataset.category;
1884
1885 // Filter action cards
1886 filterActionCards(category, searchInput.value);
1887 });
1888 });
1889
1890 // Add search functionality
1891 if (searchInput) {
1892 searchInput.addEventListener('input', function() {
1893 // Get active category
1894 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
1895 //console.log('Search input changed, active category:', activeCategory);
1896
1897 // Filter action cards
1898 filterActionCards(activeCategory, this.value);
1899 });
1900 }
1901
1902 // Add click event listeners to action cards
1903 actionCards.forEach(card => {
1904 card.addEventListener('click', function() {
1905 // Get the action data
1906 const isPro = this.dataset.pro === 'true';
1907 const isInstalled = this.dataset.installed === 'true';
1908 const addonName = this.dataset.addon || '';
1909 const actionValue = this.dataset.value;
1910 const actionLabel = this.dataset.label;
1911 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
1912 const actionDescription = this.querySelector('p').textContent;
1913
1914 // Pro check using the proper detection method
1915 const proIsActivated = typeof mxchatAdmin !== 'undefined' &&
1916 (mxchatAdmin.is_activated === '1' ||
1917 mxchatAdmin.is_activated === 'true' ||
1918 mxchatAdmin.is_activated === true);
1919
1920 // Handle different states
1921 if (isPro && !proIsActivated) {
1922 // Pro feature but no Pro license
1923 showProFeatureNotice();
1924 return;
1925 }
1926
1927 if (addonName && !isInstalled) {
1928 // Add-on required but not installed
1929 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '') || addonName + ' Add-on';
1930 showAddonRequiredNotice(addonDisplayName);
1931 return;
1932 }
1933
1934 // If we get here, the action is available - proceed as normal
1935 callbackInput.value = actionValue;
1936
1937 // Update the selected action display in step 2
1938 document.getElementById('selected-action-title').textContent = actionLabel;
1939 document.getElementById('selected-action-description').textContent = actionDescription;
1940 document.getElementById('selected-action-icon').innerHTML =
1941 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1942
1943 // Set a default label based on the action type (user can change it)
1944 if (!labelField.value) {
1945 labelField.value = actionLabel;
1946 }
1947
1948 // Move to step 2
1949 actionStep1.classList.remove('active');
1950 actionStep2.classList.add('active');
1951
1952 // Update modal title
1953 });
1954 });
1955
1956 // Back button functionality
1957 if (backToStep1Btn) {
1958 backToStep1Btn.addEventListener('click', function() {
1959 //console.log('Back button clicked');
1960 actionStep2.classList.remove('active');
1961 actionStep1.classList.add('active');
1962 });
1963 }
1964
1965 // Function to filter action cards by category and search term
1966 function filterActionCards(category, searchTerm) {
1967 searchTerm = searchTerm.toLowerCase().trim();
1968 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
1969
1970 let visibleCount = 0;
1971
1972 // Show all cards initially with animation
1973 actionCards.forEach((card, index) => {
1974 // Reset animation
1975 card.style.animation = 'none';
1976 // Trigger reflow
1977 void card.offsetWidth;
1978
1979 // Determine if card should be visible based on category and search term
1980 const cardCategory = card.dataset.category || '';
1981 const matchesCategory = category === 'all' || cardCategory === category;
1982
1983 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
1984 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
1985 const matchesSearch = searchTerm === '' ||
1986 cardTitle.includes(searchTerm) ||
1987 cardDesc.includes(searchTerm);
1988
1989 // Show/hide card with animation
1990 if (matchesCategory && matchesSearch) {
1991 card.style.display = 'flex';
1992 // Staggered animation for cards
1993 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
1994 visibleCount++;
1995 } else {
1996 card.style.display = 'none';
1997 }
1998 });
1999
2000 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
2001 }
2002
2003 // Function to show notice for Pro features
2004 function showProFeatureNotice() {
2005 //console.log('Showing Pro feature notice');
2006 // Check if we already have a notification container
2007 let noticeContainer = document.querySelector('.mxchat-pro-notice');
2008
2009 if (!noticeContainer) {
2010 // Create the notice container
2011 noticeContainer = document.createElement('div');
2012 noticeContainer.className = 'mxchat-pro-notice';
2013
2014 // Create content
2015 noticeContainer.innerHTML = `
2016 <div class="mxchat-pro-notice-content">
2017 <h3>MxChat Pro Feature</h3>
2018 <p>This action is available in the Pro version only.</p>
2019 <div class="mxchat-pro-notice-buttons">
2020 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
2021 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
2022 </div>
2023 </div>
2024 `;
2025
2026 // Append to body
2027 document.body.appendChild(noticeContainer);
2028
2029 // Add close functionality
2030 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
2031 closeButton.addEventListener('click', function() {
2032 noticeContainer.classList.remove('active');
2033 setTimeout(() => {
2034 noticeContainer.remove();
2035 }, 300);
2036 });
2037
2038 // Click outside to close
2039 noticeContainer.addEventListener('click', function(e) {
2040 if (e.target === noticeContainer) {
2041 closeButton.click();
2042 }
2043 });
2044
2045 // Show with animation
2046 setTimeout(() => {
2047 noticeContainer.classList.add('active');
2048 }, 10);
2049 } else {
2050 // If it already exists, just make it visible again
2051 noticeContainer.classList.add('active');
2052 }
2053 }
2054
2055 // Function to show notice for add-on requirements
2056 function showAddonRequiredNotice(addonName) {
2057 //console.log(`Showing add-on notice for: ${addonName}`);
2058 // Check if we already have a notification container
2059 let noticeContainer = document.querySelector('.mxchat-addon-notice');
2060
2061 if (!noticeContainer) {
2062 // Create the notice container
2063 noticeContainer = document.createElement('div');
2064 noticeContainer.className = 'mxchat-addon-notice';
2065
2066 // Create content
2067 noticeContainer.innerHTML = `
2068 <div class="mxchat-addon-notice-content">
2069 <span class="mxchat-addon-notice-icon">🧩</span>
2070 <h3>Add-on Required</h3>
2071 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
2072 <div class="mxchat-addon-notice-buttons">
2073 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
2074 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
2075 </div>
2076 </div>
2077 `;
2078
2079 // Append to body
2080 document.body.appendChild(noticeContainer);
2081
2082 // Add close functionality
2083 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
2084 closeButton.addEventListener('click', function() {
2085 noticeContainer.classList.remove('active');
2086 setTimeout(() => {
2087 noticeContainer.remove();
2088 }, 300);
2089 });
2090
2091 // Click outside to close
2092 noticeContainer.addEventListener('click', function(e) {
2093 if (e.target === noticeContainer) {
2094 closeButton.click();
2095 }
2096 });
2097
2098 // Show with animation
2099 setTimeout(() => {
2100 noticeContainer.classList.add('active');
2101 }, 10);
2102 } else {
2103 // If it already exists, update the content
2104 const addonNameElement = noticeContainer.querySelector('p strong');
2105 if (addonNameElement) {
2106 addonNameElement.textContent = addonName;
2107 }
2108
2109 // Make it visible again
2110 noticeContainer.classList.add('active');
2111 }
2112 }
2113
2114 // Form submission handling
2115 if (actionForm) {
2116 actionForm.addEventListener('submit', function() {
2117 //console.log('Form submitted');
2118 document.getElementById('mxchat-action-loading').style.display = 'flex';
2119 this.querySelector('button[type="submit"]').disabled = true;
2120 });
2121 }
2122 }
2123
2124 // Setup add action buttons (only if we're on the correct page)
2125 if (modal) {
2126 // Update the modal open function to support the step-based flow
2127 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '', enabledBots = null) {
2128 //console.log('Modal opening, edit mode:', isEdit);
2129
2130 // Get form fields
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 callbackInput = document.getElementById('callback_function');
2136 const saveButton = document.getElementById('mxchat-save-action-btn');
2137 const nonceContainer = document.getElementById('action-nonce-container');
2138 const thresholdSlider = document.getElementById('similarity_threshold');
2139 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2140 const actionStep1 = document.getElementById('mxchat-action-step-1');
2141 const actionStep2 = document.getElementById('mxchat-action-step-2');
2142 const searchInput = document.getElementById('action-type-search');
2143
2144 // Set up modal for edit or create
2145 if (isEdit) {
2146 saveButton.textContent = 'Update Action';
2147 formActionType.value = 'mxchat_edit_intent';
2148 actionIdField.value = actionId;
2149 labelField.value = label;
2150 phrasesField.value = phrases;
2151 callbackInput.value = callbackFunction;
2152 thresholdSlider.value = threshold; // Set the current threshold value
2153 thresholdDisplay.textContent = threshold + '%'; // Update display
2154
2155 // NEW: Handle bot selection checkboxes for editing
2156 // First uncheck all bot checkboxes
2157 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2158 checkbox.checked = false;
2159 });
2160
2161 // Then check the ones that should be enabled
2162 if (enabledBots) {
2163 let botsArray;
2164 if (typeof enabledBots === 'string') {
2165 try {
2166 botsArray = JSON.parse(enabledBots);
2167 } catch (e) {
2168 console.warn('Failed to parse enabledBots:', enabledBots);
2169 botsArray = ['default']; // fallback
2170 }
2171 } else if (Array.isArray(enabledBots)) {
2172 botsArray = enabledBots;
2173 } else {
2174 botsArray = ['default']; // fallback
2175 }
2176
2177 botsArray.forEach(botId => {
2178 const checkbox = document.querySelector(`input[name="enabled_bots[]"][value="${botId}"]`);
2179 if (checkbox) {
2180 checkbox.checked = true;
2181 }
2182 });
2183 } else {
2184 // Fallback to default if no bot data
2185 const defaultCheckbox = document.querySelector('input[name="enabled_bots[]"][value="default"]');
2186 if (defaultCheckbox) {
2187 defaultCheckbox.checked = true;
2188 }
2189 }
2190
2191 // Update the nonce field for editing
2192 nonceContainer.innerHTML = ''; // Clear existing nonce
2193 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
2194 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
2195 }
2196
2197 // For editing, go directly to step 2 and update the selected action display
2198 actionStep1.classList.remove('active');
2199 actionStep2.classList.add('active');
2200
2201 // Find the matching action card to get its details
2202 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
2203 let foundCard = null;
2204
2205 actionCards.forEach(card => {
2206 if (card.dataset.value === callbackFunction) {
2207 foundCard = card;
2208 }
2209 });
2210
2211 if (foundCard) {
2212 //console.log('Found matching action card for:', callbackFunction);
2213 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
2214 const actionIconElement = foundCard.querySelector('.dashicons');
2215 const actionIcon = actionIconElement
2216 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
2217 : 'admin-generic';
2218 const actionDescription = foundCard.querySelector('p')?.textContent || '';
2219
2220 document.getElementById('selected-action-title').textContent = actionLabel;
2221 document.getElementById('selected-action-description').textContent = actionDescription;
2222 document.getElementById('selected-action-icon').innerHTML =
2223 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2224 } else {
2225 //console.log('No matching action card found for:', callbackFunction);
2226 // Fallback if we can't find the card
2227 document.getElementById('selected-action-title').textContent = label;
2228 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
2229 document.getElementById('selected-action-icon').innerHTML =
2230 `<span class="dashicons dashicons-admin-generic"></span>`;
2231 }
2232 } else {
2233 //console.log('Setting up create mode');
2234 saveButton.textContent = 'Save Action';
2235 formActionType.value = 'mxchat_add_intent';
2236 actionIdField.value = '';
2237 labelField.value = '';
2238 phrasesField.value = '';
2239 callbackInput.value = '';
2240 thresholdSlider.value = 85; // Default value for new actions
2241 thresholdDisplay.textContent = '85%'; // Default display
2242
2243 // For new actions, ensure default is checked and others are unchecked
2244 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2245 checkbox.checked = (checkbox.value === 'default');
2246 });
2247
2248 // Update the nonce field for adding
2249 nonceContainer.innerHTML = ''; // Clear existing nonce
2250 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
2251 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
2252 }
2253
2254 // For creating new, start at step 1
2255 actionStep1.classList.add('active');
2256 actionStep2.classList.remove('active');
2257 }
2258
2259 // Show modal with animation
2260 modal.style.display = 'flex';
2261 requestAnimationFrame(() => {
2262 modal.classList.add('active');
2263 });
2264
2265 // Set up close handlers
2266 const closeModal = () => {
2267 //console.log('Closing modal');
2268 modal.classList.remove('active');
2269 setTimeout(() => {
2270 modal.style.display = 'none';
2271 }, 300); // Match the CSS transition time
2272 };
2273
2274 // Close button handler
2275 const closeBtn = modal.querySelector('.mxchat-modal-close');
2276 if (closeBtn) {
2277 closeBtn.onclick = closeModal;
2278 }
2279
2280 // Cancel button handler
2281 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
2282 if (cancelBtns) {
2283 cancelBtns.forEach(btn => {
2284 btn.onclick = closeModal;
2285 });
2286 }
2287
2288 // Click outside modal to close
2289 modal.onclick = (e) => {
2290 if (e.target === modal) {
2291 closeModal();
2292 }
2293 };
2294
2295 // Escape key to close modal
2296 document.addEventListener('keydown', function(e) {
2297 if (e.key === 'Escape' && modal.classList.contains('active')) {
2298 closeModal();
2299 }
2300 }, { once: true });
2301
2302 // Focus appropriate field based on current step
2303 if (isEdit || actionStep2.classList.contains('active')) {
2304 if (labelField) labelField.focus();
2305 } else {
2306 if (searchInput) searchInput.focus();
2307 }
2308
2309 return closeModal; // Return close function for external use
2310 };
2311 // Setup add action buttons
2312 const addActionBtn = document.getElementById('mxchat-add-action-btn');
2313 if (addActionBtn) {
2314 //console.log('Add action button found');
2315 addActionBtn.onclick = () => window.mxchatOpenActionModal();
2316 }
2317
2318 const createFirstAction = document.getElementById('mxchat-create-first-action');
2319 if (createFirstAction) {
2320 //console.log('Create first action button found');
2321 createFirstAction.onclick = () => window.mxchatOpenActionModal();
2322 }
2323
2324 // Setup edit buttons
2325 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
2326 //console.log('Edit buttons found:', editButtons.length);
2327 editButtons.forEach(button => {
2328 button.onclick = () => {
2329 const actionId = button.dataset.actionId;
2330 const phrases = button.dataset.phrases;
2331 const label = button.dataset.label;
2332 const threshold = button.dataset.threshold || 85;
2333 const callbackFunction = button.dataset.callbackFunction;
2334 const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
2335
2336 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
2337 };
2338 });
2339 }
2340 });
2341
2342 jQuery(document).ready(function($) {
2343 // Toggle custom post types container
2344 $('#mxchat-custom-post-types-toggle').on('click', function(e) {
2345 e.preventDefault();
2346
2347 $('#mxchat-custom-post-types-container').slideToggle(300);
2348
2349 // Rotate the toggle icon
2350 const $icon = $(this).find('.mxchat-accordion-icon');
2351 if ($('#mxchat-custom-post-types-container').is(':visible')) {
2352 $icon.css('transform', 'rotate(180deg)');
2353 $(this).closest('.mxchat-settings-accordion').addClass('active');
2354 } else {
2355 $icon.css('transform', 'rotate(0deg)');
2356 $(this).closest('.mxchat-settings-accordion').removeClass('active');
2357 }
2358 });
2359
2360 // If there are any selections made, auto-expand the container
2361 function autoExpandIfNeeded() {
2362 // Check if any checkbox in the container is checked
2363 const hasCheckedItems = $('#mxchat-custom-post-types-container input[type="checkbox"]:checked').length > 0;
2364
2365 if (hasCheckedItems) {
2366 $('#mxchat-custom-post-types-container').show();
2367 $('#mxchat-custom-post-types-toggle .mxchat-accordion-icon').css('transform', 'rotate(180deg)');
2368 $('.mxchat-settings-accordion').addClass('active');
2369 }
2370 }
2371
2372 // Run on page load
2373 autoExpandIfNeeded();
2374 });
2375
2376 document.addEventListener('DOMContentLoaded', function() {
2377 var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
2378 var modal = document.getElementById('mxchatSampleModal');
2379 var modalClose = document.getElementById('mxchatModalClose');
2380 var closeBtn = document.getElementById('mxchatCloseBtn');
2381 var copyBtn = document.getElementById('mxchatCopyBtn');
2382 var instructionsContent = document.querySelector('.mxchat-instructions-content');
2383 var modalContent = document.querySelector('.mxchat-instructions-modal-content');
2384
2385 if (!viewSampleBtn || !modal) {
2386 return;
2387 }
2388
2389 // Open modal
2390 viewSampleBtn.addEventListener('click', function(e) {
2391 e.preventDefault();
2392 e.stopPropagation();
2393 modal.classList.add('mxchat-instructions-show');
2394 });
2395
2396 // Close modal function
2397 function closeModal(e) {
2398 if (e) {
2399 e.preventDefault();
2400 e.stopPropagation();
2401 }
2402 modal.classList.remove('mxchat-instructions-show');
2403 }
2404
2405 // Close modal events
2406 if (modalClose) {
2407 modalClose.addEventListener('click', function(e) {
2408 closeModal(e);
2409 });
2410 }
2411
2412 if (closeBtn) {
2413 closeBtn.addEventListener('click', function(e) {
2414 closeModal(e);
2415 });
2416 }
2417
2418 // Close on backdrop click ONLY (not on hover)
2419 modal.addEventListener('click', function(e) {
2420 // Only close if clicking directly on the overlay, not on child elements
2421 if (e.target === modal) {
2422 closeModal(e);
2423 }
2424 });
2425
2426 // Prevent modal content clicks from closing the modal
2427 if (modalContent) {
2428 modalContent.addEventListener('click', function(e) {
2429 e.stopPropagation();
2430 });
2431 }
2432
2433 // Close on escape key
2434 document.addEventListener('keydown', function(e) {
2435 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2436 closeModal();
2437 }
2438 });
2439
2440 // Copy functionality
2441 if (copyBtn && instructionsContent) {
2442 copyBtn.addEventListener('click', function(e) {
2443 e.preventDefault();
2444 e.stopPropagation();
2445
2446 var text = instructionsContent.textContent;
2447
2448 if (navigator.clipboard) {
2449 navigator.clipboard.writeText(text).then(function() {
2450 showCopySuccess();
2451 }).catch(function() {
2452 fallbackCopy(text);
2453 });
2454 } else {
2455 fallbackCopy(text);
2456 }
2457 });
2458 }
2459
2460 function fallbackCopy(text) {
2461 var textArea = document.createElement('textarea');
2462 textArea.value = text;
2463 textArea.style.position = 'fixed';
2464 textArea.style.left = '-999999px';
2465 textArea.style.top = '-999999px';
2466 document.body.appendChild(textArea);
2467 textArea.select();
2468 try {
2469 document.execCommand('copy');
2470 showCopySuccess();
2471 } catch (err) {
2472 console.error('Copy failed');
2473 }
2474 document.body.removeChild(textArea);
2475 }
2476
2477 function showCopySuccess() {
2478 var originalText = copyBtn.innerHTML;
2479 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!';
2480
2481 setTimeout(function() {
2482 copyBtn.innerHTML = originalText;
2483 }, 2000);
2484 }
2485 });
2486
2487 jQuery(document).ready(function($) {
2488 var emailDependentFields = [
2489 '#email_blocker_header_content',
2490 '#email_blocker_button_text',
2491 '#enable_name_field',
2492 '#name_field_placeholder'
2493 ];
2494
2495 // Add visual indicators
2496 emailDependentFields.forEach(function(fieldId) {
2497 var $row = $(fieldId).closest('tr');
2498 $row.addClass('email-dependent-field');
2499
2500 // Add an icon to show it's dependent
2501 var $label = $row.find('th label, th');
2502 $label.prepend('<span class="email-dependent-icon" style="color: #0073aa; margin-right: 5px;">↳</span>');
2503 });
2504
2505 // Hide initially with opacity for smoother transition
2506 emailDependentFields.forEach(function(fieldId) {
2507 $(fieldId).closest('tr').hide().css('opacity', '0');
2508 });
2509
2510 function toggleEmailFields() {
2511 var emailEnabled = $('#enable_email_block').is(':checked');
2512
2513 emailDependentFields.forEach(function(fieldId) {
2514 var $row = $(fieldId).closest('tr');
2515 if (emailEnabled) {
2516 $row.slideDown(400).animate({opacity: 1}, 200);
2517 } else {
2518 $row.animate({opacity: 0}, 200).slideUp(400);
2519 }
2520 });
2521 }
2522
2523 toggleEmailFields();
2524 $('#enable_email_block').on('change', toggleEmailFields);
2525 });
2526
2527
2528
2529 //Handle role restriction changes
2530 jQuery(document).ready(function($) {
2531 //Handle role restriction changes for both data sources
2532 $(document).on('change', '.mxchat-role-select', function() {
2533 const $select = $(this);
2534 const entryId = $select.data('entry-id');
2535 const dataSource = $select.data('data-source');
2536 const roleRestriction = $select.val();
2537 const nonce = $select.data('nonce');
2538
2539 // Visual feedback
2540 $select.prop('disabled', true).addClass('updating');
2541
2542 $.ajax({
2543 url: ajaxurl,
2544 type: 'POST',
2545 data: {
2546 action: 'mxchat_update_role_restriction',
2547 nonce: nonce,
2548 entry_id: entryId,
2549 data_source: dataSource, // NEW: Include data source
2550 role_restriction: roleRestriction
2551 },
2552 success: function(response) {
2553 if (response.success) {
2554 // Show success feedback
2555 $select.removeClass('updating').addClass('updated');
2556 setTimeout(() => {
2557 $select.removeClass('updated');
2558 }, 2000);
2559 } else {
2560 alert('Failed to update role restriction: ' + response.data);
2561 // Revert selection
2562 $select.val($select.data('original-value'));
2563 }
2564 },
2565 error: function() {
2566 alert('Error updating role restriction');
2567 // Revert selection
2568 $select.val($select.data('original-value'));
2569 },
2570 complete: function() {
2571 $select.prop('disabled', false);
2572 }
2573 });
2574
2575 // Store original value for potential revert
2576 $select.data('original-value', roleRestriction);
2577 });
2578
2579 // Store initial values
2580 $('.mxchat-role-select').each(function() {
2581 $(this).data('original-value', $(this).val());
2582 });
2583 });
2584
2585 // Bot Selector Handler
2586 jQuery(document).ready(function($) {
2587 var saveTimer;
2588
2589 // Function to update bot_id in forms
2590 function updateBotIdInForm(formSelector) {
2591 var botId = $('#mxchat-bot-selector').val();
2592 var form = $(formSelector);
2593
2594 if (form.length > 0) {
2595 // Remove existing bot_id hidden input
2596 form.find('input[name="bot_id"]').remove();
2597
2598 // Add new bot_id hidden input if not default
2599 if (botId && botId !== 'default') {
2600 form.append('<input type="hidden" name="bot_id" value="' + botId + '">');
2601 }
2602 }
2603 }
2604
2605 $('#mxchat-bot-selector').on('change', function() {
2606 var botId = $(this).val();
2607
2608 // Clear any existing timer
2609 clearTimeout(saveTimer);
2610
2611 // Update all forms with new bot_id when bot selection changes
2612 updateBotIdInForm('#mxchat-url-form');
2613 updateBotIdInForm('#mxchat-content-form');
2614
2615 // Save the selection via AJAX
2616 $.ajax({
2617 url: ajaxurl,
2618 type: 'POST',
2619 data: {
2620 action: 'mxchat_save_selected_bot',
2621 bot_id: botId,
2622 nonce: mxchatAdmin.setting_nonce
2623 },
2624 success: function(response) {
2625 if (response.success) {
2626 // Show saved indicator
2627 $('#mxchat-bot-save-status').fadeIn().delay(2000).fadeOut();
2628
2629 // Reload the page after a short delay to refresh content
2630 saveTimer = setTimeout(function() {
2631 var currentUrl = new URL(window.location.href);
2632 currentUrl.searchParams.set('bot_id', botId);
2633 currentUrl.searchParams.set('page', 'mxchat-prompts');
2634 window.location.href = currentUrl.toString();
2635 }, 500);
2636 }
2637 },
2638 error: function() {
2639 console.error('Failed to save bot selection');
2640 }
2641 });
2642 });
2643
2644 // Initialize forms with current bot_id when the page loads
2645 setTimeout(function() {
2646 updateBotIdInForm('#mxchat-url-form');
2647 updateBotIdInForm('#mxchat-content-form');
2648 }, 100);
2649 });
2650
2651 // API Key Status Indicator for Chat Models
2652 jQuery(document).ready(function($) {
2653 function updateChatModelAPIStatus(apiKeyStatuses) {
2654 var selectedModel = $('#model').val();
2655
2656 // Hide all status messages
2657 $('.mxchat-api-status').hide();
2658
2659 // Return early if no model is selected
2660 if (!selectedModel) {
2661 return;
2662 }
2663
2664 // Map models to providers
2665 var provider = null;
2666
2667 if (selectedModel.startsWith('gpt-')) {
2668 provider = 'openai';
2669 } else if (selectedModel.startsWith('claude-')) {
2670 provider = 'claude';
2671 } else if (selectedModel.startsWith('grok-')) {
2672 provider = 'xai';
2673 } else if (selectedModel.startsWith('deepseek-')) {
2674 provider = 'deepseek';
2675 } else if (selectedModel.startsWith('gemini-')) {
2676 provider = 'gemini';
2677 } else if (selectedModel === 'openrouter') {
2678 provider = 'openrouter';
2679 }
2680
2681 // If we have fresh API key data, update the messages
2682 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
2683 var $statusElement = $('.mxchat-api-status[data-provider="' + provider + '"]');
2684 var hasKey = apiKeyStatuses[provider];
2685
2686 if (hasKey) {
2687 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
2688 } else {
2689 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
2690 }
2691 }
2692
2693 // Show the appropriate status message
2694 if (provider) {
2695 $('.mxchat-api-status[data-provider="' + provider + '"]').show();
2696 }
2697 }
2698
2699 function updateEmbeddingModelAPIStatus(apiKeyStatuses) {
2700 var selectedModel = $('#embedding_model').val();
2701
2702 // Hide all status messages
2703 $('.mxchat-embedding-api-status').hide();
2704
2705 // Return early if no model is selected
2706 if (!selectedModel) {
2707 return;
2708 }
2709
2710 // Map models to providers
2711 var provider = null;
2712
2713 if (selectedModel.startsWith('text-embedding-')) {
2714 provider = 'openai';
2715 } else if (selectedModel.startsWith('voyage-')) {
2716 provider = 'voyage';
2717 } else if (selectedModel.startsWith('gemini-embedding-')) {
2718 provider = 'gemini';
2719 }
2720
2721 // If we have fresh API key data, update the messages
2722 if (apiKeyStatuses && provider && apiKeyStatuses[provider] !== undefined) {
2723 var $statusElement = $('.mxchat-embedding-api-status[data-provider="' + provider + '"]');
2724 var hasKey = apiKeyStatuses[provider];
2725
2726 if (hasKey) {
2727 $statusElement.html('<span style="color: #00a32a;">✓ API key for ' + getProviderName(provider) + ' detected</span>');
2728 } else {
2729 $statusElement.html('<span style="color: #d63638;">⚠ No API key for ' + getProviderName(provider) + ' detected. Please enter API key in API Keys tab.</span>');
2730 }
2731 }
2732
2733 // Show the appropriate status message
2734 if (provider) {
2735 $('.mxchat-embedding-api-status[data-provider="' + provider + '"]').show();
2736 }
2737 }
2738
2739 function getProviderName(provider) {
2740 var names = {
2741 'openai': 'OpenAI',
2742 'claude': 'Anthropic (Claude)',
2743 'xai': 'X.AI (Grok)',
2744 'deepseek': 'DeepSeek',
2745 'gemini': 'Google Gemini',
2746 'openrouter': 'OpenRouter',
2747 'voyage': 'Voyage AI'
2748 };
2749 return names[provider] || provider;
2750 }
2751
2752 function refreshAPIKeyStatus() {
2753 $.ajax({
2754 url: ajaxurl,
2755 type: 'POST',
2756 data: {
2757 action: 'mxchat_check_api_keys',
2758 nonce: mxchatAdmin.setting_nonce
2759 },
2760 success: function(response) {
2761 if (response.success && response.data) {
2762 updateChatModelAPIStatus(response.data);
2763 updateEmbeddingModelAPIStatus(response.data);
2764 }
2765 }
2766 });
2767 }
2768
2769 // Expose refresh function globally so auto-save can call it
2770 window.mxchatRefreshAPIKeyStatus = refreshAPIKeyStatus;
2771
2772 // Run on page load
2773 updateChatModelAPIStatus();
2774 updateEmbeddingModelAPIStatus();
2775
2776 // Check if we just saved settings (WordPress redirects with ?settings-updated=true)
2777 var urlParams = new URLSearchParams(window.location.search);
2778 if (urlParams.get('settings-updated') === 'true') {
2779 // Page was just reloaded after save, fetch fresh API key status
2780 refreshAPIKeyStatus();
2781 }
2782
2783 // Run when model changes
2784 $('#model').on('change', function() { updateChatModelAPIStatus(); });
2785 $('#embedding_model').on('change', function() { updateEmbeddingModelAPIStatus(); });
2786 });