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

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

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