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

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

2,063 lines 95.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Simple debounce function implementation
2 function debounce(func, wait) {
3 let timeout;
4 return function executedFunction(...args) {
5 const later = () => {
6 clearTimeout(timeout);
7 func(...args);
8 };
9 clearTimeout(timeout);
10 timeout = setTimeout(later, wait);
11 };
12 }
13
14 // Helper function to open edit modal for intents/actions
15 function mxchatOpenEditModal(intentId, phrases) {
16 const modal = document.getElementById('mxchat-edit-modal');
17 if (!modal) return;
18
19 // Get form fields
20 const intentIdField = document.getElementById('edit_intent_id');
21 const phrasesField = document.getElementById('edit_phrases');
22
23 // Set values
24 intentIdField.value = intentId;
25 phrasesField.value = phrases;
26
27 // Show modal with animation
28 modal.style.display = 'flex';
29 requestAnimationFrame(() => {
30 modal.classList.add('active');
31 });
32
33 // Set up close handlers
34 const closeModal = () => {
35 modal.classList.remove('active');
36 setTimeout(() => {
37 modal.style.display = 'none';
38 }, 300); // Match the CSS transition time
39 };
40
41 // Close button handler
42 const closeBtn = modal.querySelector('.mxchat-modal-close');
43 if (closeBtn) {
44 closeBtn.onclick = closeModal;
45 }
46
47 // Cancel button handler
48 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
49 if (cancelBtn) {
50 cancelBtn.onclick = closeModal;
51 }
52
53 // Click outside modal to close
54 modal.onclick = (e) => {
55 if (e.target === modal) {
56 closeModal();
57 }
58 };
59
60 // Focus the textarea
61 phrasesField.focus();
62 }
63 // Live Agent Notice Dismissal Function
64 function dismissLiveAgentNotice() {
65 if (typeof jQuery !== 'undefined' && typeof mxchatLiveAgent !== 'undefined') {
66 jQuery.post(mxchatLiveAgent.ajaxurl, {
67 action: 'dismiss_live_agent_notice',
68 nonce: mxchatLiveAgent.nonce
69 }, function(response) {
70 if (response.success) {
71 jQuery('#mxchat-disabled-notice').fadeOut(300);
72 }
73 }).fail(function() {
74 // Fallback: just hide the notice if AJAX fails
75 jQuery('#mxchat-disabled-notice').fadeOut(300);
76 });
77 } else {
78 // Fallback for cases where jQuery or localized data isn't available
79 var notice = document.getElementById('mxchat-disabled-notice');
80 if (notice) {
81 notice.style.display = 'none';
82 }
83 }
84 }
85
86 // Updated mxchatOpenActionModal function to integrate with the new selector
87 function mxchatOpenActionModal(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
88 const modal = document.getElementById('mxchat-action-modal');
89 if (!modal) return;
90
91 // Get form fields
92 const actionIdField = document.getElementById('edit_action_id');
93 const labelField = document.getElementById('intent_label');
94 const phrasesField = document.getElementById('action_phrases');
95 const formActionType = document.getElementById('form_action_type');
96 const callbackGroup = document.getElementById('callback_selection_group');
97 const callbackSelect = document.getElementById('callback_function');
98 const saveButton = document.getElementById('mxchat-save-action-btn');
99 const nonceContainer = document.getElementById('action-nonce-container');
100 const thresholdSlider = document.getElementById('similarity_threshold');
101 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
102
103 // Set up modal for edit or create
104 if (isEdit) {
105 saveButton.textContent = 'Update Action';
106 formActionType.value = 'mxchat_edit_intent';
107 actionIdField.value = actionId;
108 labelField.value = label;
109 phrasesField.value = phrases;
110 callbackGroup.style.display = 'none'; // Hide callback selection when editing
111 thresholdSlider.value = threshold; // Set the current threshold value
112 thresholdDisplay.textContent = threshold + '%'; // Update display
113
114 // Remove the required attribute when editing
115 callbackSelect.removeAttribute('required');
116
117 // Update the nonce field for editing
118 nonceContainer.innerHTML = ''; // Clear existing nonce
119 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
120 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
121 }
122 } else {
123 saveButton.textContent = 'Save Action';
124 formActionType.value = 'mxchat_add_intent';
125 actionIdField.value = '';
126 labelField.value = '';
127 phrasesField.value = '';
128 callbackGroup.style.display = 'block'; // Show callback selection when creating
129 thresholdSlider.value = 85; // Default value for new actions
130 thresholdDisplay.textContent = '85%'; // Default display
131
132 // Ensure the required attribute is present when adding
133 callbackSelect.setAttribute('required', 'required');
134
135 // Update the nonce field for adding
136 nonceContainer.innerHTML = ''; // Clear existing nonce
137 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
138 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
139 }
140 }
141
142 // Show modal with animation
143 modal.style.display = 'flex';
144 requestAnimationFrame(() => {
145 modal.classList.add('active');
146 });
147
148 // Set up close handlers
149 const closeModal = () => {
150 modal.classList.remove('active');
151 setTimeout(() => {
152 modal.style.display = 'none';
153 }, 300); // Match the CSS transition time
154 };
155
156 // Close button handler
157 const closeBtn = modal.querySelector('.mxchat-modal-close');
158 if (closeBtn) {
159 closeBtn.onclick = closeModal;
160 }
161
162 // Cancel button handler
163 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
164 if (cancelBtn) {
165 cancelBtn.onclick = closeModal;
166 }
167
168 // Click outside modal to close
169 modal.onclick = (e) => {
170 if (e.target === modal) {
171 closeModal();
172 }
173 };
174
175 // Escape key to close modal
176 document.addEventListener('keydown', function(e) {
177 if (e.key === 'Escape' && modal.classList.contains('active')) {
178 closeModal();
179 }
180 }, { once: true });
181
182 // Focus the first field
183 labelField.focus();
184
185 // Dispatch an event for the action type selector to catch
186 const event = new CustomEvent('mxchatModalOpened', {
187 detail: {
188 isEdit: isEdit,
189 callbackFunction: callbackFunction || (isEdit ? callbackSelect.value : '')
190 }
191 });
192 document.dispatchEvent(event);
193
194 return closeModal; // Return close function for external use
195 }
196
197 // Initialize event listeners
198 document.addEventListener('DOMContentLoaded', () => {
199 // Set up edit button handlers for intents
200 document.querySelectorAll('.mxchat-edit-button').forEach(button => {
201 button.onclick = () => {
202 const intentId = button.dataset.intentId;
203 const phrases = button.dataset.phrases;
204 mxchatOpenEditModal(intentId, phrases);
205 };
206 });
207
208 // 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 { value: 'gpt-4.1-2025-04-14', label: 'GPT-4.1', description: 'Flagship model for complex tasks' },
937 { value: 'gpt-4o', label: 'GPT-4o', description: 'Recommended for most use cases' },
938 { value: 'gpt-4o-mini', label: 'GPT-4o Mini', description: 'Fast and lightweight' },
939 { value: 'gpt-4-turbo', label: 'GPT-4 Turbo', description: 'High-performance model' },
940 { value: 'gpt-4', label: 'GPT-4', description: 'High intelligence model' },
941 { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', description: 'Affordable and fast' },
942 ],
943 claude: [
944 { value: 'claude-opus-4-20250514', label: 'Claude 4 Opus', description: 'Most capable Claude model' },
945 { value: 'claude-sonnet-4-20250514', label: 'Claude 4 Sonnet', description: 'High performance' },
946 { value: 'claude-3-7-sonnet-20250219', label: 'Claude 3.7 Sonnet', description: 'High intelligence' },
947 { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet', description: 'Intelligent and balanced' },
948 { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus', description: 'Highly complex tasks' },
949 { value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', description: 'Balanced performance' },
950 { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', description: 'Fastest Claude model' },
951 ],
952 xai: [
953 { value: 'grok-4-0709', label: 'Grok 4', description: 'Latest flagship model - unparalleled performance in natural language, math and reasoning' }, // Added Grok-4
954 { value: 'grok-3-beta', label: 'Grok-3', description: 'Powerful model with 131K context' },
955 { value: 'grok-3-fast-beta', label: 'Grok-3 Fast', description: 'High performance with faster responses' },
956 { value: 'grok-3-mini-beta', label: 'Grok-3 Mini', description: 'Affordable model with good performance' },
957 { value: 'grok-3-mini-fast-beta', label: 'Grok-3 Mini Fast', description: 'Quick and cost-effective' },
958 { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' },
959 ],
960 deepseek: [
961 { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
962 ],
963 };
964
965 let allModels = [];
966 Object.keys(models).forEach(key => {
967 if (category === 'all' || category === key) {
968 allModels = allModels.concat(models[key]);
969 }
970 });
971
972 // Filter by search term if present
973 if (filter) {
974 const lowerFilter = filter.toLowerCase();
975 allModels = allModels.filter(model =>
976 model.label.toLowerCase().includes(lowerFilter) ||
977 model.description.toLowerCase().includes(lowerFilter)
978 );
979 }
980
981 // Create model cards
982 allModels.forEach(model => {
983 const isSelected = $modelSelect.val() === model.value;
984 const $modelCard = $(`
985 <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
986 <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
987 <div class="mxchat-model-selector-info">
988 <h4 class="mxchat-model-selector-title">${model.label}</h4>
989 <p class="mxchat-model-selector-description">${model.description}</p>
990 </div>
991 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
992 </div>
993 `);
994 $grid.append($modelCard);
995 });
996 }
997
998
999 // Helper function to get icon for each model
1000 function getModelIcon(modelValue) {
1001 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>';
1002 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>';
1003 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>';
1004 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>';
1005 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>';
1006 return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
1007 }
1008
1009 // Event handlers
1010 $modelSelectorButton.on('click', function() {
1011 $('#mxchat_model_selector_modal').show();
1012 populateModelsGrid('', 'all');
1013 });
1014
1015 $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
1016 $('#mxchat_model_selector_modal').hide();
1017 });
1018
1019 $('.mxchat-model-category-btn').on('click', function() {
1020 $('.mxchat-model-category-btn').removeClass('active');
1021 $(this).addClass('active');
1022 const category = $(this).data('category');
1023 const searchTerm = $('#mxchat_model_search_input').val();
1024 populateModelsGrid(searchTerm, category);
1025 });
1026
1027 $('#mxchat_model_search_input').on('input', function() {
1028 const searchTerm = $(this).val();
1029 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
1030 populateModelsGrid(searchTerm, activeCategory);
1031 });
1032
1033 $(document).on('click', '.mxchat-model-selector-card', function() {
1034 const modelValue = $(this).data('value');
1035 $modelSelect.val(modelValue).trigger('change');
1036 updateButtonText();
1037 $('#mxchat_model_selector_modal').hide();
1038 });
1039
1040 // Close modal when clicking outside
1041 $(window).on('click', function(event) {
1042 if ($(event.target).is('#mxchat_model_selector_modal')) {
1043 $('#mxchat_model_selector_modal').hide();
1044 }
1045 });
1046 }
1047
1048 // Embedding model selector - completely separate from chat model selector
1049 function setupMxChatEmbeddingModelSelector() {
1050 const $embeddingModelSelect = $('#embedding_model');
1051
1052 // Skip if the element doesn't exist on the page
1053 if ($embeddingModelSelect.length === 0) {
1054 return;
1055 }
1056
1057 const $embeddingModelSelectorButton = $('<button>', {
1058 type: 'button',
1059 id: 'mxchat_embedding_model_selector_btn',
1060 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
1061 text: 'Select Embedding Model'
1062 });
1063
1064 // Replace the select dropdown with a button
1065 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
1066
1067 // Update button text to show currently selected model
1068 function updateButtonText() {
1069 const selectedModel = $embeddingModelSelect.val();
1070 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
1071 $embeddingModelSelectorButton.text(selectedModelText);
1072 }
1073
1074 // Initialize button text
1075 updateButtonText();
1076
1077 // Create a unique ID for the modal to avoid conflicts
1078 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
1079
1080 // Create and append modal HTML with unique IDs
1081 const embeddingModelSelectorModal = `
1082 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
1083 <div class="mxchat-embedding-model-selector-modal-content">
1084 <div class="mxchat-embedding-model-selector-modal-header">
1085 <h3>Select Embedding Model</h3>
1086 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
1087 </div>
1088 <div class="mxchat-embedding-model-selector-modal-body">
1089 <div class="mxchat-embedding-model-selector-search-container">
1090 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
1091 </div>
1092 <div class="mxchat-embedding-model-selector-categories">
1093 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
1094 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
1095 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
1096 <button class="mxchat-embedding-model-category-btn" data-category="gemini">Google Gemini</button>
1097 </div>
1098 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
1099 </div>
1100 <div class="mxchat-embedding-model-selector-modal-footer">
1101 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
1102 </div>
1103 </div>
1104 </div>
1105 `;
1106
1107 // Use jQuery's append to ensure it doesn't clash with existing modals
1108 $('body').append(embeddingModelSelectorModal);
1109
1110 // Populate models grid
1111 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1112 const $grid = $('#mxchat_embedding_models_grid');
1113 $grid.empty();
1114
1115 // Define embedding models with descriptions and context lengths
1116 const models = {
1117 openai: [
1118 {
1119 value: 'text-embedding-3-small',
1120 label: 'TE3 Small',
1121 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1122 },
1123 {
1124 value: 'text-embedding-ada-002',
1125 label: 'Ada 2',
1126 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1127 },
1128 {
1129 value: 'text-embedding-3-large',
1130 label: 'TE3 Large',
1131 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1132 }
1133 ],
1134 voyage: [
1135 {
1136 value: 'voyage-3-large',
1137 label: 'Voyage-3 Large',
1138 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1139 }
1140 ],
1141 gemini: [
1142 {
1143 value: 'gemini-embedding-exp-03-07',
1144 label: 'Gemini Embedding',
1145 description: 'Experimental SOTA embeddings (1536 dimensions, 8K context)'
1146 }
1147 ]
1148 };
1149
1150 let allModels = [];
1151 Object.keys(models).forEach(key => {
1152 if (category === 'all' || category === key) {
1153 allModels = allModels.concat(models[key]);
1154 }
1155 });
1156
1157 // Filter by search term if present
1158 if (filter) {
1159 const lowerFilter = filter.toLowerCase();
1160 allModels = allModels.filter(model =>
1161 model.label.toLowerCase().includes(lowerFilter) ||
1162 model.description.toLowerCase().includes(lowerFilter)
1163 );
1164 }
1165
1166 // Create model cards
1167 allModels.forEach(model => {
1168 const isSelected = $embeddingModelSelect.val() === model.value;
1169 let providerClass = 'mxchat-embedding-model-provider-openai';
1170
1171 if (model.value.startsWith('voyage-')) {
1172 providerClass = 'mxchat-embedding-model-provider-voyage';
1173 } else if (model.value.startsWith('gemini-embedding-')) {
1174 providerClass = 'mxchat-embedding-model-provider-gemini';
1175 }
1176
1177 let iconHTML = '';
1178 if (model.value.startsWith('voyage-')) {
1179 iconHTML = '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>';
1180 } else if (model.value.startsWith('gemini-embedding-')) {
1181 iconHTML = '<span class="dashicons dashicons-google mxchat-embedding-model-icon-gemini"></span>';
1182 } else {
1183 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>';
1184 }
1185
1186 const $modelCard = $(`
1187 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1188 <div class="mxchat-embedding-model-selector-icon">
1189 ${iconHTML}
1190 </div>
1191 <div class="mxchat-embedding-model-selector-info">
1192 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1193 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1194 </div>
1195 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1196 </div>
1197 `);
1198
1199 $grid.append($modelCard);
1200 });
1201 }
1202
1203 // Event handlers - use namespaced events to avoid conflicts
1204 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1205 e.stopPropagation(); // Prevent event bubbling
1206 $('#' + embeddingModalId).show();
1207 populateEmbeddingModelsGrid('', 'all');
1208 });
1209
1210 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1211 e.stopPropagation(); // Prevent event bubbling
1212 $('#' + embeddingModalId).hide();
1213 });
1214
1215 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1216 e.stopPropagation(); // Prevent event bubbling
1217 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1218 $(this).addClass('active');
1219 const category = $(this).data('category');
1220 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1221 populateEmbeddingModelsGrid(searchTerm, category);
1222 });
1223
1224 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1225 const searchTerm = $(this).val();
1226 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1227 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1228 });
1229
1230 // Use a direct selector to avoid conflicts with other card elements
1231 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1232 e.stopPropagation(); // Prevent event bubbling
1233 const modelValue = $(this).data('value');
1234
1235 // Important: Only update this specific select element
1236 $embeddingModelSelect.val(modelValue);
1237
1238 // Manually trigger change only on this element
1239 const changeEvent = new Event('change', { bubbles: true });
1240 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1241
1242 // Update button text
1243 updateButtonText();
1244
1245 // Hide modal
1246 $('#' + embeddingModalId).hide();
1247 });
1248
1249 // Close modal when clicking outside - use namespaced events
1250 $(window).on('click.embeddingModelSelector', function(event) {
1251 if ($(event.target).is('#' + embeddingModalId)) {
1252 $('#' + embeddingModalId).hide();
1253 }
1254 });
1255 }
1256
1257 // Call this function after the DOM is fully loaded
1258 $(document).ready(function() {
1259 setupMxChatModelSelector();
1260 setupMxChatEmbeddingModelSelector();
1261 });
1262
1263 // Initialize API key visibility
1264 setupAPIKeyVisibility();
1265
1266 // Add Intent Form Submission
1267 $('#mxchat-add-intent-form').on('submit', function(event) {
1268 $('#mxchat-intent-loading').show();
1269 $('#mxchat-intent-loading-text').show();
1270 $(this).find('button[type="submit"]').hide();
1271 });
1272
1273 // Inline Edit Functionality
1274 $('.edit-button').on('click', function() {
1275 var row = $(this).closest('tr');
1276 row.find('.content-view, .url-view').hide();
1277 row.find('.content-edit, .url-edit').show();
1278 row.find('.edit-button').hide();
1279 row.find('.save-button').show();
1280 });
1281
1282 // Save button handler
1283 // Save button handler
1284 $('.save-button').on('click', function() {
1285 var button = $(this);
1286 var row = button.closest('tr');
1287 var id = button.data('id');
1288 var nonce = button.data('nonce'); // Get nonce from button data attribute
1289 var newContent = row.find('.content-edit').val();
1290 var newUrl = row.find('.url-edit').val();
1291
1292 //console.log('Nonce from button:', nonce); // Debug
1293
1294 button.prop('disabled', true);
1295 button.text('Saving...');
1296
1297 $.ajax({
1298 url: mxchatAdmin.ajax_url,
1299 type: 'POST',
1300 data: {
1301 action: 'mxchat_save_inline_prompt',
1302 id: id,
1303 article_content: newContent,
1304 article_url: newUrl,
1305 _ajax_nonce: nonce // Use nonce from button
1306 },
1307 success: function(response) {
1308 button.prop('disabled', false);
1309 button.text('Save');
1310
1311 if (response.success) {
1312 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1313 if (newUrl) {
1314 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank">' + newUrl + '</a>');
1315 } else {
1316 row.find('.url-view').html('N/A');
1317 }
1318
1319 row.find('.content-edit, .url-edit').hide();
1320 row.find('.content-view, .url-view').show();
1321 row.find('.save-button').hide();
1322 row.find('.edit-button').show();
1323 } else {
1324 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1325 }
1326 },
1327 error: function() {
1328 button.prop('disabled', false);
1329 button.text('Save');
1330 alert('An error occurred while saving.');
1331 }
1332 });
1333 });
1334
1335
1336 // Questions handling
1337 $('.mxchat-add-question').on('click', function () {
1338 const container = $('#mxchat-additional-questions-container');
1339 const questionCount = container.find('.mxchat-question-row').length + 4;
1340 const questionIndex = container.find('.mxchat-question-row').length;
1341
1342 const newQuestion = `
1343 <div class="mxchat-question-row">
1344 <input type="text"
1345 name="additional_popular_questions[]"
1346 placeholder="Enter Additional Popular Question ${questionCount}"
1347 class="regular-text mxchat-question-input"
1348 data-question-index="${questionIndex}" />
1349 <button type="button" class="button mxchat-remove-question"
1350 aria-label="Remove question">Remove</button>
1351 </div>
1352 `;
1353 container.append(newQuestion);
1354 });
1355
1356 $(document).on('click', '.mxchat-remove-question', function () {
1357 $(this).closest('.mxchat-question-row').remove();
1358 saveQuestions();
1359 });
1360
1361 $(document).on('change', '.mxchat-question-input', function() {
1362 saveQuestions();
1363 });
1364
1365 function saveQuestions() {
1366 const questions = [];
1367 $('.mxchat-question-input').each(function() {
1368 const value = $(this).val().trim();
1369 if (value) {
1370 questions.push(value);
1371 }
1372 });
1373
1374 const feedbackContainer = $('<div class="feedback-container"></div>');
1375 const spinner = $('<div class="saving-spinner"></div>');
1376 const successIcon = $('<div class="success-icon">✔</div>');
1377
1378 // Append feedback after the add button
1379 $('.mxchat-add-question').after(feedbackContainer);
1380 feedbackContainer.append(spinner);
1381
1382 // Save via AJAX
1383 $.ajax({
1384 url: mxchatAdmin.ajax_url,
1385 type: 'POST',
1386 data: {
1387 action: 'mxchat_save_setting',
1388 name: 'additional_popular_questions',
1389 value: JSON.stringify(questions),
1390 _ajax_nonce: mxchatAdmin.setting_nonce
1391 },
1392 success: function(response) {
1393 if (response.success) {
1394 spinner.fadeOut(200, function() {
1395 feedbackContainer.append(successIcon);
1396 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
1397 feedbackContainer.remove();
1398 });
1399 });
1400 } else {
1401 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
1402 feedbackContainer.remove();
1403 }
1404 },
1405 error: function() {
1406 alert('An error occurred while saving questions.');
1407 feedbackContainer.remove();
1408 }
1409 });
1410 }
1411
1412 // Live agent status handler
1413 const statusToggle = document.getElementById('live_agent_status');
1414 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
1415 if (statusToggle && statusText) {
1416 statusToggle.addEventListener('change', function() {
1417 // Update display text
1418 statusText.textContent = this.checked ? 'Online' : 'Offline';
1419
1420 // Send the correct on/off value to the server
1421 if (window.mxchatSaveSetting) {
1422 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
1423 }
1424 });
1425 }
1426
1427 // Function to adjust the textarea height to content
1428 function adjustTextareaHeight() {
1429 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
1430 this.style.height = this.scrollHeight + 'px'; // Expand to content height
1431 }
1432
1433 // Function to reset the textarea height to initial
1434 function resetTextareaHeight() {
1435 this.style.height = ''; // Remove inline height, reverting to CSS default
1436 }
1437
1438 // Target the specific textarea by ID
1439 var $textarea = $('#system_prompt_instructions');
1440
1441 // Bind events
1442 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
1443 .on('blur', resetTextareaHeight); // Reset on blur
1444 });
1445
1446
1447
1448 document.addEventListener('DOMContentLoaded', function() {
1449 // Check if we're on the correct page before initializing
1450 const modal = document.getElementById('mxchat-action-modal');
1451
1452 // Only initialize if the modal exists on this page
1453 if (modal) {
1454 //console.log('MXChat Action Modal JS Loaded');
1455
1456 // Initialize the action modal functionality
1457 initStepBasedActionModal();
1458 }
1459
1460 // Function to initialize the step-based action modal
1461 function initStepBasedActionModal() {
1462 // We already checked for modal existence above, so no need to check again
1463
1464 const actionStep1 = document.getElementById('mxchat-action-step-1');
1465 const actionStep2 = document.getElementById('mxchat-action-step-2');
1466 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
1467 const searchInput = document.getElementById('action-type-search');
1468 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
1469 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
1470 const actionForm = document.getElementById('mxchat-action-form');
1471 const callbackInput = document.getElementById('callback_function');
1472 const actionIdField = document.getElementById('edit_action_id');
1473 const labelField = document.getElementById('intent_label');
1474 const phrasesField = document.getElementById('action_phrases');
1475 const formActionType = document.getElementById('form_action_type');
1476 const nonceContainer = document.getElementById('action-nonce-container');
1477 const thresholdSlider = document.getElementById('similarity_threshold');
1478 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1479
1480 // Rest of your initialization code remains the same...
1481
1482 // Log the structure of one action card for debugging
1483 if (actionCards.length > 0) {
1484 //console.log('First action card data attributes:', actionCards[0].dataset);
1485 //console.log('First action card HTML:', actionCards[0].outerHTML);
1486 }
1487
1488 // Add click event listeners to category buttons
1489 categoryButtons.forEach(button => {
1490 button.addEventListener('click', function() {
1491 //console.log('Category button clicked:', this.dataset.category);
1492
1493 // Remove active class from all buttons
1494 categoryButtons.forEach(btn => btn.classList.remove('active'));
1495
1496 // Add active class to clicked button
1497 this.classList.add('active');
1498
1499 // Get selected category
1500 const category = this.dataset.category;
1501
1502 // Filter action cards
1503 filterActionCards(category, searchInput.value);
1504 });
1505 });
1506
1507 // Add search functionality
1508 if (searchInput) {
1509 searchInput.addEventListener('input', function() {
1510 // Get active category
1511 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
1512 //console.log('Search input changed, active category:', activeCategory);
1513
1514 // Filter action cards
1515 filterActionCards(activeCategory, this.value);
1516 });
1517 }
1518
1519 // Add click event listeners to action cards
1520 actionCards.forEach(card => {
1521 card.addEventListener('click', function() {
1522 // Get the action data
1523 const isPro = this.dataset.pro === 'true';
1524 const isInstalled = this.dataset.installed === 'true';
1525 const addonName = this.dataset.addon || '';
1526 const actionValue = this.dataset.value;
1527 const actionLabel = this.dataset.label;
1528 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
1529 const actionDescription = this.querySelector('p').textContent;
1530
1531 // Pro check using the proper detection method
1532 const proIsActivated = typeof mxchatAdmin !== 'undefined' &&
1533 (mxchatAdmin.is_activated === '1' ||
1534 mxchatAdmin.is_activated === 'true' ||
1535 mxchatAdmin.is_activated === true);
1536
1537 // Handle different states
1538 if (isPro && !proIsActivated) {
1539 // Pro feature but no Pro license
1540 showProFeatureNotice();
1541 return;
1542 }
1543
1544 if (addonName && !isInstalled) {
1545 // Add-on required but not installed
1546 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '') || addonName + ' Add-on';
1547 showAddonRequiredNotice(addonDisplayName);
1548 return;
1549 }
1550
1551 // If we get here, the action is available - proceed as normal
1552 callbackInput.value = actionValue;
1553
1554 // Update the selected action display in step 2
1555 document.getElementById('selected-action-title').textContent = actionLabel;
1556 document.getElementById('selected-action-description').textContent = actionDescription;
1557 document.getElementById('selected-action-icon').innerHTML =
1558 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1559
1560 // Set a default label based on the action type (user can change it)
1561 if (!labelField.value) {
1562 labelField.value = actionLabel;
1563 }
1564
1565 // Move to step 2
1566 actionStep1.classList.remove('active');
1567 actionStep2.classList.add('active');
1568
1569 // Update modal title
1570 });
1571 });
1572
1573 // Back button functionality
1574 if (backToStep1Btn) {
1575 backToStep1Btn.addEventListener('click', function() {
1576 //console.log('Back button clicked');
1577 actionStep2.classList.remove('active');
1578 actionStep1.classList.add('active');
1579 });
1580 }
1581
1582 // Function to filter action cards by category and search term
1583 function filterActionCards(category, searchTerm) {
1584 searchTerm = searchTerm.toLowerCase().trim();
1585 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
1586
1587 let visibleCount = 0;
1588
1589 // Show all cards initially with animation
1590 actionCards.forEach((card, index) => {
1591 // Reset animation
1592 card.style.animation = 'none';
1593 // Trigger reflow
1594 void card.offsetWidth;
1595
1596 // Determine if card should be visible based on category and search term
1597 const cardCategory = card.dataset.category || '';
1598 const matchesCategory = category === 'all' || cardCategory === category;
1599
1600 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
1601 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
1602 const matchesSearch = searchTerm === '' ||
1603 cardTitle.includes(searchTerm) ||
1604 cardDesc.includes(searchTerm);
1605
1606 // Show/hide card with animation
1607 if (matchesCategory && matchesSearch) {
1608 card.style.display = 'flex';
1609 // Staggered animation for cards
1610 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
1611 visibleCount++;
1612 } else {
1613 card.style.display = 'none';
1614 }
1615 });
1616
1617 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
1618 }
1619
1620 // Function to show notice for Pro features
1621 function showProFeatureNotice() {
1622 //console.log('Showing Pro feature notice');
1623 // Check if we already have a notification container
1624 let noticeContainer = document.querySelector('.mxchat-pro-notice');
1625
1626 if (!noticeContainer) {
1627 // Create the notice container
1628 noticeContainer = document.createElement('div');
1629 noticeContainer.className = 'mxchat-pro-notice';
1630
1631 // Create content
1632 noticeContainer.innerHTML = `
1633 <div class="mxchat-pro-notice-content">
1634 <h3>MxChat Pro Feature</h3>
1635 <p>This action is available in the Pro version only.</p>
1636 <div class="mxchat-pro-notice-buttons">
1637 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
1638 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
1639 </div>
1640 </div>
1641 `;
1642
1643 // Append to body
1644 document.body.appendChild(noticeContainer);
1645
1646 // Add close functionality
1647 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
1648 closeButton.addEventListener('click', function() {
1649 noticeContainer.classList.remove('active');
1650 setTimeout(() => {
1651 noticeContainer.remove();
1652 }, 300);
1653 });
1654
1655 // Click outside to close
1656 noticeContainer.addEventListener('click', function(e) {
1657 if (e.target === noticeContainer) {
1658 closeButton.click();
1659 }
1660 });
1661
1662 // Show with animation
1663 setTimeout(() => {
1664 noticeContainer.classList.add('active');
1665 }, 10);
1666 } else {
1667 // If it already exists, just make it visible again
1668 noticeContainer.classList.add('active');
1669 }
1670 }
1671
1672 // Function to show notice for add-on requirements
1673 function showAddonRequiredNotice(addonName) {
1674 //console.log(`Showing add-on notice for: ${addonName}`);
1675 // Check if we already have a notification container
1676 let noticeContainer = document.querySelector('.mxchat-addon-notice');
1677
1678 if (!noticeContainer) {
1679 // Create the notice container
1680 noticeContainer = document.createElement('div');
1681 noticeContainer.className = 'mxchat-addon-notice';
1682
1683 // Create content
1684 noticeContainer.innerHTML = `
1685 <div class="mxchat-addon-notice-content">
1686 <span class="mxchat-addon-notice-icon">🧩</span>
1687 <h3>Add-on Required</h3>
1688 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
1689 <div class="mxchat-addon-notice-buttons">
1690 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
1691 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
1692 </div>
1693 </div>
1694 `;
1695
1696 // Append to body
1697 document.body.appendChild(noticeContainer);
1698
1699 // Add close functionality
1700 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
1701 closeButton.addEventListener('click', function() {
1702 noticeContainer.classList.remove('active');
1703 setTimeout(() => {
1704 noticeContainer.remove();
1705 }, 300);
1706 });
1707
1708 // Click outside to close
1709 noticeContainer.addEventListener('click', function(e) {
1710 if (e.target === noticeContainer) {
1711 closeButton.click();
1712 }
1713 });
1714
1715 // Show with animation
1716 setTimeout(() => {
1717 noticeContainer.classList.add('active');
1718 }, 10);
1719 } else {
1720 // If it already exists, update the content
1721 const addonNameElement = noticeContainer.querySelector('p strong');
1722 if (addonNameElement) {
1723 addonNameElement.textContent = addonName;
1724 }
1725
1726 // Make it visible again
1727 noticeContainer.classList.add('active');
1728 }
1729 }
1730
1731 // Form submission handling
1732 if (actionForm) {
1733 actionForm.addEventListener('submit', function() {
1734 //console.log('Form submitted');
1735 document.getElementById('mxchat-action-loading').style.display = 'flex';
1736 this.querySelector('button[type="submit"]').disabled = true;
1737 });
1738 }
1739 }
1740
1741 // Setup add action buttons (only if we're on the correct page)
1742 if (modal) {
1743 // Update the modal open function to support the step-based flow
1744 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
1745 //console.log('Modal opening, edit mode:', isEdit);
1746
1747 // No need to check again, we already verified modal exists
1748
1749 // Get form fields
1750 const actionIdField = document.getElementById('edit_action_id');
1751 const labelField = document.getElementById('intent_label');
1752 const phrasesField = document.getElementById('action_phrases');
1753 const formActionType = document.getElementById('form_action_type');
1754 const callbackInput = document.getElementById('callback_function');
1755 const saveButton = document.getElementById('mxchat-save-action-btn');
1756 const nonceContainer = document.getElementById('action-nonce-container');
1757 const thresholdSlider = document.getElementById('similarity_threshold');
1758 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1759 const actionStep1 = document.getElementById('mxchat-action-step-1');
1760 const actionStep2 = document.getElementById('mxchat-action-step-2');
1761 const searchInput = document.getElementById('action-type-search');
1762
1763 // Set up modal for edit or create
1764 if (isEdit) {
1765 saveButton.textContent = 'Update Action';
1766 formActionType.value = 'mxchat_edit_intent';
1767 actionIdField.value = actionId;
1768 labelField.value = label;
1769 phrasesField.value = phrases;
1770 callbackInput.value = callbackFunction;
1771 thresholdSlider.value = threshold; // Set the current threshold value
1772 thresholdDisplay.textContent = threshold + '%'; // Update display
1773
1774 // Update the nonce field for editing
1775 nonceContainer.innerHTML = ''; // Clear existing nonce
1776 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
1777 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
1778 }
1779
1780 // For editing, go directly to step 2 and update the selected action display
1781 actionStep1.classList.remove('active');
1782 actionStep2.classList.add('active');
1783
1784 // Find the matching action card to get its details
1785 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
1786 let foundCard = null;
1787
1788 actionCards.forEach(card => {
1789 if (card.dataset.value === callbackFunction) {
1790 foundCard = card;
1791 }
1792 });
1793
1794 if (foundCard) {
1795 //console.log('Found matching action card for:', callbackFunction);
1796 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
1797 const actionIconElement = foundCard.querySelector('.dashicons');
1798 const actionIcon = actionIconElement
1799 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
1800 : 'admin-generic';
1801 const actionDescription = foundCard.querySelector('p')?.textContent || '';
1802
1803 document.getElementById('selected-action-title').textContent = actionLabel;
1804 document.getElementById('selected-action-description').textContent = actionDescription;
1805 document.getElementById('selected-action-icon').innerHTML =
1806 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1807 } else {
1808 //console.log('No matching action card found for:', callbackFunction);
1809 // Fallback if we can't find the card
1810 document.getElementById('selected-action-title').textContent = label;
1811 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
1812 document.getElementById('selected-action-icon').innerHTML =
1813 `<span class="dashicons dashicons-admin-generic"></span>`;
1814 }
1815 } else {
1816 //console.log('Setting up create mode');
1817 saveButton.textContent = 'Save Action';
1818 formActionType.value = 'mxchat_add_intent';
1819 actionIdField.value = '';
1820 labelField.value = '';
1821 phrasesField.value = '';
1822 callbackInput.value = '';
1823 thresholdSlider.value = 85; // Default value for new actions
1824 thresholdDisplay.textContent = '85%'; // Default display
1825
1826 // Update the nonce field for adding
1827 nonceContainer.innerHTML = ''; // Clear existing nonce
1828 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
1829 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
1830 }
1831
1832 // For creating new, start at step 1
1833 actionStep1.classList.add('active');
1834 actionStep2.classList.remove('active');
1835 }
1836
1837 // Show modal with animation
1838 modal.style.display = 'flex';
1839 requestAnimationFrame(() => {
1840 modal.classList.add('active');
1841 });
1842
1843 // Set up close handlers
1844 const closeModal = () => {
1845 //console.log('Closing modal');
1846 modal.classList.remove('active');
1847 setTimeout(() => {
1848 modal.style.display = 'none';
1849 }, 300); // Match the CSS transition time
1850 };
1851
1852 // Close button handler
1853 const closeBtn = modal.querySelector('.mxchat-modal-close');
1854 if (closeBtn) {
1855 closeBtn.onclick = closeModal;
1856 }
1857
1858 // Cancel button handler
1859 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
1860 if (cancelBtns) {
1861 cancelBtns.forEach(btn => {
1862 btn.onclick = closeModal;
1863 });
1864 }
1865
1866 // Click outside modal to close
1867 modal.onclick = (e) => {
1868 if (e.target === modal) {
1869 closeModal();
1870 }
1871 };
1872
1873 // Escape key to close modal
1874 document.addEventListener('keydown', function(e) {
1875 if (e.key === 'Escape' && modal.classList.contains('active')) {
1876 closeModal();
1877 }
1878 }, { once: true });
1879
1880 // Focus appropriate field based on current step
1881 if (isEdit || actionStep2.classList.contains('active')) {
1882 if (labelField) labelField.focus();
1883 } else {
1884 if (searchInput) searchInput.focus();
1885 }
1886
1887 return closeModal; // Return close function for external use
1888 };
1889
1890 // Setup add action buttons
1891 const addActionBtn = document.getElementById('mxchat-add-action-btn');
1892 if (addActionBtn) {
1893 //console.log('Add action button found');
1894 addActionBtn.onclick = () => window.mxchatOpenActionModal();
1895 }
1896
1897 const createFirstAction = document.getElementById('mxchat-create-first-action');
1898 if (createFirstAction) {
1899 //console.log('Create first action button found');
1900 createFirstAction.onclick = () => window.mxchatOpenActionModal();
1901 }
1902
1903 // Setup edit buttons
1904 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
1905 //console.log('Edit buttons found:', editButtons.length);
1906 editButtons.forEach(button => {
1907 button.onclick = () => {
1908 const actionId = button.dataset.actionId;
1909 const phrases = button.dataset.phrases;
1910 const label = button.dataset.label;
1911 const threshold = button.dataset.threshold || 85;
1912 const callbackFunction = button.dataset.callbackFunction;
1913
1914 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction);
1915 };
1916 });
1917 }
1918 });
1919
1920 jQuery(document).ready(function($) {
1921 // Toggle custom post types container
1922 $('#mxchat-custom-post-types-toggle').on('click', function(e) {
1923 e.preventDefault();
1924
1925 $('#mxchat-custom-post-types-container').slideToggle(300);
1926
1927 // Rotate the toggle icon
1928 const $icon = $(this).find('.mxchat-accordion-icon');
1929 if ($('#mxchat-custom-post-types-container').is(':visible')) {
1930 $icon.css('transform', 'rotate(180deg)');
1931 $(this).closest('.mxchat-settings-accordion').addClass('active');
1932 } else {
1933 $icon.css('transform', 'rotate(0deg)');
1934 $(this).closest('.mxchat-settings-accordion').removeClass('active');
1935 }
1936 });
1937
1938 // If there are any selections made, auto-expand the container
1939 function autoExpandIfNeeded() {
1940 // Check if any checkbox in the container is checked
1941 const hasCheckedItems = $('#mxchat-custom-post-types-container input[type="checkbox"]:checked').length > 0;
1942
1943 if (hasCheckedItems) {
1944 $('#mxchat-custom-post-types-container').show();
1945 $('#mxchat-custom-post-types-toggle .mxchat-accordion-icon').css('transform', 'rotate(180deg)');
1946 $('.mxchat-settings-accordion').addClass('active');
1947 }
1948 }
1949
1950 // Run on page load
1951 autoExpandIfNeeded();
1952 });
1953
1954 document.addEventListener('DOMContentLoaded', function() {
1955 var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
1956 var modal = document.getElementById('mxchatSampleModal');
1957 var modalClose = document.getElementById('mxchatModalClose');
1958 var closeBtn = document.getElementById('mxchatCloseBtn');
1959 var copyBtn = document.getElementById('mxchatCopyBtn');
1960 var instructionsContent = document.querySelector('.mxchat-instructions-content');
1961 var modalContent = document.querySelector('.mxchat-instructions-modal-content');
1962
1963 if (!viewSampleBtn || !modal) {
1964 return;
1965 }
1966
1967 // Open modal
1968 viewSampleBtn.addEventListener('click', function(e) {
1969 e.preventDefault();
1970 e.stopPropagation();
1971 modal.classList.add('mxchat-instructions-show');
1972 });
1973
1974 // Close modal function
1975 function closeModal(e) {
1976 if (e) {
1977 e.preventDefault();
1978 e.stopPropagation();
1979 }
1980 modal.classList.remove('mxchat-instructions-show');
1981 }
1982
1983 // Close modal events
1984 if (modalClose) {
1985 modalClose.addEventListener('click', function(e) {
1986 closeModal(e);
1987 });
1988 }
1989
1990 if (closeBtn) {
1991 closeBtn.addEventListener('click', function(e) {
1992 closeModal(e);
1993 });
1994 }
1995
1996 // Close on backdrop click ONLY (not on hover)
1997 modal.addEventListener('click', function(e) {
1998 // Only close if clicking directly on the overlay, not on child elements
1999 if (e.target === modal) {
2000 closeModal(e);
2001 }
2002 });
2003
2004 // Prevent modal content clicks from closing the modal
2005 if (modalContent) {
2006 modalContent.addEventListener('click', function(e) {
2007 e.stopPropagation();
2008 });
2009 }
2010
2011 // Close on escape key
2012 document.addEventListener('keydown', function(e) {
2013 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2014 closeModal();
2015 }
2016 });
2017
2018 // Copy functionality
2019 if (copyBtn && instructionsContent) {
2020 copyBtn.addEventListener('click', function(e) {
2021 e.preventDefault();
2022 e.stopPropagation();
2023
2024 var text = instructionsContent.textContent;
2025
2026 if (navigator.clipboard) {
2027 navigator.clipboard.writeText(text).then(function() {
2028 showCopySuccess();
2029 }).catch(function() {
2030 fallbackCopy(text);
2031 });
2032 } else {
2033 fallbackCopy(text);
2034 }
2035 });
2036 }
2037
2038 function fallbackCopy(text) {
2039 var textArea = document.createElement('textarea');
2040 textArea.value = text;
2041 textArea.style.position = 'fixed';
2042 textArea.style.left = '-999999px';
2043 textArea.style.top = '-999999px';
2044 document.body.appendChild(textArea);
2045 textArea.select();
2046 try {
2047 document.execCommand('copy');
2048 showCopySuccess();
2049 } catch (err) {
2050 console.error('Copy failed');
2051 }
2052 document.body.removeChild(textArea);
2053 }
2054
2055 function showCopySuccess() {
2056 var originalText = copyBtn.innerHTML;
2057 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!';
2058
2059 setTimeout(function() {
2060 copyBtn.innerHTML = originalText;
2061 }, 2000);
2062 }
2063 });