PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.5.2
MxChat – AI Chatbot & Content Generation for WordPress v2.5.2
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / js / mxchat-admin.js

mxchat-admin.js in MxChat – AI Chatbot & Content Generation for WordPress 2.5.2, at js/mxchat-admin.js

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