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

2,536 lines 119.5 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'); // Add this line to ensure we have the element
1055
1056 // Check if OpenRouter was selected
1057 if (modelValue === 'openrouter') {
1058 // Load OpenRouter models instead of closing
1059 loadOpenRouterModels();
1060 } else {
1061 // Normal model selection
1062 $modelSelect.val(modelValue).trigger('change');
1063
1064 // Manually save the model via AJAX
1065 jQuery.ajax({
1066 url: mxchatAdmin.ajax_url,
1067 type: 'POST',
1068 data: {
1069 action: 'mxchat_save_setting',
1070 name: 'model',
1071 value: modelValue,
1072 _ajax_nonce: mxchatAdmin.setting_nonce
1073 },
1074 success: function(response) {
1075 if (response.success) {
1076 // Update button text after successful save
1077 const selectedModelText = $modelSelect.find('option:selected').text();
1078 $('#mxchat_model_selector_btn').text(selectedModelText);
1079 }
1080 }
1081 });
1082
1083 $('#mxchat_model_selector_modal').hide();
1084 }
1085 });
1086
1087 // Close modal when clicking outside
1088 $(window).on('click', function(event) {
1089 if ($(event.target).is('#mxchat_model_selector_modal')) {
1090 $('#mxchat_model_selector_modal').hide();
1091 }
1092 });
1093 }
1094
1095 function loadOpenRouterModels() {
1096 const apiKey = $('#openrouter_api_key').val(); // This line already re-checks the field
1097 const $modal = $('#mxchat_model_selector_modal');
1098 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1099
1100 if (!apiKey || apiKey.trim() === '') {
1101 // Show error message in modal
1102 $modalBody.html(`
1103 <div style="text-align: center; padding: 40px;">
1104 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1105 <h3>OpenRouter API Key Required</h3>
1106 <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>
1107 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1108 </div>
1109 `);
1110
1111 $('#mxchat_back_to_models').on('click', function(e) {
1112 e.preventDefault();
1113 // CHANGE THIS: Instead of reloading, restore the original modal content
1114 $modalBody.html(`
1115 <div class="mxchat-model-selector-search-container">
1116 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1117 </div>
1118 <div class="mxchat-model-selector-categories">
1119 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1120 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1121 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1122 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1123 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1124 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1125 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1126 </div>
1127 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1128 `);
1129
1130 // Re-populate the grid
1131 populateModelsGrid('', 'all');
1132
1133 // Re-bind event handlers
1134 rebindModalEventHandlers();
1135 });
1136 return;
1137 }
1138
1139 // Show loading state
1140 $modalBody.html(`
1141 <div style="text-align: center; padding: 60px 20px;">
1142 <div class="spinner is-active" style="float: none; margin: 0 auto 20px;"></div>
1143 <h3>Loading OpenRouter Models...</h3>
1144 <p>Fetching available models from OpenRouter</p>
1145 </div>
1146 `);
1147
1148 // Fetch models from OpenRouter
1149 jQuery.ajax({
1150 url: mxchatAdmin.ajax_url,
1151 type: 'POST',
1152 data: {
1153 action: 'mxchat_fetch_openrouter_models',
1154 api_key: apiKey,
1155 nonce: mxchatAdmin.fetch_openrouter_models_nonce
1156 },
1157 success: function(response) {
1158 if (response.success && response.data.models) {
1159 displayOpenRouterModels(response.data.models);
1160 } else {
1161 $modalBody.html(`
1162 <div style="text-align: center; padding: 40px;">
1163 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1164 <h3>Error Loading Models</h3>
1165 <p>${response.data.message || 'Failed to load models from OpenRouter'}</p>
1166 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1167 </div>
1168 `);
1169
1170 $('#mxchat_back_to_models').on('click', function(e) {
1171 e.preventDefault();
1172 // CHANGE THIS: Restore original content instead of reloading
1173 restoreOriginalModalContent();
1174 });
1175 }
1176 },
1177 error: function() {
1178 $modalBody.html(`
1179 <div style="text-align: center; padding: 40px;">
1180 <span class="dashicons dashicons-warning" style="font-size: 48px; color: #d63638; margin-bottom: 20px;"></span>
1181 <h3>Connection Error</h3>
1182 <p>Failed to connect to OpenRouter. Please check your API key and try again.</p>
1183 <button class="button button-primary" id="mxchat_back_to_models">Back to Models</button>
1184 </div>
1185 `);
1186
1187 $('#mxchat_back_to_models').on('click', function(e) {
1188 e.preventDefault();
1189 // CHANGE THIS: Restore original content instead of reloading
1190 restoreOriginalModalContent();
1191 });
1192 }
1193 });
1194 }
1195 function restoreOriginalModalContent() {
1196 const $modalBody = $('#mxchat_model_selector_modal').find('.mxchat-model-selector-modal-body');
1197
1198 $modalBody.html(`
1199 <div class="mxchat-model-selector-search-container">
1200 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
1201 </div>
1202 <div class="mxchat-model-selector-categories">
1203 <button class="mxchat-model-category-btn active" data-category="all">All</button>
1204 <button class="mxchat-model-category-btn" data-category="openrouter">OpenRouter</button>
1205 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
1206 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
1207 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
1208 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
1209 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
1210 </div>
1211 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
1212 `);
1213
1214 // Re-populate the grid
1215 window.populateModelsGrid('', 'all');
1216
1217 // Re-bind event handlers
1218 rebindModalEventHandlers();
1219 }
1220 function rebindModalEventHandlers() {
1221 const $modal = $('#mxchat_model_selector_modal');
1222
1223 // Re-bind category button clicks
1224 $('.mxchat-model-category-btn').off('click').on('click', function() {
1225 $('.mxchat-model-category-btn').removeClass('active');
1226 $(this).addClass('active');
1227 const category = $(this).data('category');
1228 const searchTerm = $('#mxchat_model_search_input').val();
1229 window.populateModelsGrid(searchTerm, category);
1230 });
1231
1232 // Re-bind search input
1233 $('#mxchat_model_search_input').off('input').on('input', function() {
1234 const searchTerm = $(this).val();
1235 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1236 populateModelsGrid(searchTerm, activeCategory);
1237 });
1238 }
1239
1240 function displayOpenRouterModels(models) {
1241 const $modal = $('#mxchat_model_selector_modal');
1242 const $modalBody = $modal.find('.mxchat-model-selector-modal-body');
1243 const currentSelected = $('#openrouter_selected_model').val();
1244
1245 // Build new modal content with search and models
1246 const newContent = `
1247 <div class="mxchat-model-selector-search-container">
1248 <input type="text" id="mxchat_openrouter_search" class="mxchat-model-search-input" placeholder="Search OpenRouter models...">
1249 <p style="margin: 10px 0; color: #666; font-size: 13px;">
1250 <strong>${models.length} models available</strong> ·
1251 <a href="#" id="mxchat_back_to_provider_select" style="color: #2271b1;">← Back to providers</a>
1252 </p>
1253 </div>
1254 <div class="mxchat-model-selector-grid" id="mxchat_openrouter_models_grid"></div>
1255 `;
1256
1257 $modalBody.html(newContent);
1258
1259 // Function to render models
1260 function renderOpenRouterModels(filterText = '') {
1261 const $grid = $('#mxchat_openrouter_models_grid');
1262 $grid.empty();
1263
1264 let filteredModels = models;
1265 if (filterText) {
1266 const lowerFilter = filterText.toLowerCase();
1267 filteredModels = models.filter(m =>
1268 m.id.toLowerCase().includes(lowerFilter) ||
1269 m.name.toLowerCase().includes(lowerFilter) ||
1270 (m.description && m.description.toLowerCase().includes(lowerFilter))
1271 );
1272 }
1273
1274 filteredModels.forEach(model => {
1275 const isSelected = currentSelected === model.id;
1276 const contextLength = model.context_length ? `${(model.context_length / 1000).toFixed(0)}K` : '';
1277 const promptPrice = model.pricing.prompt ? `$${(model.pricing.prompt * 1000000).toFixed(2)}/1M` : '';
1278
1279 const $card = jQuery(`
1280 <div class="mxchat-model-selector-card mxchat-openrouter-card ${isSelected ? 'mxchat-model-selected' : ''}" data-model-id="${model.id}">
1281 <div class="mxchat-model-selector-icon">
1282 ${getOpenRouterIcon(model.id)}
1283 </div>
1284 <div class="mxchat-model-selector-info">
1285 <h4 class="mxchat-model-selector-title">${model.name}</h4>
1286 <div style="font-size: 12px; color: #666; margin-top: 5px;">
1287 ${contextLength ? '<span style="margin-right: 12px;">📄 ' + contextLength + '</span>' : ''}
1288 ${promptPrice ? '<span>💰 ' + promptPrice + '</span>' : ''}
1289 </div>
1290 </div>
1291 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
1292 </div>
1293 `);
1294
1295 $grid.append($card);
1296 });
1297 }
1298
1299 // Helper to get icon
1300 function getOpenRouterIcon(modelId) {
1301 if (modelId.includes('gpt') || modelId.includes('openai')) {
1302 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>';
1303 } else if (modelId.includes('claude') || modelId.includes('anthropic')) {
1304 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>';
1305 } else if (modelId.includes('gemini') || modelId.includes('google')) {
1306 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>';
1307 }
1308 return '<span class="dashicons dashicons-cloud" style="font-size: 24px; color: #6750A4;"></span>';
1309 }
1310
1311 // Initial render
1312 renderOpenRouterModels();
1313
1314 // Search handler
1315 $('#mxchat_openrouter_search').on('input', function() {
1316 renderOpenRouterModels($(this).val());
1317 });
1318
1319 $('#mxchat_back_to_provider_select').on('click', function(e) {
1320 e.preventDefault();
1321 // Instead of location.reload(), restore original content
1322 restoreOriginalModalContent();
1323 });
1324
1325 // Model selection
1326 $(document).on('click', '.mxchat-openrouter-card', function() {
1327 const modelId = $(this).data('model-id');
1328 const modelName = models.find(m => m.id === modelId)?.name || modelId;
1329
1330 // First, save that we're using OpenRouter
1331 jQuery.ajax({
1332 url: mxchatAdmin.ajax_url,
1333 type: 'POST',
1334 data: {
1335 action: 'mxchat_save_setting',
1336 name: 'model',
1337 value: 'openrouter',
1338 _ajax_nonce: mxchatAdmin.setting_nonce
1339 },
1340 success: function() {
1341 // After model is set, save the model ID
1342 jQuery.ajax({
1343 url: mxchatAdmin.ajax_url,
1344 type: 'POST',
1345 data: {
1346 action: 'mxchat_save_setting',
1347 name: 'openrouter_selected_model',
1348 value: modelId,
1349 _ajax_nonce: mxchatAdmin.setting_nonce
1350 },
1351 success: function() {
1352 // Update DOM immediately
1353 $('#openrouter_selected_model').val(modelId);
1354
1355 // After model ID is saved, save the display name
1356 jQuery.ajax({
1357 url: mxchatAdmin.ajax_url,
1358 type: 'POST',
1359 data: {
1360 action: 'mxchat_save_setting',
1361 name: 'openrouter_selected_model_name',
1362 value: modelName,
1363 _ajax_nonce: mxchatAdmin.setting_nonce
1364 },
1365 success: function() {
1366 // Update DOM immediately
1367 $('#openrouter_selected_model_name').val(modelName);
1368
1369 // Update button text
1370 $('#mxchat_model_selector_btn').text('OpenRouter: ' + modelName);
1371 }
1372 });
1373 }
1374 });
1375 }
1376 });
1377
1378 // Close modal
1379 $modal.hide();
1380 });
1381
1382 }
1383
1384 // Embedding model selector - completely separate from chat model selector
1385 function setupMxChatEmbeddingModelSelector() {
1386 const $embeddingModelSelect = $('#embedding_model');
1387
1388 // Skip if the element doesn't exist on the page
1389 if ($embeddingModelSelect.length === 0) {
1390 return;
1391 }
1392
1393 const $embeddingModelSelectorButton = $('<button>', {
1394 type: 'button',
1395 id: 'mxchat_embedding_model_selector_btn',
1396 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
1397 text: 'Select Embedding Model'
1398 });
1399
1400 // Replace the select dropdown with a button
1401 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
1402
1403 // Update button text to show currently selected model
1404 function updateButtonText() {
1405 const selectedModel = $embeddingModelSelect.val();
1406 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
1407 $embeddingModelSelectorButton.text(selectedModelText);
1408 }
1409
1410 // Initialize button text
1411 updateButtonText();
1412
1413 // Create a unique ID for the modal to avoid conflicts
1414 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
1415
1416 // Create and append modal HTML with unique IDs
1417 const embeddingModelSelectorModal = `
1418 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
1419 <div class="mxchat-embedding-model-selector-modal-content">
1420 <div class="mxchat-embedding-model-selector-modal-header">
1421 <h3>Select Embedding Model</h3>
1422 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
1423 </div>
1424 <div class="mxchat-embedding-model-selector-modal-body">
1425 <div class="mxchat-embedding-model-selector-search-container">
1426 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
1427 </div>
1428 <div class="mxchat-embedding-model-selector-categories">
1429 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
1430 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
1431 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
1432 <button class="mxchat-embedding-model-category-btn" data-category="gemini">Google Gemini</button>
1433 </div>
1434 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
1435 </div>
1436 <div class="mxchat-embedding-model-selector-modal-footer">
1437 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
1438 </div>
1439 </div>
1440 </div>
1441 `;
1442
1443 // Use jQuery's append to ensure it doesn't clash with existing modals
1444 $('body').append(embeddingModelSelectorModal);
1445
1446 // Populate models grid
1447 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1448 const $grid = $('#mxchat_embedding_models_grid');
1449 $grid.empty();
1450
1451 // Define embedding models with descriptions and context lengths
1452 const models = {
1453 openai: [
1454 {
1455 value: 'text-embedding-3-small',
1456 label: 'TE3 Small',
1457 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1458 },
1459 {
1460 value: 'text-embedding-ada-002',
1461 label: 'Ada 2',
1462 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1463 },
1464 {
1465 value: 'text-embedding-3-large',
1466 label: 'TE3 Large',
1467 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1468 }
1469 ],
1470 voyage: [
1471 {
1472 value: 'voyage-3-large',
1473 label: 'Voyage-3 Large',
1474 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1475 }
1476 ],
1477 gemini: [
1478 {
1479 value: 'gemini-embedding-exp-03-07',
1480 label: 'Gemini Embedding',
1481 description: 'Experimental SOTA embeddings (1536 dimensions, 8K context)'
1482 }
1483 ]
1484 };
1485
1486 let allModels = [];
1487 Object.keys(models).forEach(key => {
1488 if (category === 'all' || category === key) {
1489 allModels = allModels.concat(models[key]);
1490 }
1491 });
1492
1493 // Filter by search term if present
1494 if (filter) {
1495 const lowerFilter = filter.toLowerCase();
1496 allModels = allModels.filter(model =>
1497 model.label.toLowerCase().includes(lowerFilter) ||
1498 model.description.toLowerCase().includes(lowerFilter)
1499 );
1500 }
1501
1502 // Create model cards
1503 allModels.forEach(model => {
1504 const isSelected = $embeddingModelSelect.val() === model.value;
1505 let providerClass = 'mxchat-embedding-model-provider-openai';
1506
1507 if (model.value.startsWith('voyage-')) {
1508 providerClass = 'mxchat-embedding-model-provider-voyage';
1509 } else if (model.value.startsWith('gemini-embedding-')) {
1510 providerClass = 'mxchat-embedding-model-provider-gemini';
1511 }
1512
1513 let iconHTML = '';
1514 if (model.value.startsWith('voyage-')) {
1515 iconHTML = '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>';
1516 } else if (model.value.startsWith('gemini-embedding-')) {
1517 iconHTML = '<span class="dashicons dashicons-google mxchat-embedding-model-icon-gemini"></span>';
1518 } else {
1519 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>';
1520 }
1521
1522 const $modelCard = $(`
1523 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1524 <div class="mxchat-embedding-model-selector-icon">
1525 ${iconHTML}
1526 </div>
1527 <div class="mxchat-embedding-model-selector-info">
1528 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1529 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1530 </div>
1531 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1532 </div>
1533 `);
1534
1535 $grid.append($modelCard);
1536 });
1537 }
1538
1539 // Event handlers - use namespaced events to avoid conflicts
1540 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1541 e.stopPropagation(); // Prevent event bubbling
1542 $('#' + embeddingModalId).show();
1543 populateEmbeddingModelsGrid('', 'all');
1544 });
1545
1546 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1547 e.stopPropagation(); // Prevent event bubbling
1548 $('#' + embeddingModalId).hide();
1549 });
1550
1551 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1552 e.stopPropagation(); // Prevent event bubbling
1553 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1554 $(this).addClass('active');
1555 const category = $(this).data('category');
1556 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1557 populateEmbeddingModelsGrid(searchTerm, category);
1558 });
1559
1560 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1561 const searchTerm = $(this).val();
1562 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1563 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1564 });
1565
1566 // Use a direct selector to avoid conflicts with other card elements
1567 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1568 e.stopPropagation(); // Prevent event bubbling
1569 const modelValue = $(this).data('value');
1570
1571 // Important: Only update this specific select element
1572 $embeddingModelSelect.val(modelValue);
1573
1574 // Manually trigger change only on this element
1575 const changeEvent = new Event('change', { bubbles: true });
1576 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1577
1578 // Update button text
1579 updateButtonText();
1580
1581 // Hide modal
1582 $('#' + embeddingModalId).hide();
1583 });
1584
1585 // Close modal when clicking outside - use namespaced events
1586 $(window).on('click.embeddingModelSelector', function(event) {
1587 if ($(event.target).is('#' + embeddingModalId)) {
1588 $('#' + embeddingModalId).hide();
1589 }
1590 });
1591 }
1592
1593 // Call this function after the DOM is fully loaded
1594 $(document).ready(function() {
1595 setupMxChatModelSelector();
1596 setupMxChatEmbeddingModelSelector();
1597 });
1598
1599 // Initialize API key visibility
1600 setupAPIKeyVisibility();
1601
1602 // Add Intent Form Submission
1603 $('#mxchat-add-intent-form').on('submit', function(event) {
1604 $('#mxchat-intent-loading').show();
1605 $('#mxchat-intent-loading-text').show();
1606 $(this).find('button[type="submit"]').hide();
1607 });
1608
1609 // Inline Edit Functionality
1610 $('.edit-button').on('click', function() {
1611 var row = $(this).closest('tr');
1612 row.find('.content-view, .url-view').hide();
1613 row.find('.content-edit, .url-edit').show();
1614 row.find('.edit-button').hide();
1615 row.find('.save-button').show();
1616 });
1617
1618 // Save button handler
1619 // Save button handler
1620 $('.save-button').on('click', function() {
1621 var button = $(this);
1622 var row = button.closest('tr');
1623 var id = button.data('id');
1624 var nonce = button.data('nonce'); // Get nonce from button data attribute
1625 var newContent = row.find('.content-edit').val();
1626 var newUrl = row.find('.url-edit').val();
1627
1628 //console.log('Nonce from button:', nonce); // Debug
1629
1630 button.prop('disabled', true);
1631 button.text('Saving...');
1632
1633 $.ajax({
1634 url: mxchatAdmin.ajax_url,
1635 type: 'POST',
1636 data: {
1637 action: 'mxchat_save_inline_prompt',
1638 id: id,
1639 article_content: newContent,
1640 article_url: newUrl,
1641 _ajax_nonce: nonce // Use nonce from button
1642 },
1643 success: function(response) {
1644 button.prop('disabled', false);
1645 button.text('Save');
1646
1647 if (response.success) {
1648 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1649 if (newUrl) {
1650 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank">' + newUrl + '</a>');
1651 } else {
1652 row.find('.url-view').html('N/A');
1653 }
1654
1655 row.find('.content-edit, .url-edit').hide();
1656 row.find('.content-view, .url-view').show();
1657 row.find('.save-button').hide();
1658 row.find('.edit-button').show();
1659 } else {
1660 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1661 }
1662 },
1663 error: function() {
1664 button.prop('disabled', false);
1665 button.text('Save');
1666 alert('An error occurred while saving.');
1667 }
1668 });
1669 });
1670
1671
1672 // Questions handling
1673 $('.mxchat-add-question').on('click', function () {
1674 const container = $('#mxchat-additional-questions-container');
1675 const questionCount = container.find('.mxchat-question-row').length + 4;
1676 const questionIndex = container.find('.mxchat-question-row').length;
1677
1678 const newQuestion = `
1679 <div class="mxchat-question-row">
1680 <input type="text"
1681 name="additional_popular_questions[]"
1682 placeholder="Enter Additional Popular Question ${questionCount}"
1683 class="regular-text mxchat-question-input"
1684 data-question-index="${questionIndex}" />
1685 <button type="button" class="button mxchat-remove-question"
1686 aria-label="Remove question">Remove</button>
1687 </div>
1688 `;
1689 container.append(newQuestion);
1690 });
1691
1692 $(document).on('click', '.mxchat-remove-question', function () {
1693 $(this).closest('.mxchat-question-row').remove();
1694 saveQuestions();
1695 });
1696
1697 $(document).on('change', '.mxchat-question-input', function() {
1698 saveQuestions();
1699 });
1700
1701 function saveQuestions() {
1702 const questions = [];
1703 $('.mxchat-question-input').each(function() {
1704 const value = $(this).val().trim();
1705 if (value) {
1706 questions.push(value);
1707 }
1708 });
1709
1710 const feedbackContainer = $('<div class="feedback-container"></div>');
1711 const spinner = $('<div class="saving-spinner"></div>');
1712 const successIcon = $('<div class="success-icon">✔</div>');
1713
1714 // Append feedback after the add button
1715 $('.mxchat-add-question').after(feedbackContainer);
1716 feedbackContainer.append(spinner);
1717
1718 // Save via AJAX
1719 $.ajax({
1720 url: mxchatAdmin.ajax_url,
1721 type: 'POST',
1722 data: {
1723 action: 'mxchat_save_setting',
1724 name: 'additional_popular_questions',
1725 value: JSON.stringify(questions),
1726 _ajax_nonce: mxchatAdmin.setting_nonce
1727 },
1728 success: function(response) {
1729 if (response.success) {
1730 spinner.fadeOut(200, function() {
1731 feedbackContainer.append(successIcon);
1732 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
1733 feedbackContainer.remove();
1734 });
1735 });
1736 } else {
1737 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
1738 feedbackContainer.remove();
1739 }
1740 },
1741 error: function() {
1742 alert('An error occurred while saving questions.');
1743 feedbackContainer.remove();
1744 }
1745 });
1746 }
1747
1748 // Live agent status handler
1749 const statusToggle = document.getElementById('live_agent_status');
1750 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
1751 if (statusToggle && statusText) {
1752 statusToggle.addEventListener('change', function() {
1753 // Update display text
1754 statusText.textContent = this.checked ? 'Online' : 'Offline';
1755
1756 // Send the correct on/off value to the server
1757 if (window.mxchatSaveSetting) {
1758 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
1759 }
1760 });
1761 }
1762
1763 // Function to adjust the textarea height to content
1764 function adjustTextareaHeight() {
1765 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
1766 this.style.height = this.scrollHeight + 'px'; // Expand to content height
1767 }
1768
1769 // Function to reset the textarea height to initial
1770 function resetTextareaHeight() {
1771 this.style.height = ''; // Remove inline height, reverting to CSS default
1772 }
1773
1774 // Target the specific textarea by ID
1775 var $textarea = $('#system_prompt_instructions');
1776
1777 // Bind events
1778 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
1779 .on('blur', resetTextareaHeight); // Reset on blur
1780 });
1781
1782
1783
1784 document.addEventListener('DOMContentLoaded', function() {
1785 // Check if we're on the correct page before initializing
1786 const modal = document.getElementById('mxchat-action-modal');
1787
1788 // Only initialize if the modal exists on this page
1789 if (modal) {
1790 //console.log('MXChat Action Modal JS Loaded');
1791
1792 // Initialize the action modal functionality
1793 initStepBasedActionModal();
1794 }
1795
1796 // Function to initialize the step-based action modal
1797 function initStepBasedActionModal() {
1798 // We already checked for modal existence above, so no need to check again
1799
1800 const actionStep1 = document.getElementById('mxchat-action-step-1');
1801 const actionStep2 = document.getElementById('mxchat-action-step-2');
1802 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
1803 const searchInput = document.getElementById('action-type-search');
1804 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
1805 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
1806 const actionForm = document.getElementById('mxchat-action-form');
1807 const callbackInput = document.getElementById('callback_function');
1808 const actionIdField = document.getElementById('edit_action_id');
1809 const labelField = document.getElementById('intent_label');
1810 const phrasesField = document.getElementById('action_phrases');
1811 const formActionType = document.getElementById('form_action_type');
1812 const nonceContainer = document.getElementById('action-nonce-container');
1813 const thresholdSlider = document.getElementById('similarity_threshold');
1814 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1815
1816 // Rest of your initialization code remains the same...
1817
1818 // Log the structure of one action card for debugging
1819 if (actionCards.length > 0) {
1820 //console.log('First action card data attributes:', actionCards[0].dataset);
1821 //console.log('First action card HTML:', actionCards[0].outerHTML);
1822 }
1823
1824 // Add click event listeners to category buttons
1825 categoryButtons.forEach(button => {
1826 button.addEventListener('click', function() {
1827 //console.log('Category button clicked:', this.dataset.category);
1828
1829 // Remove active class from all buttons
1830 categoryButtons.forEach(btn => btn.classList.remove('active'));
1831
1832 // Add active class to clicked button
1833 this.classList.add('active');
1834
1835 // Get selected category
1836 const category = this.dataset.category;
1837
1838 // Filter action cards
1839 filterActionCards(category, searchInput.value);
1840 });
1841 });
1842
1843 // Add search functionality
1844 if (searchInput) {
1845 searchInput.addEventListener('input', function() {
1846 // Get active category
1847 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
1848 //console.log('Search input changed, active category:', activeCategory);
1849
1850 // Filter action cards
1851 filterActionCards(activeCategory, this.value);
1852 });
1853 }
1854
1855 // Add click event listeners to action cards
1856 actionCards.forEach(card => {
1857 card.addEventListener('click', function() {
1858 // Get the action data
1859 const isPro = this.dataset.pro === 'true';
1860 const isInstalled = this.dataset.installed === 'true';
1861 const addonName = this.dataset.addon || '';
1862 const actionValue = this.dataset.value;
1863 const actionLabel = this.dataset.label;
1864 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
1865 const actionDescription = this.querySelector('p').textContent;
1866
1867 // Pro check using the proper detection method
1868 const proIsActivated = typeof mxchatAdmin !== 'undefined' &&
1869 (mxchatAdmin.is_activated === '1' ||
1870 mxchatAdmin.is_activated === 'true' ||
1871 mxchatAdmin.is_activated === true);
1872
1873 // Handle different states
1874 if (isPro && !proIsActivated) {
1875 // Pro feature but no Pro license
1876 showProFeatureNotice();
1877 return;
1878 }
1879
1880 if (addonName && !isInstalled) {
1881 // Add-on required but not installed
1882 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '') || addonName + ' Add-on';
1883 showAddonRequiredNotice(addonDisplayName);
1884 return;
1885 }
1886
1887 // If we get here, the action is available - proceed as normal
1888 callbackInput.value = actionValue;
1889
1890 // Update the selected action display in step 2
1891 document.getElementById('selected-action-title').textContent = actionLabel;
1892 document.getElementById('selected-action-description').textContent = actionDescription;
1893 document.getElementById('selected-action-icon').innerHTML =
1894 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1895
1896 // Set a default label based on the action type (user can change it)
1897 if (!labelField.value) {
1898 labelField.value = actionLabel;
1899 }
1900
1901 // Move to step 2
1902 actionStep1.classList.remove('active');
1903 actionStep2.classList.add('active');
1904
1905 // Update modal title
1906 });
1907 });
1908
1909 // Back button functionality
1910 if (backToStep1Btn) {
1911 backToStep1Btn.addEventListener('click', function() {
1912 //console.log('Back button clicked');
1913 actionStep2.classList.remove('active');
1914 actionStep1.classList.add('active');
1915 });
1916 }
1917
1918 // Function to filter action cards by category and search term
1919 function filterActionCards(category, searchTerm) {
1920 searchTerm = searchTerm.toLowerCase().trim();
1921 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
1922
1923 let visibleCount = 0;
1924
1925 // Show all cards initially with animation
1926 actionCards.forEach((card, index) => {
1927 // Reset animation
1928 card.style.animation = 'none';
1929 // Trigger reflow
1930 void card.offsetWidth;
1931
1932 // Determine if card should be visible based on category and search term
1933 const cardCategory = card.dataset.category || '';
1934 const matchesCategory = category === 'all' || cardCategory === category;
1935
1936 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
1937 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
1938 const matchesSearch = searchTerm === '' ||
1939 cardTitle.includes(searchTerm) ||
1940 cardDesc.includes(searchTerm);
1941
1942 // Show/hide card with animation
1943 if (matchesCategory && matchesSearch) {
1944 card.style.display = 'flex';
1945 // Staggered animation for cards
1946 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
1947 visibleCount++;
1948 } else {
1949 card.style.display = 'none';
1950 }
1951 });
1952
1953 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
1954 }
1955
1956 // Function to show notice for Pro features
1957 function showProFeatureNotice() {
1958 //console.log('Showing Pro feature notice');
1959 // Check if we already have a notification container
1960 let noticeContainer = document.querySelector('.mxchat-pro-notice');
1961
1962 if (!noticeContainer) {
1963 // Create the notice container
1964 noticeContainer = document.createElement('div');
1965 noticeContainer.className = 'mxchat-pro-notice';
1966
1967 // Create content
1968 noticeContainer.innerHTML = `
1969 <div class="mxchat-pro-notice-content">
1970 <h3>MxChat Pro Feature</h3>
1971 <p>This action is available in the Pro version only.</p>
1972 <div class="mxchat-pro-notice-buttons">
1973 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
1974 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
1975 </div>
1976 </div>
1977 `;
1978
1979 // Append to body
1980 document.body.appendChild(noticeContainer);
1981
1982 // Add close functionality
1983 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
1984 closeButton.addEventListener('click', function() {
1985 noticeContainer.classList.remove('active');
1986 setTimeout(() => {
1987 noticeContainer.remove();
1988 }, 300);
1989 });
1990
1991 // Click outside to close
1992 noticeContainer.addEventListener('click', function(e) {
1993 if (e.target === noticeContainer) {
1994 closeButton.click();
1995 }
1996 });
1997
1998 // Show with animation
1999 setTimeout(() => {
2000 noticeContainer.classList.add('active');
2001 }, 10);
2002 } else {
2003 // If it already exists, just make it visible again
2004 noticeContainer.classList.add('active');
2005 }
2006 }
2007
2008 // Function to show notice for add-on requirements
2009 function showAddonRequiredNotice(addonName) {
2010 //console.log(`Showing add-on notice for: ${addonName}`);
2011 // Check if we already have a notification container
2012 let noticeContainer = document.querySelector('.mxchat-addon-notice');
2013
2014 if (!noticeContainer) {
2015 // Create the notice container
2016 noticeContainer = document.createElement('div');
2017 noticeContainer.className = 'mxchat-addon-notice';
2018
2019 // Create content
2020 noticeContainer.innerHTML = `
2021 <div class="mxchat-addon-notice-content">
2022 <span class="mxchat-addon-notice-icon">🧩</span>
2023 <h3>Add-on Required</h3>
2024 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
2025 <div class="mxchat-addon-notice-buttons">
2026 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
2027 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
2028 </div>
2029 </div>
2030 `;
2031
2032 // Append to body
2033 document.body.appendChild(noticeContainer);
2034
2035 // Add close functionality
2036 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
2037 closeButton.addEventListener('click', function() {
2038 noticeContainer.classList.remove('active');
2039 setTimeout(() => {
2040 noticeContainer.remove();
2041 }, 300);
2042 });
2043
2044 // Click outside to close
2045 noticeContainer.addEventListener('click', function(e) {
2046 if (e.target === noticeContainer) {
2047 closeButton.click();
2048 }
2049 });
2050
2051 // Show with animation
2052 setTimeout(() => {
2053 noticeContainer.classList.add('active');
2054 }, 10);
2055 } else {
2056 // If it already exists, update the content
2057 const addonNameElement = noticeContainer.querySelector('p strong');
2058 if (addonNameElement) {
2059 addonNameElement.textContent = addonName;
2060 }
2061
2062 // Make it visible again
2063 noticeContainer.classList.add('active');
2064 }
2065 }
2066
2067 // Form submission handling
2068 if (actionForm) {
2069 actionForm.addEventListener('submit', function() {
2070 //console.log('Form submitted');
2071 document.getElementById('mxchat-action-loading').style.display = 'flex';
2072 this.querySelector('button[type="submit"]').disabled = true;
2073 });
2074 }
2075 }
2076
2077 // Setup add action buttons (only if we're on the correct page)
2078 if (modal) {
2079 // Update the modal open function to support the step-based flow
2080 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '', enabledBots = null) {
2081 //console.log('Modal opening, edit mode:', isEdit);
2082
2083 // Get form fields
2084 const actionIdField = document.getElementById('edit_action_id');
2085 const labelField = document.getElementById('intent_label');
2086 const phrasesField = document.getElementById('action_phrases');
2087 const formActionType = document.getElementById('form_action_type');
2088 const callbackInput = document.getElementById('callback_function');
2089 const saveButton = document.getElementById('mxchat-save-action-btn');
2090 const nonceContainer = document.getElementById('action-nonce-container');
2091 const thresholdSlider = document.getElementById('similarity_threshold');
2092 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
2093 const actionStep1 = document.getElementById('mxchat-action-step-1');
2094 const actionStep2 = document.getElementById('mxchat-action-step-2');
2095 const searchInput = document.getElementById('action-type-search');
2096
2097 // Set up modal for edit or create
2098 if (isEdit) {
2099 saveButton.textContent = 'Update Action';
2100 formActionType.value = 'mxchat_edit_intent';
2101 actionIdField.value = actionId;
2102 labelField.value = label;
2103 phrasesField.value = phrases;
2104 callbackInput.value = callbackFunction;
2105 thresholdSlider.value = threshold; // Set the current threshold value
2106 thresholdDisplay.textContent = threshold + '%'; // Update display
2107
2108 // NEW: Handle bot selection checkboxes for editing
2109 // First uncheck all bot checkboxes
2110 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2111 checkbox.checked = false;
2112 });
2113
2114 // Then check the ones that should be enabled
2115 if (enabledBots) {
2116 let botsArray;
2117 if (typeof enabledBots === 'string') {
2118 try {
2119 botsArray = JSON.parse(enabledBots);
2120 } catch (e) {
2121 console.warn('Failed to parse enabledBots:', enabledBots);
2122 botsArray = ['default']; // fallback
2123 }
2124 } else if (Array.isArray(enabledBots)) {
2125 botsArray = enabledBots;
2126 } else {
2127 botsArray = ['default']; // fallback
2128 }
2129
2130 botsArray.forEach(botId => {
2131 const checkbox = document.querySelector(`input[name="enabled_bots[]"][value="${botId}"]`);
2132 if (checkbox) {
2133 checkbox.checked = true;
2134 }
2135 });
2136 } else {
2137 // Fallback to default if no bot data
2138 const defaultCheckbox = document.querySelector('input[name="enabled_bots[]"][value="default"]');
2139 if (defaultCheckbox) {
2140 defaultCheckbox.checked = true;
2141 }
2142 }
2143
2144 // Update the nonce field for editing
2145 nonceContainer.innerHTML = ''; // Clear existing nonce
2146 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
2147 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
2148 }
2149
2150 // For editing, go directly to step 2 and update the selected action display
2151 actionStep1.classList.remove('active');
2152 actionStep2.classList.add('active');
2153
2154 // Find the matching action card to get its details
2155 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
2156 let foundCard = null;
2157
2158 actionCards.forEach(card => {
2159 if (card.dataset.value === callbackFunction) {
2160 foundCard = card;
2161 }
2162 });
2163
2164 if (foundCard) {
2165 //console.log('Found matching action card for:', callbackFunction);
2166 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
2167 const actionIconElement = foundCard.querySelector('.dashicons');
2168 const actionIcon = actionIconElement
2169 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
2170 : 'admin-generic';
2171 const actionDescription = foundCard.querySelector('p')?.textContent || '';
2172
2173 document.getElementById('selected-action-title').textContent = actionLabel;
2174 document.getElementById('selected-action-description').textContent = actionDescription;
2175 document.getElementById('selected-action-icon').innerHTML =
2176 `<span class="dashicons dashicons-${actionIcon}"></span>`;
2177 } else {
2178 //console.log('No matching action card found for:', callbackFunction);
2179 // Fallback if we can't find the card
2180 document.getElementById('selected-action-title').textContent = label;
2181 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
2182 document.getElementById('selected-action-icon').innerHTML =
2183 `<span class="dashicons dashicons-admin-generic"></span>`;
2184 }
2185 } else {
2186 //console.log('Setting up create mode');
2187 saveButton.textContent = 'Save Action';
2188 formActionType.value = 'mxchat_add_intent';
2189 actionIdField.value = '';
2190 labelField.value = '';
2191 phrasesField.value = '';
2192 callbackInput.value = '';
2193 thresholdSlider.value = 85; // Default value for new actions
2194 thresholdDisplay.textContent = '85%'; // Default display
2195
2196 // For new actions, ensure default is checked and others are unchecked
2197 document.querySelectorAll('input[name="enabled_bots[]"]').forEach(checkbox => {
2198 checkbox.checked = (checkbox.value === 'default');
2199 });
2200
2201 // Update the nonce field for adding
2202 nonceContainer.innerHTML = ''; // Clear existing nonce
2203 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
2204 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
2205 }
2206
2207 // For creating new, start at step 1
2208 actionStep1.classList.add('active');
2209 actionStep2.classList.remove('active');
2210 }
2211
2212 // Show modal with animation
2213 modal.style.display = 'flex';
2214 requestAnimationFrame(() => {
2215 modal.classList.add('active');
2216 });
2217
2218 // Set up close handlers
2219 const closeModal = () => {
2220 //console.log('Closing modal');
2221 modal.classList.remove('active');
2222 setTimeout(() => {
2223 modal.style.display = 'none';
2224 }, 300); // Match the CSS transition time
2225 };
2226
2227 // Close button handler
2228 const closeBtn = modal.querySelector('.mxchat-modal-close');
2229 if (closeBtn) {
2230 closeBtn.onclick = closeModal;
2231 }
2232
2233 // Cancel button handler
2234 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
2235 if (cancelBtns) {
2236 cancelBtns.forEach(btn => {
2237 btn.onclick = closeModal;
2238 });
2239 }
2240
2241 // Click outside modal to close
2242 modal.onclick = (e) => {
2243 if (e.target === modal) {
2244 closeModal();
2245 }
2246 };
2247
2248 // Escape key to close modal
2249 document.addEventListener('keydown', function(e) {
2250 if (e.key === 'Escape' && modal.classList.contains('active')) {
2251 closeModal();
2252 }
2253 }, { once: true });
2254
2255 // Focus appropriate field based on current step
2256 if (isEdit || actionStep2.classList.contains('active')) {
2257 if (labelField) labelField.focus();
2258 } else {
2259 if (searchInput) searchInput.focus();
2260 }
2261
2262 return closeModal; // Return close function for external use
2263 };
2264 // Setup add action buttons
2265 const addActionBtn = document.getElementById('mxchat-add-action-btn');
2266 if (addActionBtn) {
2267 //console.log('Add action button found');
2268 addActionBtn.onclick = () => window.mxchatOpenActionModal();
2269 }
2270
2271 const createFirstAction = document.getElementById('mxchat-create-first-action');
2272 if (createFirstAction) {
2273 //console.log('Create first action button found');
2274 createFirstAction.onclick = () => window.mxchatOpenActionModal();
2275 }
2276
2277 // Setup edit buttons
2278 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
2279 //console.log('Edit buttons found:', editButtons.length);
2280 editButtons.forEach(button => {
2281 button.onclick = () => {
2282 const actionId = button.dataset.actionId;
2283 const phrases = button.dataset.phrases;
2284 const label = button.dataset.label;
2285 const threshold = button.dataset.threshold || 85;
2286 const callbackFunction = button.dataset.callbackFunction;
2287 const enabledBots = button.dataset.enabledBots; // ADD THIS LINE
2288
2289 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction, enabledBots);
2290 };
2291 });
2292 }
2293 });
2294
2295 jQuery(document).ready(function($) {
2296 // Toggle custom post types container
2297 $('#mxchat-custom-post-types-toggle').on('click', function(e) {
2298 e.preventDefault();
2299
2300 $('#mxchat-custom-post-types-container').slideToggle(300);
2301
2302 // Rotate the toggle icon
2303 const $icon = $(this).find('.mxchat-accordion-icon');
2304 if ($('#mxchat-custom-post-types-container').is(':visible')) {
2305 $icon.css('transform', 'rotate(180deg)');
2306 $(this).closest('.mxchat-settings-accordion').addClass('active');
2307 } else {
2308 $icon.css('transform', 'rotate(0deg)');
2309 $(this).closest('.mxchat-settings-accordion').removeClass('active');
2310 }
2311 });
2312
2313 // If there are any selections made, auto-expand the container
2314 function autoExpandIfNeeded() {
2315 // Check if any checkbox in the container is checked
2316 const hasCheckedItems = $('#mxchat-custom-post-types-container input[type="checkbox"]:checked').length > 0;
2317
2318 if (hasCheckedItems) {
2319 $('#mxchat-custom-post-types-container').show();
2320 $('#mxchat-custom-post-types-toggle .mxchat-accordion-icon').css('transform', 'rotate(180deg)');
2321 $('.mxchat-settings-accordion').addClass('active');
2322 }
2323 }
2324
2325 // Run on page load
2326 autoExpandIfNeeded();
2327 });
2328
2329 document.addEventListener('DOMContentLoaded', function() {
2330 var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
2331 var modal = document.getElementById('mxchatSampleModal');
2332 var modalClose = document.getElementById('mxchatModalClose');
2333 var closeBtn = document.getElementById('mxchatCloseBtn');
2334 var copyBtn = document.getElementById('mxchatCopyBtn');
2335 var instructionsContent = document.querySelector('.mxchat-instructions-content');
2336 var modalContent = document.querySelector('.mxchat-instructions-modal-content');
2337
2338 if (!viewSampleBtn || !modal) {
2339 return;
2340 }
2341
2342 // Open modal
2343 viewSampleBtn.addEventListener('click', function(e) {
2344 e.preventDefault();
2345 e.stopPropagation();
2346 modal.classList.add('mxchat-instructions-show');
2347 });
2348
2349 // Close modal function
2350 function closeModal(e) {
2351 if (e) {
2352 e.preventDefault();
2353 e.stopPropagation();
2354 }
2355 modal.classList.remove('mxchat-instructions-show');
2356 }
2357
2358 // Close modal events
2359 if (modalClose) {
2360 modalClose.addEventListener('click', function(e) {
2361 closeModal(e);
2362 });
2363 }
2364
2365 if (closeBtn) {
2366 closeBtn.addEventListener('click', function(e) {
2367 closeModal(e);
2368 });
2369 }
2370
2371 // Close on backdrop click ONLY (not on hover)
2372 modal.addEventListener('click', function(e) {
2373 // Only close if clicking directly on the overlay, not on child elements
2374 if (e.target === modal) {
2375 closeModal(e);
2376 }
2377 });
2378
2379 // Prevent modal content clicks from closing the modal
2380 if (modalContent) {
2381 modalContent.addEventListener('click', function(e) {
2382 e.stopPropagation();
2383 });
2384 }
2385
2386 // Close on escape key
2387 document.addEventListener('keydown', function(e) {
2388 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2389 closeModal();
2390 }
2391 });
2392
2393 // Copy functionality
2394 if (copyBtn && instructionsContent) {
2395 copyBtn.addEventListener('click', function(e) {
2396 e.preventDefault();
2397 e.stopPropagation();
2398
2399 var text = instructionsContent.textContent;
2400
2401 if (navigator.clipboard) {
2402 navigator.clipboard.writeText(text).then(function() {
2403 showCopySuccess();
2404 }).catch(function() {
2405 fallbackCopy(text);
2406 });
2407 } else {
2408 fallbackCopy(text);
2409 }
2410 });
2411 }
2412
2413 function fallbackCopy(text) {
2414 var textArea = document.createElement('textarea');
2415 textArea.value = text;
2416 textArea.style.position = 'fixed';
2417 textArea.style.left = '-999999px';
2418 textArea.style.top = '-999999px';
2419 document.body.appendChild(textArea);
2420 textArea.select();
2421 try {
2422 document.execCommand('copy');
2423 showCopySuccess();
2424 } catch (err) {
2425 console.error('Copy failed');
2426 }
2427 document.body.removeChild(textArea);
2428 }
2429
2430 function showCopySuccess() {
2431 var originalText = copyBtn.innerHTML;
2432 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!';
2433
2434 setTimeout(function() {
2435 copyBtn.innerHTML = originalText;
2436 }, 2000);
2437 }
2438 });
2439
2440 jQuery(document).ready(function($) {
2441 var emailDependentFields = [
2442 '#email_blocker_header_content',
2443 '#email_blocker_button_text',
2444 '#enable_name_field',
2445 '#name_field_placeholder'
2446 ];
2447
2448 // Add visual indicators
2449 emailDependentFields.forEach(function(fieldId) {
2450 var $row = $(fieldId).closest('tr');
2451 $row.addClass('email-dependent-field');
2452
2453 // Add an icon to show it's dependent
2454 var $label = $row.find('th label, th');
2455 $label.prepend('<span class="email-dependent-icon" style="color: #0073aa; margin-right: 5px;">↳</span>');
2456 });
2457
2458 // Hide initially with opacity for smoother transition
2459 emailDependentFields.forEach(function(fieldId) {
2460 $(fieldId).closest('tr').hide().css('opacity', '0');
2461 });
2462
2463 function toggleEmailFields() {
2464 var emailEnabled = $('#enable_email_block').is(':checked');
2465
2466 emailDependentFields.forEach(function(fieldId) {
2467 var $row = $(fieldId).closest('tr');
2468 if (emailEnabled) {
2469 $row.slideDown(400).animate({opacity: 1}, 200);
2470 } else {
2471 $row.animate({opacity: 0}, 200).slideUp(400);
2472 }
2473 });
2474 }
2475
2476 toggleEmailFields();
2477 $('#enable_email_block').on('change', toggleEmailFields);
2478 });
2479
2480
2481
2482 //Handle role restriction changes
2483 jQuery(document).ready(function($) {
2484 //Handle role restriction changes for both data sources
2485 $(document).on('change', '.mxchat-role-select', function() {
2486 const $select = $(this);
2487 const entryId = $select.data('entry-id');
2488 const dataSource = $select.data('data-source');
2489 const roleRestriction = $select.val();
2490 const nonce = $select.data('nonce');
2491
2492 // Visual feedback
2493 $select.prop('disabled', true).addClass('updating');
2494
2495 $.ajax({
2496 url: ajaxurl,
2497 type: 'POST',
2498 data: {
2499 action: 'mxchat_update_role_restriction',
2500 nonce: nonce,
2501 entry_id: entryId,
2502 data_source: dataSource, // NEW: Include data source
2503 role_restriction: roleRestriction
2504 },
2505 success: function(response) {
2506 if (response.success) {
2507 // Show success feedback
2508 $select.removeClass('updating').addClass('updated');
2509 setTimeout(() => {
2510 $select.removeClass('updated');
2511 }, 2000);
2512 } else {
2513 alert('Failed to update role restriction: ' + response.data);
2514 // Revert selection
2515 $select.val($select.data('original-value'));
2516 }
2517 },
2518 error: function() {
2519 alert('Error updating role restriction');
2520 // Revert selection
2521 $select.val($select.data('original-value'));
2522 },
2523 complete: function() {
2524 $select.prop('disabled', false);
2525 }
2526 });
2527
2528 // Store original value for potential revert
2529 $select.data('original-value', roleRestriction);
2530 });
2531
2532 // Store initial values
2533 $('.mxchat-role-select').each(function() {
2534 $(this).data('original-value', $(this).val());
2535 });
2536 });