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

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

2,168 lines 99.7 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 // Set up edit button handlers for actions (new functionality)
209 document.querySelectorAll('.mxchat-action-card .mxchat-edit-button').forEach(button => {
210 button.onclick = () => {
211 const actionId = button.dataset.actionId;
212 const phrases = button.dataset.phrases;
213 const label = button.dataset.label;
214 const threshold = button.dataset.threshold || 85;
215 const callbackFunction = button.dataset.callbackFunction; // Add this data attribute
216 mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction);
217 };
218 });
219
220 // Set up add new action buttons (new functionality)
221 const addActionBtn = document.getElementById('mxchat-add-action-btn');
222 if (addActionBtn) {
223 addActionBtn.onclick = () => mxchatOpenActionModal();
224 }
225
226 const createFirstAction = document.getElementById('mxchat-create-first-action');
227 if (createFirstAction) {
228 createFirstAction.onclick = () => mxchatOpenActionModal();
229 }
230
231 // Setup category-specific new action buttons (new functionality)
232 document.querySelectorAll('.mxchat-new-action-button').forEach(button => {
233 button.onclick = () => {
234 const category = button.closest('.mxchat-new-action-card').dataset.category;
235 const closeModal = mxchatOpenActionModal();
236
237 // Pre-select the appropriate callback based on category
238 if (category) {
239 const callbackSelect = document.getElementById('callback_function');
240 if (callbackSelect) {
241 setTimeout(() => {
242 // Map categories to default callbacks
243 const categoryToCallback = {
244 'data_collection': 'mxchat_handle_form_collection',
245 'integrations': 'mxchat_handle_slack_message',
246 'custom_actions': 'mxchat_handle_custom_action',
247 'recommendations': 'mxchat_handle_product_recommendations'
248 // Add more mappings as needed
249 };
250
251 if (categoryToCallback[category]) {
252 callbackSelect.value = categoryToCallback[category];
253 }
254 }, 100);
255 }
256 }
257 };
258 });
259
260 // Handle action toggle switches (new functionality)
261 document.querySelectorAll('.mxchat-action-toggle').forEach(toggle => {
262 toggle.onchange = function() {
263 const actionId = this.dataset.actionId;
264 const isEnabled = this.checked;
265
266 // Show loading indicator
267 const loadingEl = document.getElementById('mxchat-action-loading');
268 if (loadingEl) loadingEl.style.display = 'flex';
269
270 // Send AJAX request to update status
271 fetch(ajaxurl, {
272 method: 'POST',
273 headers: {
274 'Content-Type': 'application/x-www-form-urlencoded',
275 },
276 body: new URLSearchParams({
277 action: 'mxchat_toggle_action',
278 intent_id: actionId,
279 enabled: isEnabled ? 1 : 0,
280 nonce: mxchatAdmin.toggle_action_nonce // Use the correct nonce
281 })
282 })
283 .then(response => response.json())
284 .then(data => {
285 if (!data.success) {
286 alert('Failed to update action status: ' + (data.data?.message || 'Unknown error'));
287 this.checked = !isEnabled; // Revert the toggle
288 }
289 })
290 .catch(error => {
291 //console.error('Error:', error);
292 alert('Server error. Please try again.');
293 this.checked = !isEnabled; // Revert the toggle
294 })
295 .finally(() => {
296 if (loadingEl) loadingEl.style.display = 'none';
297 });
298 };
299 });
300
301 // Handle threshold sliders in action cards (new functionality)
302 document.querySelectorAll('.mxchat-threshold-slider').forEach(slider => {
303 slider.oninput = function() {
304 const actionId = this.id.replace('intent_threshold_', '');
305 document.getElementById('threshold_output_' + actionId).textContent = this.value + '%';
306 };
307 });
308
309 // Handle threshold save buttons in action cards (new functionality)
310 document.querySelectorAll('.mxchat-threshold-save').forEach(button => {
311 button.onclick = function(e) {
312 e.preventDefault();
313 const form = this.closest('form');
314 const intentId = form.querySelector('input[name="intent_id"]').value;
315 const threshold = form.querySelector('input[name="intent_threshold"]').value;
316 const nonce = form.querySelector('input[name="_wpnonce"]').value;
317
318 // Show loading indicator
319 const loadingEl = document.getElementById('mxchat-action-loading');
320 if (loadingEl) loadingEl.style.display = 'flex';
321
322 // Send AJAX request
323 fetch(ajaxurl, {
324 method: 'POST',
325 headers: {
326 'Content-Type': 'application/x-www-form-urlencoded',
327 },
328 body: new URLSearchParams({
329 action: 'mxchat_update_intent_threshold',
330 intent_id: intentId,
331 intent_threshold: threshold,
332 _wpnonce: nonce
333 })
334 })
335 .then(response => response.json())
336 .then(data => {
337 if (data.success) {
338 // Visual feedback of success
339 const card = this.closest('.mxchat-action-card');
340 card.style.background = 'rgba(120, 115, 245, 0.1)';
341 setTimeout(() => {
342 card.style.background = 'white';
343 }, 300);
344 } else {
345 alert('Failed to update threshold: ' + (data.data?.message || 'Unknown error'));
346 }
347 })
348 .catch(error => {
349 //console.error('Error:', error);
350 alert('Server error. Please try again.');
351 })
352 .finally(() => {
353 if (loadingEl) loadingEl.style.display = 'none';
354 });
355 };
356 });
357 });
358
359 jQuery(document).ready(function($) {
360 // Ensure we have a debounce function (use lodash if available, otherwise use our implementation)
361 const useDebounce = (window._ && window._.debounce) ? window._.debounce : debounce;
362
363 // --- AJAX Auto-Save ---
364 let $autosaveSections = $('.mxchat-autosave-section');
365
366 // *** ADD THIS: Extend auto-save sections to include Pinecone settings ***
367 const $pineconeAutosaveSection = $('#mxchat-kb-tab-pinecone');
368 if ($pineconeAutosaveSection.length) {
369 $autosaveSections = $autosaveSections.add($pineconeAutosaveSection);
370 //console.log('Added Pinecone section to auto-save monitoring');
371 }
372
373 // Track whether fields have been modified by user
374 const userModifiedFields = new Set();
375
376 if ($autosaveSections.length) {
377 // Track user interactions with input fields to determine if changes are user-initiated
378 $autosaveSections.find('input, textarea, select').on('focus keydown paste', function() {
379 const fieldName = $(this).attr('name');
380 if (fieldName) {
381 userModifiedFields.add(fieldName);
382 }
383 });
384
385 // Handle real-time range slider value updates
386 $autosaveSections.find('input[type="range"]').on('input', function() {
387 const value = $(this).val();
388 $('#threshold_value').text(value);
389 });
390
391 // Handle all input changes (including range slider)
392 $autosaveSections.find('input, textarea, select').on('change', function() {
393 const $field = $(this);
394 const name = $field.attr('name');
395
396 // Skip saving for API key fields that haven't been interacted with and are empty
397 const isApiKeyField = name && (
398 name === 'loops_api_key' ||
399 name === 'api_key' ||
400 name === 'xai_api_key' ||
401 name === 'claude_api_key' ||
402 name === 'voyage_api_key' ||
403 name === 'gemini_api_key' ||
404 name === 'deepseek_api_key' ||
405 name.indexOf('_api_key') !== -1
406 );
407
408 // Skip processing if:
409 // 1. It's an API key field
410 // 2. The user hasn't interacted with it
411 // 3. The field is empty
412 if (isApiKeyField && !userModifiedFields.has(name) && (!$field.val() || $field.val().trim() === '')) {
413 //console.log('Skipping auto-save for untouched API key field:', name);
414 return;
415 }
416
417 let value;
418
419 // Handle different input types
420 if ($field.attr('type') === 'checkbox') {
421 // *** UPDATED: Handle Pinecone checkboxes differently ***
422 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
423 value = $field.is(':checked') ? '1' : '0';
424 } else {
425 value = $field.is(':checked') ? 'on' : 'off';
426 }
427 } else {
428 value = $field.val();
429 }
430
431 // Create feedback container
432 const feedbackContainer = $('<div class="feedback-container"></div>');
433 const spinner = $('<div class="saving-spinner"></div>');
434 const successIcon = $('<div class="success-icon">✔</div>');
435
436 // Position feedback container based on input type
437 if ($field.closest('.toggle-switch').length) {
438 $field.closest('td').append(feedbackContainer);
439 } else if ($field.closest('.mxchat-toggle-switch').length) {
440 $field.closest('.mxchat-toggle-container').append(feedbackContainer);
441 } else if ($field.closest('.slider-container').length) {
442 $field.closest('.slider-container').after(feedbackContainer);
443 } else {
444 $field.after(feedbackContainer);
445 }
446 feedbackContainer.append(spinner);
447
448 // Determine which AJAX action and nonce to use:
449 var ajaxAction, nonce;
450 // *** UPDATED: Add Pinecone fields to prompts action ***
451 if (name.indexOf('mxchat_prompts_options') !== -1 ||
452 name === 'mxchat_auto_sync_posts' ||
453 name === 'mxchat_auto_sync_pages' ||
454 name.indexOf('mxchat_auto_sync_') === 0 ||
455 name.indexOf('mxchat_pinecone_addon_options') !== -1) { // *** ADD THIS LINE ***
456 ajaxAction = 'mxchat_save_prompts_setting';
457 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
458 } else {
459 // Otherwise, use the existing AJAX action.
460 ajaxAction = 'mxchat_save_setting';
461 nonce = mxchatAdmin.setting_nonce;
462 }
463
464 // *** ADD THIS: Debug logging for Pinecone fields ***
465 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
466 //console.log('Saving Pinecone field:', name, '=', value);
467 }
468
469 // AJAX save request
470 $.ajax({
471 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
472 type: 'POST',
473 data: {
474 action: ajaxAction,
475 name: name,
476 value: value,
477 _ajax_nonce: nonce
478 },
479 success: function(response) {
480 if (response.success) {
481 spinner.fadeOut(200, function() {
482 feedbackContainer.append(successIcon);
483 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
484 feedbackContainer.remove();
485 });
486 });
487
488 // *** ADD THIS: Update Pinecone checkbox state after successful save ***
489 if (name && name.indexOf('mxchat_pinecone_addon_options[mxchat_use_pinecone]') !== -1) {
490 //console.log('Pinecone toggle saved successfully, value:', value);
491
492 // The checkbox state is already updated by the user interaction
493 // But let's make sure the UI state matches the saved value
494 var $checkbox = $('input[name="mxchat_pinecone_addon_options[mxchat_use_pinecone]"]');
495 var settingsDiv = $('.mxchat-pinecone-settings');
496
497 // Double-check the UI state matches what was saved
498 if (value === '1' && !$checkbox.is(':checked')) {
499 $checkbox.prop('checked', true);
500 settingsDiv.slideDown(300);
501 } else if (value === '0' && $checkbox.is(':checked')) {
502 $checkbox.prop('checked', false);
503 settingsDiv.slideUp(300);
504 }
505
506 //console.log('Pinecone UI state synchronized');
507
508 // Check if Knowledge Import tab is currently active
509 if ($('.mxchat-kb-tab-button[data-tab="import"]').hasClass('active')) {
510 // Show a notice that we need to refresh
511 var $knowledgeCard = $('#mxchat-kb-tab-import .mxchat-card').eq(1);
512 if ($knowledgeCard.length > 0) {
513 // Add a refresh notice at the top of the knowledge base card
514 var refreshNotice = $('<div class="notice notice-warning" style="margin: 15px 0; padding: 10px 15px;">' +
515 '<p style="margin: 0;">' +
516 '<span class="dashicons dashicons-info" style="color: #f0ad4e; margin-right: 5px;"></span>' +
517 'Database settings have changed. ' +
518 '<a href="#" onclick="location.reload(); return false;" style="font-weight: bold;">Click here to refresh</a> to see the updated knowledge base.' +
519 '</p></div>');
520
521 $knowledgeCard.prepend(refreshNotice);
522 }
523 } else {
524 // If not on import tab, set a flag to refresh when they go there
525 sessionStorage.setItem('mxchat_pinecone_changed', 'true');
526 }
527 }
528
529 // *** ADD THIS: Debug logging for successful saves ***
530 if (name && name.indexOf('mxchat_pinecone_addon_options') !== -1) {
531 //console.log('Pinecone field saved successfully:', name, '=', value);
532 }
533
534 // Check if the response contains a "no changes" message and log it
535 if (response.data && response.data.message === 'No changes detected') {
536 //console.log('No changes detected for field:', name);
537 }
538 } else {
539 // Only show alert for actual errors, not for "no changes"
540 let errorMessage = response.data?.message || 'Unknown error';
541
542 // Don't display an alert for "no changes" message
543 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
544 alert('Error saving: ' + errorMessage);
545 } else {
546 // Still provide visual feedback that no changes were needed
547 spinner.fadeOut(200, function() {
548 feedbackContainer.append(successIcon);
549 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
550 feedbackContainer.remove();
551 });
552 });
553 //console.log('No changes detected for field:', name);
554 return;
555 }
556
557 // Only revert checkbox state if it was an actual error
558 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
559 if ($field.attr('type') === 'checkbox') {
560 $field.prop('checked', !$field.is(':checked'));
561 }
562 }
563
564 // Always clean up the feedback container
565 feedbackContainer.remove();
566 }
567 },
568 error: function(xhr, textStatus, error) {
569 //console.error('AJAX Error:', textStatus, error);
570 alert('An error occurred while saving. Please try again.');
571
572 // Revert checkbox state on error
573 if ($field.attr('type') === 'checkbox') {
574 $field.prop('checked', !$field.is(':checked'));
575 }
576
577 feedbackContainer.remove();
578 }
579 });
580 });
581
582 // Initialize color pickers with debouncing
583 $autosaveSections.find('.my-color-field').each(function() {
584 const $colorField = $(this);
585
586 $(this).wpColorPicker({
587 change: useDebounce(function(event, ui) {
588 // Safety check - ensure we have a valid field and value
589 if (!$colorField || !$colorField.val()) {
590 //console.warn('Color picker not ready');
591 return;
592 }
593
594 const name = $colorField.attr('name');
595 const value = $colorField.val();
596
597 if (!name || !value) {
598 //console.warn('Missing required color picker values');
599 return;
600 }
601
602 // Create feedback container
603 const feedbackContainer = $('<div class="feedback-container"></div>');
604 const spinner = $('<div class="saving-spinner"></div>');
605 const successIcon = $('<div class="success-icon">✔</div>');
606
607 // Position feedback container
608 $colorField.closest('.wp-picker-container').after(feedbackContainer);
609 feedbackContainer.append(spinner);
610
611 // Determine which AJAX action and nonce to use:
612 var ajaxAction, nonce;
613 // Use the new AJAX action for submenu fields:
614 if (name.indexOf('mxchat_prompts_options') !== -1 ||
615 name === 'mxchat_auto_sync_posts' ||
616 name === 'mxchat_auto_sync_pages' ||
617 name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields
618 ajaxAction = 'mxchat_save_prompts_setting';
619 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
620 } else {
621 // Otherwise, use the existing AJAX action.
622 ajaxAction = 'mxchat_save_setting';
623 nonce = mxchatAdmin.setting_nonce;
624 }
625 // AJAX save request
626 $.ajax({
627 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
628 type: 'POST',
629 data: {
630 action: ajaxAction,
631 name: name,
632 value: value,
633 _ajax_nonce: nonce
634 },
635 success: function(response) {
636 if (response.success) {
637 spinner.fadeOut(200, function() {
638 feedbackContainer.append(successIcon);
639 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
640 feedbackContainer.remove();
641 });
642 });
643 } else {
644 alert('Error saving: ' + (response.data?.message || 'Unknown error'));
645 feedbackContainer.remove();
646 }
647 },
648 error: function() {
649 alert('An error occurred while saving.');
650 feedbackContainer.remove();
651 }
652 });
653 }, 500)
654 });
655 });
656
657 // Reinitialize color pickers when switching tabs
658 $('.mxchat-tab-button').on('click.mxchat', function() {
659 setTimeout(function() {
660 $('.my-color-field:visible').wpColorPicker('close');
661 }, 100);
662 });
663 }
664
665 // Initialize tabs system
666 function initTabs() {
667 // Remove any existing handlers first
668 $('.mxchat-tab-button').off('click.mxchat');
669
670 // Add new click handlers
671 $('.mxchat-tab-button').on('click.mxchat', function(e) {
672 e.preventDefault();
673 e.stopPropagation();
674
675 var $this = $(this);
676
677 // Get tab ID from data-tab attribute
678 var tabId = $this.data('tab') || 'chatbot';
679
680 // Safety check for empty tabId
681 if (!tabId) {
682 //console.warn('No tab identifier found');
683 return;
684 }
685
686 // Update tab buttons
687 $('.mxchat-tab-button').removeClass('active');
688 $this.addClass('active');
689
690 // Update content areas - with safety check
691 $('.mxchat-tab-content').removeClass('active');
692 var $targetTab = $('#' + tabId);
693 if ($targetTab.length) {
694 $targetTab.addClass('active');
695 // Removed localStorage saving functionality
696 } else {
697 //console.warn('Tab content #' + tabId + ' not found');
698 }
699 });
700 }
701
702 // Initialize tabs and handle events
703 initTabs();
704 $(document).on('widget-added widget-updated postbox-toggled', initTabs);
705
706 // Always activate the first tab (Chatbot)
707 $('.mxchat-tab-button').first().trigger('click.mxchat');
708
709 // Attach edit modal event handler
710 $(document).on('click', '.mxchat-edit-button', function() {
711 const intentId = $(this).data('intent-id');
712 const phrases = $(this).data('phrases');
713 mxchatOpenEditModal(intentId, phrases);
714 });
715
716 // Toggle visibility handlers
717 function toggleVisibility(selector) {
718 $(selector).on('click', function() {
719 var inputField = $(this).prev('input');
720 if (inputField.attr('type') === 'password') {
721 inputField.attr('type', 'text');
722 $(this).text('Hide');
723 } else {
724 inputField.attr('type', 'password');
725 $(this).text('Show');
726 }
727 });
728 }
729
730 // Initialize all toggle visibility buttons
731 [
732 '#toggleApiKeyVisibility',
733 '#toggleWooCommerceSecretVisibility',
734 '#toggleVoyageAPIKeyVisibility',
735 '#toggleLoopsApiKeyVisibility',
736 '#toggleXaiApiKeyVisibility',
737 '#toggleClaudeApiKeyVisibility',
738 '#toggleBraveApiKeyVisibility',
739 '#toggleWebhookUrlVisibility',
740 '#toggleSecretKeyVisibility',
741 '#toggleBotTokenVisibility',
742 '#toggleDeepSeekApiKeyVisibility',
743 '#toggleGeminiApiKeyVisibility' // Added Gemini toggle
744 ].forEach(toggleVisibility);
745
746 // Handle API key visibility based on model selection
747 function setupAPIKeyVisibility() {
748 // Cache the selectors
749 const $chatModelSelect = $('#model');
750 const $embeddingModelSelect = $('#embedding_model');
751
752 // First, locate and mark the API key rows
753 setupAPIKeyRows();
754
755 // Initial setup based on current selections
756 updateApiKeyVisibility();
757
758 // Listen for changes to the model selectors
759 $chatModelSelect.on('change', updateApiKeyVisibility);
760 $embeddingModelSelect.on('change', updateApiKeyVisibility);
761
762 /**
763 * Locate and mark rows that contain API key fields
764 */
765 function setupAPIKeyRows() {
766 // Find key rows by their field IDs
767 const providerMap = {
768 'api_key': 'openai',
769 'xai_api_key': 'xai',
770 'claude_api_key': 'claude',
771 'deepseek_api_key': 'deepseek',
772 'voyage_api_key': 'voyage',
773 'gemini_api_key': 'gemini' // Added Gemini API key mapping
774 };
775
776 $.each(providerMap, function(fieldId, provider) {
777 const $field = $('#' + fieldId);
778 if ($field.length) {
779 const $row = $field.closest('tr');
780 $row.addClass('mxchat-setting-row');
781 $row.attr('data-provider', provider);
782 }
783 });
784 }
785
786 /**
787 * Updates the visibility of API key fields based on current model selections
788 */
789 function updateApiKeyVisibility() {
790 const chatModel = $chatModelSelect.val();
791 const embeddingModel = $embeddingModelSelect.val();
792
793 // Determine which providers are needed
794 const isOpenAIChat = chatModel && chatModel.startsWith('gpt-');
795 const isXAI = chatModel && chatModel.startsWith('grok-');
796 const isClaude = chatModel && chatModel.startsWith('claude-');
797 const isDeepSeek = chatModel && chatModel.startsWith('deepseek-');
798 const isGemini = chatModel && chatModel.startsWith('gemini-'); // Added Gemini detection
799
800 const isOpenAIEmbedding = embeddingModel && embeddingModel.startsWith('text-embedding-');
801 const isVoyage = embeddingModel && embeddingModel.startsWith('voyage-');
802 const isGeminiEmbedding = embeddingModel && embeddingModel.startsWith('gemini-embedding-');
803
804 // Update API key visibility for each provider
805 updateWrapperVisibility('openai', isOpenAIChat || isOpenAIEmbedding);
806 updateWrapperVisibility('xai', isXAI);
807 updateWrapperVisibility('claude', isClaude);
808 updateWrapperVisibility('deepseek', isDeepSeek);
809 updateWrapperVisibility('voyage', isVoyage);
810 updateWrapperVisibility('gemini', isGemini || isGeminiEmbedding); // Updated Gemini visibility for both chat and embedding
811
812 // Update provider-specific notices for OpenAI
813 if (isOpenAIChat && isOpenAIEmbedding) {
814 $('div[data-provider="openai"] .api-key-notice').text(
815 'Required for your selected chat model and embedding model. Important: You must add credits before use.'
816 );
817 } else if (isOpenAIChat) {
818 $('div[data-provider="openai"] .api-key-notice').text(
819 'Required for your selected chat model. Important: You must add credits before use.'
820 );
821 } else if (isOpenAIEmbedding) {
822 $('div[data-provider="openai"] .api-key-notice').text(
823 'Required for your selected embedding model. Important: You must add credits before use.'
824 );
825 }
826
827 // Update provider-specific notices for Gemini
828 if (isGemini && isGeminiEmbedding) {
829 $('div[data-provider="gemini"] .api-key-notice').text(
830 'Required for your selected chat model and embedding model.'
831 );
832 } else if (isGemini) {
833 $('div[data-provider="gemini"] .api-key-notice').text(
834 'Required for your selected chat model.'
835 );
836 } else if (isGeminiEmbedding) {
837 $('div[data-provider="gemini"] .api-key-notice').text(
838 'Required for your selected embedding model.'
839 );
840 }
841 }
842
843 /**
844 * Updates visibility of a specific provider's API key wrapper
845 */
846 function updateWrapperVisibility(provider, isVisible) {
847 const $row = $('tr.mxchat-setting-row[data-provider="' + provider + '"]');
848
849 if (!$row.length) {
850 //console.warn('API key row not found for provider: ' + provider);
851 return;
852 }
853
854 if (isVisible) {
855 $row.show();
856 if (!$row.hasClass('highlighted')) {
857 $row.addClass('highlighted');
858 setTimeout(() => {
859 $row.removeClass('highlighted');
860 }, 1500);
861 }
862 } else {
863 $row.hide();
864 }
865 }
866 }
867
868 // Add this to your JavaScript file
869 function setupMxChatModelSelector() {
870 const $modelSelect = $('#model');
871 const $modelSelectorButton = $('<button>', {
872 type: 'button',
873 id: 'mxchat_model_selector_btn',
874 class: 'button-primary mxchat-model-selector-btn',
875 text: 'Select AI Model'
876 });
877
878 // Replace the select dropdown with a button
879 $modelSelect.hide().after($modelSelectorButton);
880
881 // Update button text to show currently selected model
882 function updateButtonText() {
883 const selectedModel = $modelSelect.val();
884 const selectedModelText = $modelSelect.find('option:selected').text();
885 $modelSelectorButton.text(selectedModelText);
886 }
887
888 // Initialize button text
889 updateButtonText();
890
891 // Create and append modal HTML
892 const modelSelectorModal = `
893 <div id="mxchat_model_selector_modal" class="mxchat-model-selector-modal">
894 <div class="mxchat-model-selector-modal-content">
895 <div class="mxchat-model-selector-modal-header">
896 <h3>Select AI Model</h3>
897 <span class="mxchat-model-selector-modal-close">&times;</span>
898 </div>
899 <div class="mxchat-model-selector-modal-body">
900 <div class="mxchat-model-selector-search-container">
901 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
902 </div>
903 <div class="mxchat-model-selector-categories">
904 <button class="mxchat-model-category-btn active" data-category="all">All</button>
905 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
906 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
907 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
908 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
909 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
910 </div>
911 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
912 </div>
913 <div class="mxchat-model-selector-modal-footer">
914 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
915 </div>
916 </div>
917 </div>
918 `;
919
920 $('body').append(modelSelectorModal);
921
922 // Populate models grid
923 // Populate models grid
924 function populateModelsGrid(filter = '', category = 'all') {
925 const $grid = $('#mxchat_models_grid');
926 $grid.empty();
927
928 const models = {
929 gemini: [
930 { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Next-Gen features, speed & multimodal generation' },
931 { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Cost-efficient with low latency' },
932 { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Complex reasoning tasks requiring more intelligence' },
933 { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Fast and versatile performance' },
934 ],
935 openai: [
936 // NEW: GPT-5 family
937 { value: 'gpt-5', label: 'GPT-5', description: 'Flagship for coding, reasoning, and agentic tasks across domains' },
938 { value: 'gpt-5-mini', label: 'GPT-5 Mini', description: 'Faster, more cost-efficient for well-defined tasks and precise prompts' },
939 { value: 'gpt-5-nano', label: 'GPT-5 Nano', description: 'Fastest and cheapest; ideal for summarization and classification' },
940
941 // Existing OpenAI models
942 { value: 'gpt-4.1-2025-04-14', label: 'GPT-4.1', description: 'Flagship model for complex tasks' },
943 { value: 'gpt-4o', label: 'GPT-4o', description: 'Recommended for most use cases' },
944 { value: 'gpt-4o-mini', label: 'GPT-4o Mini', description: 'Fast and lightweight' },
945 { value: 'gpt-4-turbo', label: 'GPT-4 Turbo', description: 'High-performance model' },
946 { value: 'gpt-4', label: 'GPT-4', description: 'High intelligence model' },
947 { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', description: 'Affordable and fast' },
948 ],
949 claude: [
950 { value: 'claude-opus-4-20250514', label: 'Claude 4 Opus', description: 'Most capable Claude model' },
951 { value: 'claude-sonnet-4-20250514', label: 'Claude 4 Sonnet', description: 'High performance' },
952 { value: 'claude-3-7-sonnet-20250219', label: 'Claude 3.7 Sonnet', description: 'High intelligence' },
953 { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet', description: 'Intelligent and balanced' },
954 { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus', description: 'Highly complex tasks' },
955 { value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', description: 'Balanced performance' },
956 { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', description: 'Fastest Claude model' },
957 ],
958 xai: [
959 { value: 'grok-4-0709', label: 'Grok 4', description: 'Latest flagship model - unparalleled performance in natural language, math and reasoning' }, // Added Grok-4
960 { value: 'grok-3-beta', label: 'Grok-3', description: 'Powerful model with 131K context' },
961 { value: 'grok-3-fast-beta', label: 'Grok-3 Fast', description: 'High performance with faster responses' },
962 { value: 'grok-3-mini-beta', label: 'Grok-3 Mini', description: 'Affordable model with good performance' },
963 { value: 'grok-3-mini-fast-beta', label: 'Grok-3 Mini Fast', description: 'Quick and cost-effective' },
964 { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' },
965 ],
966 deepseek: [
967 { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
968 ],
969 };
970
971 let allModels = [];
972 Object.keys(models).forEach(key => {
973 if (category === 'all' || category === key) {
974 allModels = allModels.concat(models[key]);
975 }
976 });
977
978 // Filter by search term if present
979 if (filter) {
980 const lowerFilter = filter.toLowerCase();
981 allModels = allModels.filter(model =>
982 model.label.toLowerCase().includes(lowerFilter) ||
983 model.description.toLowerCase().includes(lowerFilter)
984 );
985 }
986
987 // Create model cards
988 allModels.forEach(model => {
989 const isSelected = $modelSelect.val() === model.value;
990 const $modelCard = $(`
991 <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
992 <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
993 <div class="mxchat-model-selector-info">
994 <h4 class="mxchat-model-selector-title">${model.label}</h4>
995 <p class="mxchat-model-selector-description">${model.description}</p>
996 </div>
997 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
998 </div>
999 `);
1000 $grid.append($modelCard);
1001 });
1002 }
1003
1004
1005
1006 // Helper function to get icon for each model
1007 function getModelIcon(modelValue) {
1008 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>';
1009 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>';
1010 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>';
1011 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>';
1012 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>';
1013 return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
1014 }
1015
1016 // Event handlers
1017 $modelSelectorButton.on('click', function() {
1018 $('#mxchat_model_selector_modal').show();
1019 populateModelsGrid('', 'all');
1020 });
1021
1022 $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
1023 $('#mxchat_model_selector_modal').hide();
1024 });
1025
1026 $('.mxchat-model-category-btn').on('click', function() {
1027 $('.mxchat-model-category-btn').removeClass('active');
1028 $(this).addClass('active');
1029 const category = $(this).data('category');
1030 const searchTerm = $('#mxchat_model_search_input').val();
1031 populateModelsGrid(searchTerm, category);
1032 });
1033
1034 $('#mxchat_model_search_input').on('input', function() {
1035 const searchTerm = $(this).val();
1036 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1037 populateModelsGrid(searchTerm, activeCategory);
1038 });
1039
1040 $(document).on('click', '.mxchat-model-selector-card', function() {
1041 const modelValue = $(this).data('value');
1042 $modelSelect.val(modelValue).trigger('change');
1043 updateButtonText();
1044 $('#mxchat_model_selector_modal').hide();
1045 });
1046
1047 // Close modal when clicking outside
1048 $(window).on('click', function(event) {
1049 if ($(event.target).is('#mxchat_model_selector_modal')) {
1050 $('#mxchat_model_selector_modal').hide();
1051 }
1052 });
1053 }
1054
1055 // Embedding model selector - completely separate from chat model selector
1056 function setupMxChatEmbeddingModelSelector() {
1057 const $embeddingModelSelect = $('#embedding_model');
1058
1059 // Skip if the element doesn't exist on the page
1060 if ($embeddingModelSelect.length === 0) {
1061 return;
1062 }
1063
1064 const $embeddingModelSelectorButton = $('<button>', {
1065 type: 'button',
1066 id: 'mxchat_embedding_model_selector_btn',
1067 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
1068 text: 'Select Embedding Model'
1069 });
1070
1071 // Replace the select dropdown with a button
1072 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
1073
1074 // Update button text to show currently selected model
1075 function updateButtonText() {
1076 const selectedModel = $embeddingModelSelect.val();
1077 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
1078 $embeddingModelSelectorButton.text(selectedModelText);
1079 }
1080
1081 // Initialize button text
1082 updateButtonText();
1083
1084 // Create a unique ID for the modal to avoid conflicts
1085 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
1086
1087 // Create and append modal HTML with unique IDs
1088 const embeddingModelSelectorModal = `
1089 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
1090 <div class="mxchat-embedding-model-selector-modal-content">
1091 <div class="mxchat-embedding-model-selector-modal-header">
1092 <h3>Select Embedding Model</h3>
1093 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
1094 </div>
1095 <div class="mxchat-embedding-model-selector-modal-body">
1096 <div class="mxchat-embedding-model-selector-search-container">
1097 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
1098 </div>
1099 <div class="mxchat-embedding-model-selector-categories">
1100 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
1101 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
1102 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
1103 <button class="mxchat-embedding-model-category-btn" data-category="gemini">Google Gemini</button>
1104 </div>
1105 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
1106 </div>
1107 <div class="mxchat-embedding-model-selector-modal-footer">
1108 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
1109 </div>
1110 </div>
1111 </div>
1112 `;
1113
1114 // Use jQuery's append to ensure it doesn't clash with existing modals
1115 $('body').append(embeddingModelSelectorModal);
1116
1117 // Populate models grid
1118 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1119 const $grid = $('#mxchat_embedding_models_grid');
1120 $grid.empty();
1121
1122 // Define embedding models with descriptions and context lengths
1123 const models = {
1124 openai: [
1125 {
1126 value: 'text-embedding-3-small',
1127 label: 'TE3 Small',
1128 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1129 },
1130 {
1131 value: 'text-embedding-ada-002',
1132 label: 'Ada 2',
1133 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1134 },
1135 {
1136 value: 'text-embedding-3-large',
1137 label: 'TE3 Large',
1138 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1139 }
1140 ],
1141 voyage: [
1142 {
1143 value: 'voyage-3-large',
1144 label: 'Voyage-3 Large',
1145 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1146 }
1147 ],
1148 gemini: [
1149 {
1150 value: 'gemini-embedding-exp-03-07',
1151 label: 'Gemini Embedding',
1152 description: 'Experimental SOTA embeddings (1536 dimensions, 8K context)'
1153 }
1154 ]
1155 };
1156
1157 let allModels = [];
1158 Object.keys(models).forEach(key => {
1159 if (category === 'all' || category === key) {
1160 allModels = allModels.concat(models[key]);
1161 }
1162 });
1163
1164 // Filter by search term if present
1165 if (filter) {
1166 const lowerFilter = filter.toLowerCase();
1167 allModels = allModels.filter(model =>
1168 model.label.toLowerCase().includes(lowerFilter) ||
1169 model.description.toLowerCase().includes(lowerFilter)
1170 );
1171 }
1172
1173 // Create model cards
1174 allModels.forEach(model => {
1175 const isSelected = $embeddingModelSelect.val() === model.value;
1176 let providerClass = 'mxchat-embedding-model-provider-openai';
1177
1178 if (model.value.startsWith('voyage-')) {
1179 providerClass = 'mxchat-embedding-model-provider-voyage';
1180 } else if (model.value.startsWith('gemini-embedding-')) {
1181 providerClass = 'mxchat-embedding-model-provider-gemini';
1182 }
1183
1184 let iconHTML = '';
1185 if (model.value.startsWith('voyage-')) {
1186 iconHTML = '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>';
1187 } else if (model.value.startsWith('gemini-embedding-')) {
1188 iconHTML = '<span class="dashicons dashicons-google mxchat-embedding-model-icon-gemini"></span>';
1189 } else {
1190 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>';
1191 }
1192
1193 const $modelCard = $(`
1194 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1195 <div class="mxchat-embedding-model-selector-icon">
1196 ${iconHTML}
1197 </div>
1198 <div class="mxchat-embedding-model-selector-info">
1199 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1200 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1201 </div>
1202 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1203 </div>
1204 `);
1205
1206 $grid.append($modelCard);
1207 });
1208 }
1209
1210 // Event handlers - use namespaced events to avoid conflicts
1211 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1212 e.stopPropagation(); // Prevent event bubbling
1213 $('#' + embeddingModalId).show();
1214 populateEmbeddingModelsGrid('', 'all');
1215 });
1216
1217 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1218 e.stopPropagation(); // Prevent event bubbling
1219 $('#' + embeddingModalId).hide();
1220 });
1221
1222 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1223 e.stopPropagation(); // Prevent event bubbling
1224 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1225 $(this).addClass('active');
1226 const category = $(this).data('category');
1227 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1228 populateEmbeddingModelsGrid(searchTerm, category);
1229 });
1230
1231 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1232 const searchTerm = $(this).val();
1233 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1234 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1235 });
1236
1237 // Use a direct selector to avoid conflicts with other card elements
1238 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1239 e.stopPropagation(); // Prevent event bubbling
1240 const modelValue = $(this).data('value');
1241
1242 // Important: Only update this specific select element
1243 $embeddingModelSelect.val(modelValue);
1244
1245 // Manually trigger change only on this element
1246 const changeEvent = new Event('change', { bubbles: true });
1247 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1248
1249 // Update button text
1250 updateButtonText();
1251
1252 // Hide modal
1253 $('#' + embeddingModalId).hide();
1254 });
1255
1256 // Close modal when clicking outside - use namespaced events
1257 $(window).on('click.embeddingModelSelector', function(event) {
1258 if ($(event.target).is('#' + embeddingModalId)) {
1259 $('#' + embeddingModalId).hide();
1260 }
1261 });
1262 }
1263
1264 // Call this function after the DOM is fully loaded
1265 $(document).ready(function() {
1266 setupMxChatModelSelector();
1267 setupMxChatEmbeddingModelSelector();
1268 });
1269
1270 // Initialize API key visibility
1271 setupAPIKeyVisibility();
1272
1273 // Add Intent Form Submission
1274 $('#mxchat-add-intent-form').on('submit', function(event) {
1275 $('#mxchat-intent-loading').show();
1276 $('#mxchat-intent-loading-text').show();
1277 $(this).find('button[type="submit"]').hide();
1278 });
1279
1280 // Inline Edit Functionality
1281 $('.edit-button').on('click', function() {
1282 var row = $(this).closest('tr');
1283 row.find('.content-view, .url-view').hide();
1284 row.find('.content-edit, .url-edit').show();
1285 row.find('.edit-button').hide();
1286 row.find('.save-button').show();
1287 });
1288
1289 // Save button handler
1290 // Save button handler
1291 $('.save-button').on('click', function() {
1292 var button = $(this);
1293 var row = button.closest('tr');
1294 var id = button.data('id');
1295 var nonce = button.data('nonce'); // Get nonce from button data attribute
1296 var newContent = row.find('.content-edit').val();
1297 var newUrl = row.find('.url-edit').val();
1298
1299 //console.log('Nonce from button:', nonce); // Debug
1300
1301 button.prop('disabled', true);
1302 button.text('Saving...');
1303
1304 $.ajax({
1305 url: mxchatAdmin.ajax_url,
1306 type: 'POST',
1307 data: {
1308 action: 'mxchat_save_inline_prompt',
1309 id: id,
1310 article_content: newContent,
1311 article_url: newUrl,
1312 _ajax_nonce: nonce // Use nonce from button
1313 },
1314 success: function(response) {
1315 button.prop('disabled', false);
1316 button.text('Save');
1317
1318 if (response.success) {
1319 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1320 if (newUrl) {
1321 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank">' + newUrl + '</a>');
1322 } else {
1323 row.find('.url-view').html('N/A');
1324 }
1325
1326 row.find('.content-edit, .url-edit').hide();
1327 row.find('.content-view, .url-view').show();
1328 row.find('.save-button').hide();
1329 row.find('.edit-button').show();
1330 } else {
1331 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1332 }
1333 },
1334 error: function() {
1335 button.prop('disabled', false);
1336 button.text('Save');
1337 alert('An error occurred while saving.');
1338 }
1339 });
1340 });
1341
1342
1343 // Questions handling
1344 $('.mxchat-add-question').on('click', function () {
1345 const container = $('#mxchat-additional-questions-container');
1346 const questionCount = container.find('.mxchat-question-row').length + 4;
1347 const questionIndex = container.find('.mxchat-question-row').length;
1348
1349 const newQuestion = `
1350 <div class="mxchat-question-row">
1351 <input type="text"
1352 name="additional_popular_questions[]"
1353 placeholder="Enter Additional Popular Question ${questionCount}"
1354 class="regular-text mxchat-question-input"
1355 data-question-index="${questionIndex}" />
1356 <button type="button" class="button mxchat-remove-question"
1357 aria-label="Remove question">Remove</button>
1358 </div>
1359 `;
1360 container.append(newQuestion);
1361 });
1362
1363 $(document).on('click', '.mxchat-remove-question', function () {
1364 $(this).closest('.mxchat-question-row').remove();
1365 saveQuestions();
1366 });
1367
1368 $(document).on('change', '.mxchat-question-input', function() {
1369 saveQuestions();
1370 });
1371
1372 function saveQuestions() {
1373 const questions = [];
1374 $('.mxchat-question-input').each(function() {
1375 const value = $(this).val().trim();
1376 if (value) {
1377 questions.push(value);
1378 }
1379 });
1380
1381 const feedbackContainer = $('<div class="feedback-container"></div>');
1382 const spinner = $('<div class="saving-spinner"></div>');
1383 const successIcon = $('<div class="success-icon">✔</div>');
1384
1385 // Append feedback after the add button
1386 $('.mxchat-add-question').after(feedbackContainer);
1387 feedbackContainer.append(spinner);
1388
1389 // Save via AJAX
1390 $.ajax({
1391 url: mxchatAdmin.ajax_url,
1392 type: 'POST',
1393 data: {
1394 action: 'mxchat_save_setting',
1395 name: 'additional_popular_questions',
1396 value: JSON.stringify(questions),
1397 _ajax_nonce: mxchatAdmin.setting_nonce
1398 },
1399 success: function(response) {
1400 if (response.success) {
1401 spinner.fadeOut(200, function() {
1402 feedbackContainer.append(successIcon);
1403 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
1404 feedbackContainer.remove();
1405 });
1406 });
1407 } else {
1408 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
1409 feedbackContainer.remove();
1410 }
1411 },
1412 error: function() {
1413 alert('An error occurred while saving questions.');
1414 feedbackContainer.remove();
1415 }
1416 });
1417 }
1418
1419 // Live agent status handler
1420 const statusToggle = document.getElementById('live_agent_status');
1421 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
1422 if (statusToggle && statusText) {
1423 statusToggle.addEventListener('change', function() {
1424 // Update display text
1425 statusText.textContent = this.checked ? 'Online' : 'Offline';
1426
1427 // Send the correct on/off value to the server
1428 if (window.mxchatSaveSetting) {
1429 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
1430 }
1431 });
1432 }
1433
1434 // Function to adjust the textarea height to content
1435 function adjustTextareaHeight() {
1436 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
1437 this.style.height = this.scrollHeight + 'px'; // Expand to content height
1438 }
1439
1440 // Function to reset the textarea height to initial
1441 function resetTextareaHeight() {
1442 this.style.height = ''; // Remove inline height, reverting to CSS default
1443 }
1444
1445 // Target the specific textarea by ID
1446 var $textarea = $('#system_prompt_instructions');
1447
1448 // Bind events
1449 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
1450 .on('blur', resetTextareaHeight); // Reset on blur
1451 });
1452
1453
1454
1455 document.addEventListener('DOMContentLoaded', function() {
1456 // Check if we're on the correct page before initializing
1457 const modal = document.getElementById('mxchat-action-modal');
1458
1459 // Only initialize if the modal exists on this page
1460 if (modal) {
1461 //console.log('MXChat Action Modal JS Loaded');
1462
1463 // Initialize the action modal functionality
1464 initStepBasedActionModal();
1465 }
1466
1467 // Function to initialize the step-based action modal
1468 function initStepBasedActionModal() {
1469 // We already checked for modal existence above, so no need to check again
1470
1471 const actionStep1 = document.getElementById('mxchat-action-step-1');
1472 const actionStep2 = document.getElementById('mxchat-action-step-2');
1473 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
1474 const searchInput = document.getElementById('action-type-search');
1475 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
1476 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
1477 const actionForm = document.getElementById('mxchat-action-form');
1478 const callbackInput = document.getElementById('callback_function');
1479 const actionIdField = document.getElementById('edit_action_id');
1480 const labelField = document.getElementById('intent_label');
1481 const phrasesField = document.getElementById('action_phrases');
1482 const formActionType = document.getElementById('form_action_type');
1483 const nonceContainer = document.getElementById('action-nonce-container');
1484 const thresholdSlider = document.getElementById('similarity_threshold');
1485 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1486
1487 // Rest of your initialization code remains the same...
1488
1489 // Log the structure of one action card for debugging
1490 if (actionCards.length > 0) {
1491 //console.log('First action card data attributes:', actionCards[0].dataset);
1492 //console.log('First action card HTML:', actionCards[0].outerHTML);
1493 }
1494
1495 // Add click event listeners to category buttons
1496 categoryButtons.forEach(button => {
1497 button.addEventListener('click', function() {
1498 //console.log('Category button clicked:', this.dataset.category);
1499
1500 // Remove active class from all buttons
1501 categoryButtons.forEach(btn => btn.classList.remove('active'));
1502
1503 // Add active class to clicked button
1504 this.classList.add('active');
1505
1506 // Get selected category
1507 const category = this.dataset.category;
1508
1509 // Filter action cards
1510 filterActionCards(category, searchInput.value);
1511 });
1512 });
1513
1514 // Add search functionality
1515 if (searchInput) {
1516 searchInput.addEventListener('input', function() {
1517 // Get active category
1518 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
1519 //console.log('Search input changed, active category:', activeCategory);
1520
1521 // Filter action cards
1522 filterActionCards(activeCategory, this.value);
1523 });
1524 }
1525
1526 // Add click event listeners to action cards
1527 actionCards.forEach(card => {
1528 card.addEventListener('click', function() {
1529 // Get the action data
1530 const isPro = this.dataset.pro === 'true';
1531 const isInstalled = this.dataset.installed === 'true';
1532 const addonName = this.dataset.addon || '';
1533 const actionValue = this.dataset.value;
1534 const actionLabel = this.dataset.label;
1535 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
1536 const actionDescription = this.querySelector('p').textContent;
1537
1538 // Pro check using the proper detection method
1539 const proIsActivated = typeof mxchatAdmin !== 'undefined' &&
1540 (mxchatAdmin.is_activated === '1' ||
1541 mxchatAdmin.is_activated === 'true' ||
1542 mxchatAdmin.is_activated === true);
1543
1544 // Handle different states
1545 if (isPro && !proIsActivated) {
1546 // Pro feature but no Pro license
1547 showProFeatureNotice();
1548 return;
1549 }
1550
1551 if (addonName && !isInstalled) {
1552 // Add-on required but not installed
1553 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '') || addonName + ' Add-on';
1554 showAddonRequiredNotice(addonDisplayName);
1555 return;
1556 }
1557
1558 // If we get here, the action is available - proceed as normal
1559 callbackInput.value = actionValue;
1560
1561 // Update the selected action display in step 2
1562 document.getElementById('selected-action-title').textContent = actionLabel;
1563 document.getElementById('selected-action-description').textContent = actionDescription;
1564 document.getElementById('selected-action-icon').innerHTML =
1565 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1566
1567 // Set a default label based on the action type (user can change it)
1568 if (!labelField.value) {
1569 labelField.value = actionLabel;
1570 }
1571
1572 // Move to step 2
1573 actionStep1.classList.remove('active');
1574 actionStep2.classList.add('active');
1575
1576 // Update modal title
1577 });
1578 });
1579
1580 // Back button functionality
1581 if (backToStep1Btn) {
1582 backToStep1Btn.addEventListener('click', function() {
1583 //console.log('Back button clicked');
1584 actionStep2.classList.remove('active');
1585 actionStep1.classList.add('active');
1586 });
1587 }
1588
1589 // Function to filter action cards by category and search term
1590 function filterActionCards(category, searchTerm) {
1591 searchTerm = searchTerm.toLowerCase().trim();
1592 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
1593
1594 let visibleCount = 0;
1595
1596 // Show all cards initially with animation
1597 actionCards.forEach((card, index) => {
1598 // Reset animation
1599 card.style.animation = 'none';
1600 // Trigger reflow
1601 void card.offsetWidth;
1602
1603 // Determine if card should be visible based on category and search term
1604 const cardCategory = card.dataset.category || '';
1605 const matchesCategory = category === 'all' || cardCategory === category;
1606
1607 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
1608 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
1609 const matchesSearch = searchTerm === '' ||
1610 cardTitle.includes(searchTerm) ||
1611 cardDesc.includes(searchTerm);
1612
1613 // Show/hide card with animation
1614 if (matchesCategory && matchesSearch) {
1615 card.style.display = 'flex';
1616 // Staggered animation for cards
1617 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
1618 visibleCount++;
1619 } else {
1620 card.style.display = 'none';
1621 }
1622 });
1623
1624 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
1625 }
1626
1627 // Function to show notice for Pro features
1628 function showProFeatureNotice() {
1629 //console.log('Showing Pro feature notice');
1630 // Check if we already have a notification container
1631 let noticeContainer = document.querySelector('.mxchat-pro-notice');
1632
1633 if (!noticeContainer) {
1634 // Create the notice container
1635 noticeContainer = document.createElement('div');
1636 noticeContainer.className = 'mxchat-pro-notice';
1637
1638 // Create content
1639 noticeContainer.innerHTML = `
1640 <div class="mxchat-pro-notice-content">
1641 <h3>MxChat Pro Feature</h3>
1642 <p>This action is available in the Pro version only.</p>
1643 <div class="mxchat-pro-notice-buttons">
1644 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
1645 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
1646 </div>
1647 </div>
1648 `;
1649
1650 // Append to body
1651 document.body.appendChild(noticeContainer);
1652
1653 // Add close functionality
1654 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
1655 closeButton.addEventListener('click', function() {
1656 noticeContainer.classList.remove('active');
1657 setTimeout(() => {
1658 noticeContainer.remove();
1659 }, 300);
1660 });
1661
1662 // Click outside to close
1663 noticeContainer.addEventListener('click', function(e) {
1664 if (e.target === noticeContainer) {
1665 closeButton.click();
1666 }
1667 });
1668
1669 // Show with animation
1670 setTimeout(() => {
1671 noticeContainer.classList.add('active');
1672 }, 10);
1673 } else {
1674 // If it already exists, just make it visible again
1675 noticeContainer.classList.add('active');
1676 }
1677 }
1678
1679 // Function to show notice for add-on requirements
1680 function showAddonRequiredNotice(addonName) {
1681 //console.log(`Showing add-on notice for: ${addonName}`);
1682 // Check if we already have a notification container
1683 let noticeContainer = document.querySelector('.mxchat-addon-notice');
1684
1685 if (!noticeContainer) {
1686 // Create the notice container
1687 noticeContainer = document.createElement('div');
1688 noticeContainer.className = 'mxchat-addon-notice';
1689
1690 // Create content
1691 noticeContainer.innerHTML = `
1692 <div class="mxchat-addon-notice-content">
1693 <span class="mxchat-addon-notice-icon">🧩</span>
1694 <h3>Add-on Required</h3>
1695 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
1696 <div class="mxchat-addon-notice-buttons">
1697 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
1698 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
1699 </div>
1700 </div>
1701 `;
1702
1703 // Append to body
1704 document.body.appendChild(noticeContainer);
1705
1706 // Add close functionality
1707 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
1708 closeButton.addEventListener('click', function() {
1709 noticeContainer.classList.remove('active');
1710 setTimeout(() => {
1711 noticeContainer.remove();
1712 }, 300);
1713 });
1714
1715 // Click outside to close
1716 noticeContainer.addEventListener('click', function(e) {
1717 if (e.target === noticeContainer) {
1718 closeButton.click();
1719 }
1720 });
1721
1722 // Show with animation
1723 setTimeout(() => {
1724 noticeContainer.classList.add('active');
1725 }, 10);
1726 } else {
1727 // If it already exists, update the content
1728 const addonNameElement = noticeContainer.querySelector('p strong');
1729 if (addonNameElement) {
1730 addonNameElement.textContent = addonName;
1731 }
1732
1733 // Make it visible again
1734 noticeContainer.classList.add('active');
1735 }
1736 }
1737
1738 // Form submission handling
1739 if (actionForm) {
1740 actionForm.addEventListener('submit', function() {
1741 //console.log('Form submitted');
1742 document.getElementById('mxchat-action-loading').style.display = 'flex';
1743 this.querySelector('button[type="submit"]').disabled = true;
1744 });
1745 }
1746 }
1747
1748 // Setup add action buttons (only if we're on the correct page)
1749 if (modal) {
1750 // Update the modal open function to support the step-based flow
1751 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
1752 //console.log('Modal opening, edit mode:', isEdit);
1753
1754 // No need to check again, we already verified modal exists
1755
1756 // Get form fields
1757 const actionIdField = document.getElementById('edit_action_id');
1758 const labelField = document.getElementById('intent_label');
1759 const phrasesField = document.getElementById('action_phrases');
1760 const formActionType = document.getElementById('form_action_type');
1761 const callbackInput = document.getElementById('callback_function');
1762 const saveButton = document.getElementById('mxchat-save-action-btn');
1763 const nonceContainer = document.getElementById('action-nonce-container');
1764 const thresholdSlider = document.getElementById('similarity_threshold');
1765 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1766 const actionStep1 = document.getElementById('mxchat-action-step-1');
1767 const actionStep2 = document.getElementById('mxchat-action-step-2');
1768 const searchInput = document.getElementById('action-type-search');
1769
1770 // Set up modal for edit or create
1771 if (isEdit) {
1772 saveButton.textContent = 'Update Action';
1773 formActionType.value = 'mxchat_edit_intent';
1774 actionIdField.value = actionId;
1775 labelField.value = label;
1776 phrasesField.value = phrases;
1777 callbackInput.value = callbackFunction;
1778 thresholdSlider.value = threshold; // Set the current threshold value
1779 thresholdDisplay.textContent = threshold + '%'; // Update display
1780
1781 // Update the nonce field for editing
1782 nonceContainer.innerHTML = ''; // Clear existing nonce
1783 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
1784 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
1785 }
1786
1787 // For editing, go directly to step 2 and update the selected action display
1788 actionStep1.classList.remove('active');
1789 actionStep2.classList.add('active');
1790
1791 // Find the matching action card to get its details
1792 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
1793 let foundCard = null;
1794
1795 actionCards.forEach(card => {
1796 if (card.dataset.value === callbackFunction) {
1797 foundCard = card;
1798 }
1799 });
1800
1801 if (foundCard) {
1802 //console.log('Found matching action card for:', callbackFunction);
1803 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
1804 const actionIconElement = foundCard.querySelector('.dashicons');
1805 const actionIcon = actionIconElement
1806 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
1807 : 'admin-generic';
1808 const actionDescription = foundCard.querySelector('p')?.textContent || '';
1809
1810 document.getElementById('selected-action-title').textContent = actionLabel;
1811 document.getElementById('selected-action-description').textContent = actionDescription;
1812 document.getElementById('selected-action-icon').innerHTML =
1813 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1814 } else {
1815 //console.log('No matching action card found for:', callbackFunction);
1816 // Fallback if we can't find the card
1817 document.getElementById('selected-action-title').textContent = label;
1818 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
1819 document.getElementById('selected-action-icon').innerHTML =
1820 `<span class="dashicons dashicons-admin-generic"></span>`;
1821 }
1822 } else {
1823 //console.log('Setting up create mode');
1824 saveButton.textContent = 'Save Action';
1825 formActionType.value = 'mxchat_add_intent';
1826 actionIdField.value = '';
1827 labelField.value = '';
1828 phrasesField.value = '';
1829 callbackInput.value = '';
1830 thresholdSlider.value = 85; // Default value for new actions
1831 thresholdDisplay.textContent = '85%'; // Default display
1832
1833 // Update the nonce field for adding
1834 nonceContainer.innerHTML = ''; // Clear existing nonce
1835 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
1836 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
1837 }
1838
1839 // For creating new, start at step 1
1840 actionStep1.classList.add('active');
1841 actionStep2.classList.remove('active');
1842 }
1843
1844 // Show modal with animation
1845 modal.style.display = 'flex';
1846 requestAnimationFrame(() => {
1847 modal.classList.add('active');
1848 });
1849
1850 // Set up close handlers
1851 const closeModal = () => {
1852 //console.log('Closing modal');
1853 modal.classList.remove('active');
1854 setTimeout(() => {
1855 modal.style.display = 'none';
1856 }, 300); // Match the CSS transition time
1857 };
1858
1859 // Close button handler
1860 const closeBtn = modal.querySelector('.mxchat-modal-close');
1861 if (closeBtn) {
1862 closeBtn.onclick = closeModal;
1863 }
1864
1865 // Cancel button handler
1866 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
1867 if (cancelBtns) {
1868 cancelBtns.forEach(btn => {
1869 btn.onclick = closeModal;
1870 });
1871 }
1872
1873 // Click outside modal to close
1874 modal.onclick = (e) => {
1875 if (e.target === modal) {
1876 closeModal();
1877 }
1878 };
1879
1880 // Escape key to close modal
1881 document.addEventListener('keydown', function(e) {
1882 if (e.key === 'Escape' && modal.classList.contains('active')) {
1883 closeModal();
1884 }
1885 }, { once: true });
1886
1887 // Focus appropriate field based on current step
1888 if (isEdit || actionStep2.classList.contains('active')) {
1889 if (labelField) labelField.focus();
1890 } else {
1891 if (searchInput) searchInput.focus();
1892 }
1893
1894 return closeModal; // Return close function for external use
1895 };
1896
1897 // Setup add action buttons
1898 const addActionBtn = document.getElementById('mxchat-add-action-btn');
1899 if (addActionBtn) {
1900 //console.log('Add action button found');
1901 addActionBtn.onclick = () => window.mxchatOpenActionModal();
1902 }
1903
1904 const createFirstAction = document.getElementById('mxchat-create-first-action');
1905 if (createFirstAction) {
1906 //console.log('Create first action button found');
1907 createFirstAction.onclick = () => window.mxchatOpenActionModal();
1908 }
1909
1910 // Setup edit buttons
1911 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
1912 //console.log('Edit buttons found:', editButtons.length);
1913 editButtons.forEach(button => {
1914 button.onclick = () => {
1915 const actionId = button.dataset.actionId;
1916 const phrases = button.dataset.phrases;
1917 const label = button.dataset.label;
1918 const threshold = button.dataset.threshold || 85;
1919 const callbackFunction = button.dataset.callbackFunction;
1920
1921 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction);
1922 };
1923 });
1924 }
1925 });
1926
1927 jQuery(document).ready(function($) {
1928 // Toggle custom post types container
1929 $('#mxchat-custom-post-types-toggle').on('click', function(e) {
1930 e.preventDefault();
1931
1932 $('#mxchat-custom-post-types-container').slideToggle(300);
1933
1934 // Rotate the toggle icon
1935 const $icon = $(this).find('.mxchat-accordion-icon');
1936 if ($('#mxchat-custom-post-types-container').is(':visible')) {
1937 $icon.css('transform', 'rotate(180deg)');
1938 $(this).closest('.mxchat-settings-accordion').addClass('active');
1939 } else {
1940 $icon.css('transform', 'rotate(0deg)');
1941 $(this).closest('.mxchat-settings-accordion').removeClass('active');
1942 }
1943 });
1944
1945 // If there are any selections made, auto-expand the container
1946 function autoExpandIfNeeded() {
1947 // Check if any checkbox in the container is checked
1948 const hasCheckedItems = $('#mxchat-custom-post-types-container input[type="checkbox"]:checked').length > 0;
1949
1950 if (hasCheckedItems) {
1951 $('#mxchat-custom-post-types-container').show();
1952 $('#mxchat-custom-post-types-toggle .mxchat-accordion-icon').css('transform', 'rotate(180deg)');
1953 $('.mxchat-settings-accordion').addClass('active');
1954 }
1955 }
1956
1957 // Run on page load
1958 autoExpandIfNeeded();
1959 });
1960
1961 document.addEventListener('DOMContentLoaded', function() {
1962 var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
1963 var modal = document.getElementById('mxchatSampleModal');
1964 var modalClose = document.getElementById('mxchatModalClose');
1965 var closeBtn = document.getElementById('mxchatCloseBtn');
1966 var copyBtn = document.getElementById('mxchatCopyBtn');
1967 var instructionsContent = document.querySelector('.mxchat-instructions-content');
1968 var modalContent = document.querySelector('.mxchat-instructions-modal-content');
1969
1970 if (!viewSampleBtn || !modal) {
1971 return;
1972 }
1973
1974 // Open modal
1975 viewSampleBtn.addEventListener('click', function(e) {
1976 e.preventDefault();
1977 e.stopPropagation();
1978 modal.classList.add('mxchat-instructions-show');
1979 });
1980
1981 // Close modal function
1982 function closeModal(e) {
1983 if (e) {
1984 e.preventDefault();
1985 e.stopPropagation();
1986 }
1987 modal.classList.remove('mxchat-instructions-show');
1988 }
1989
1990 // Close modal events
1991 if (modalClose) {
1992 modalClose.addEventListener('click', function(e) {
1993 closeModal(e);
1994 });
1995 }
1996
1997 if (closeBtn) {
1998 closeBtn.addEventListener('click', function(e) {
1999 closeModal(e);
2000 });
2001 }
2002
2003 // Close on backdrop click ONLY (not on hover)
2004 modal.addEventListener('click', function(e) {
2005 // Only close if clicking directly on the overlay, not on child elements
2006 if (e.target === modal) {
2007 closeModal(e);
2008 }
2009 });
2010
2011 // Prevent modal content clicks from closing the modal
2012 if (modalContent) {
2013 modalContent.addEventListener('click', function(e) {
2014 e.stopPropagation();
2015 });
2016 }
2017
2018 // Close on escape key
2019 document.addEventListener('keydown', function(e) {
2020 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2021 closeModal();
2022 }
2023 });
2024
2025 // Copy functionality
2026 if (copyBtn && instructionsContent) {
2027 copyBtn.addEventListener('click', function(e) {
2028 e.preventDefault();
2029 e.stopPropagation();
2030
2031 var text = instructionsContent.textContent;
2032
2033 if (navigator.clipboard) {
2034 navigator.clipboard.writeText(text).then(function() {
2035 showCopySuccess();
2036 }).catch(function() {
2037 fallbackCopy(text);
2038 });
2039 } else {
2040 fallbackCopy(text);
2041 }
2042 });
2043 }
2044
2045 function fallbackCopy(text) {
2046 var textArea = document.createElement('textarea');
2047 textArea.value = text;
2048 textArea.style.position = 'fixed';
2049 textArea.style.left = '-999999px';
2050 textArea.style.top = '-999999px';
2051 document.body.appendChild(textArea);
2052 textArea.select();
2053 try {
2054 document.execCommand('copy');
2055 showCopySuccess();
2056 } catch (err) {
2057 console.error('Copy failed');
2058 }
2059 document.body.removeChild(textArea);
2060 }
2061
2062 function showCopySuccess() {
2063 var originalText = copyBtn.innerHTML;
2064 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!';
2065
2066 setTimeout(function() {
2067 copyBtn.innerHTML = originalText;
2068 }, 2000);
2069 }
2070 });
2071
2072 jQuery(document).ready(function($) {
2073 var emailDependentFields = [
2074 '#email_blocker_header_content',
2075 '#email_blocker_button_text',
2076 '#enable_name_field',
2077 '#name_field_placeholder'
2078 ];
2079
2080 // Add visual indicators
2081 emailDependentFields.forEach(function(fieldId) {
2082 var $row = $(fieldId).closest('tr');
2083 $row.addClass('email-dependent-field');
2084
2085 // Add an icon to show it's dependent
2086 var $label = $row.find('th label, th');
2087 $label.prepend('<span class="email-dependent-icon" style="color: #0073aa; margin-right: 5px;">↳</span>');
2088 });
2089
2090 // Hide initially with opacity for smoother transition
2091 emailDependentFields.forEach(function(fieldId) {
2092 $(fieldId).closest('tr').hide().css('opacity', '0');
2093 });
2094
2095 function toggleEmailFields() {
2096 var emailEnabled = $('#enable_email_block').is(':checked');
2097
2098 emailDependentFields.forEach(function(fieldId) {
2099 var $row = $(fieldId).closest('tr');
2100 if (emailEnabled) {
2101 $row.slideDown(400).animate({opacity: 1}, 200);
2102 } else {
2103 $row.animate({opacity: 0}, 200).slideUp(400);
2104 }
2105 });
2106 }
2107
2108 toggleEmailFields();
2109 $('#enable_email_block').on('change', toggleEmailFields);
2110 });
2111
2112
2113
2114 //Handle role restriction changes
2115 jQuery(document).ready(function($) {
2116 //Handle role restriction changes for both data sources
2117 $(document).on('change', '.mxchat-role-select', function() {
2118 const $select = $(this);
2119 const entryId = $select.data('entry-id');
2120 const dataSource = $select.data('data-source');
2121 const roleRestriction = $select.val();
2122 const nonce = $select.data('nonce');
2123
2124 // Visual feedback
2125 $select.prop('disabled', true).addClass('updating');
2126
2127 $.ajax({
2128 url: ajaxurl,
2129 type: 'POST',
2130 data: {
2131 action: 'mxchat_update_role_restriction',
2132 nonce: nonce,
2133 entry_id: entryId,
2134 data_source: dataSource, // NEW: Include data source
2135 role_restriction: roleRestriction
2136 },
2137 success: function(response) {
2138 if (response.success) {
2139 // Show success feedback
2140 $select.removeClass('updating').addClass('updated');
2141 setTimeout(() => {
2142 $select.removeClass('updated');
2143 }, 2000);
2144 } else {
2145 alert('Failed to update role restriction: ' + response.data);
2146 // Revert selection
2147 $select.val($select.data('original-value'));
2148 }
2149 },
2150 error: function() {
2151 alert('Error updating role restriction');
2152 // Revert selection
2153 $select.val($select.data('original-value'));
2154 },
2155 complete: function() {
2156 $select.prop('disabled', false);
2157 }
2158 });
2159
2160 // Store original value for potential revert
2161 $select.data('original-value', roleRestriction);
2162 });
2163
2164 // Store initial values
2165 $('.mxchat-role-select').each(function() {
2166 $(this).data('original-value', $(this).val());
2167 });
2168 });