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

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