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

2,056 lines 94.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Simple debounce function implementation
2 function debounce(func, wait) {
3 let timeout;
4 return function executedFunction(...args) {
5 const later = () => {
6 clearTimeout(timeout);
7 func(...args);
8 };
9 clearTimeout(timeout);
10 timeout = setTimeout(later, wait);
11 };
12 }
13
14 // Helper function to open edit modal for intents/actions
15 function mxchatOpenEditModal(intentId, phrases) {
16 const modal = document.getElementById('mxchat-edit-modal');
17 if (!modal) return;
18
19 // Get form fields
20 const intentIdField = document.getElementById('edit_intent_id');
21 const phrasesField = document.getElementById('edit_phrases');
22
23 // Set values
24 intentIdField.value = intentId;
25 phrasesField.value = phrases;
26
27 // Show modal with animation
28 modal.style.display = 'flex';
29 requestAnimationFrame(() => {
30 modal.classList.add('active');
31 });
32
33 // Set up close handlers
34 const closeModal = () => {
35 modal.classList.remove('active');
36 setTimeout(() => {
37 modal.style.display = 'none';
38 }, 300); // Match the CSS transition time
39 };
40
41 // Close button handler
42 const closeBtn = modal.querySelector('.mxchat-modal-close');
43 if (closeBtn) {
44 closeBtn.onclick = closeModal;
45 }
46
47 // Cancel button handler
48 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
49 if (cancelBtn) {
50 cancelBtn.onclick = closeModal;
51 }
52
53 // Click outside modal to close
54 modal.onclick = (e) => {
55 if (e.target === modal) {
56 closeModal();
57 }
58 };
59
60 // Focus the textarea
61 phrasesField.focus();
62 }
63 // Live Agent Notice Dismissal Function
64 function dismissLiveAgentNotice() {
65 if (typeof jQuery !== 'undefined' && typeof mxchatLiveAgent !== 'undefined') {
66 jQuery.post(mxchatLiveAgent.ajaxurl, {
67 action: 'dismiss_live_agent_notice',
68 nonce: mxchatLiveAgent.nonce
69 }, function(response) {
70 if (response.success) {
71 jQuery('#mxchat-disabled-notice').fadeOut(300);
72 }
73 }).fail(function() {
74 // Fallback: just hide the notice if AJAX fails
75 jQuery('#mxchat-disabled-notice').fadeOut(300);
76 });
77 } else {
78 // Fallback for cases where jQuery or localized data isn't available
79 var notice = document.getElementById('mxchat-disabled-notice');
80 if (notice) {
81 notice.style.display = 'none';
82 }
83 }
84 }
85
86 // Updated mxchatOpenActionModal function to integrate with the new selector
87 function mxchatOpenActionModal(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
88 const modal = document.getElementById('mxchat-action-modal');
89 if (!modal) return;
90
91 // Get form fields
92 const actionIdField = document.getElementById('edit_action_id');
93 const labelField = document.getElementById('intent_label');
94 const phrasesField = document.getElementById('action_phrases');
95 const formActionType = document.getElementById('form_action_type');
96 const callbackGroup = document.getElementById('callback_selection_group');
97 const callbackSelect = document.getElementById('callback_function');
98 const saveButton = document.getElementById('mxchat-save-action-btn');
99 const nonceContainer = document.getElementById('action-nonce-container');
100 const thresholdSlider = document.getElementById('similarity_threshold');
101 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
102
103 // Set up modal for edit or create
104 if (isEdit) {
105 saveButton.textContent = 'Update Action';
106 formActionType.value = 'mxchat_edit_intent';
107 actionIdField.value = actionId;
108 labelField.value = label;
109 phrasesField.value = phrases;
110 callbackGroup.style.display = 'none'; // Hide callback selection when editing
111 thresholdSlider.value = threshold; // Set the current threshold value
112 thresholdDisplay.textContent = threshold + '%'; // Update display
113
114 // Remove the required attribute when editing
115 callbackSelect.removeAttribute('required');
116
117 // Update the nonce field for editing
118 nonceContainer.innerHTML = ''; // Clear existing nonce
119 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
120 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
121 }
122 } else {
123 saveButton.textContent = 'Save Action';
124 formActionType.value = 'mxchat_add_intent';
125 actionIdField.value = '';
126 labelField.value = '';
127 phrasesField.value = '';
128 callbackGroup.style.display = 'block'; // Show callback selection when creating
129 thresholdSlider.value = 85; // Default value for new actions
130 thresholdDisplay.textContent = '85%'; // Default display
131
132 // Ensure the required attribute is present when adding
133 callbackSelect.setAttribute('required', 'required');
134
135 // Update the nonce field for adding
136 nonceContainer.innerHTML = ''; // Clear existing nonce
137 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
138 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
139 }
140 }
141
142 // Show modal with animation
143 modal.style.display = 'flex';
144 requestAnimationFrame(() => {
145 modal.classList.add('active');
146 });
147
148 // Set up close handlers
149 const closeModal = () => {
150 modal.classList.remove('active');
151 setTimeout(() => {
152 modal.style.display = 'none';
153 }, 300); // Match the CSS transition time
154 };
155
156 // Close button handler
157 const closeBtn = modal.querySelector('.mxchat-modal-close');
158 if (closeBtn) {
159 closeBtn.onclick = closeModal;
160 }
161
162 // Cancel button handler
163 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
164 if (cancelBtn) {
165 cancelBtn.onclick = closeModal;
166 }
167
168 // Click outside modal to close
169 modal.onclick = (e) => {
170 if (e.target === modal) {
171 closeModal();
172 }
173 };
174
175 // Escape key to close modal
176 document.addEventListener('keydown', function(e) {
177 if (e.key === 'Escape' && modal.classList.contains('active')) {
178 closeModal();
179 }
180 }, { once: true });
181
182 // Focus the first field
183 labelField.focus();
184
185 // Dispatch an event for the action type selector to catch
186 const event = new CustomEvent('mxchatModalOpened', {
187 detail: {
188 isEdit: isEdit,
189 callbackFunction: callbackFunction || (isEdit ? callbackSelect.value : '')
190 }
191 });
192 document.dispatchEvent(event);
193
194 return closeModal; // Return close function for external use
195 }
196
197 // Initialize event listeners
198 document.addEventListener('DOMContentLoaded', () => {
199 // Set up edit button handlers for intents
200 document.querySelectorAll('.mxchat-edit-button').forEach(button => {
201 button.onclick = () => {
202 const intentId = button.dataset.intentId;
203 const phrases = button.dataset.phrases;
204 mxchatOpenEditModal(intentId, phrases);
205 };
206 });
207
208 // 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').on('click', function() {
1281 var button = $(this);
1282 var row = button.closest('tr');
1283 var id = button.data('id');
1284 var newContent = row.find('.content-edit').val();
1285 var newUrl = row.find('.url-edit').val();
1286
1287 button.prop('disabled', true);
1288 button.text('Saving...');
1289
1290 $.ajax({
1291 url: mxchatAdmin.ajax_url,
1292 type: 'POST',
1293 data: {
1294 action: 'mxchat_save_inline_prompt',
1295 id: id,
1296 article_content: newContent,
1297 article_url: newUrl,
1298 _ajax_nonce: mxchatAdmin.inline_edit_nonce
1299 },
1300 success: function(response) {
1301 button.prop('disabled', false);
1302 button.text('Save');
1303
1304 if (response.success) {
1305 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1306 if (newUrl) {
1307 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank">' + newUrl + '</a>');
1308 } else {
1309 row.find('.url-view').html('N/A');
1310 }
1311
1312 row.find('.content-edit, .url-edit').hide();
1313 row.find('.content-view, .url-view').show();
1314 row.find('.save-button').hide();
1315 row.find('.edit-button').show();
1316 } else {
1317 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1318 }
1319 },
1320 error: function() {
1321 button.prop('disabled', false);
1322 button.text('Save');
1323 alert('An error occurred while saving.');
1324 }
1325 });
1326 });
1327
1328
1329 // Questions handling
1330 $('.mxchat-add-question').on('click', function () {
1331 const container = $('#mxchat-additional-questions-container');
1332 const questionCount = container.find('.mxchat-question-row').length + 4;
1333 const questionIndex = container.find('.mxchat-question-row').length;
1334
1335 const newQuestion = `
1336 <div class="mxchat-question-row">
1337 <input type="text"
1338 name="additional_popular_questions[]"
1339 placeholder="Enter Additional Popular Question ${questionCount}"
1340 class="regular-text mxchat-question-input"
1341 data-question-index="${questionIndex}" />
1342 <button type="button" class="button mxchat-remove-question"
1343 aria-label="Remove question">Remove</button>
1344 </div>
1345 `;
1346 container.append(newQuestion);
1347 });
1348
1349 $(document).on('click', '.mxchat-remove-question', function () {
1350 $(this).closest('.mxchat-question-row').remove();
1351 saveQuestions();
1352 });
1353
1354 $(document).on('change', '.mxchat-question-input', function() {
1355 saveQuestions();
1356 });
1357
1358 function saveQuestions() {
1359 const questions = [];
1360 $('.mxchat-question-input').each(function() {
1361 const value = $(this).val().trim();
1362 if (value) {
1363 questions.push(value);
1364 }
1365 });
1366
1367 const feedbackContainer = $('<div class="feedback-container"></div>');
1368 const spinner = $('<div class="saving-spinner"></div>');
1369 const successIcon = $('<div class="success-icon">✔</div>');
1370
1371 // Append feedback after the add button
1372 $('.mxchat-add-question').after(feedbackContainer);
1373 feedbackContainer.append(spinner);
1374
1375 // Save via AJAX
1376 $.ajax({
1377 url: mxchatAdmin.ajax_url,
1378 type: 'POST',
1379 data: {
1380 action: 'mxchat_save_setting',
1381 name: 'additional_popular_questions',
1382 value: JSON.stringify(questions),
1383 _ajax_nonce: mxchatAdmin.setting_nonce
1384 },
1385 success: function(response) {
1386 if (response.success) {
1387 spinner.fadeOut(200, function() {
1388 feedbackContainer.append(successIcon);
1389 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
1390 feedbackContainer.remove();
1391 });
1392 });
1393 } else {
1394 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
1395 feedbackContainer.remove();
1396 }
1397 },
1398 error: function() {
1399 alert('An error occurred while saving questions.');
1400 feedbackContainer.remove();
1401 }
1402 });
1403 }
1404
1405 // Live agent status handler
1406 const statusToggle = document.getElementById('live_agent_status');
1407 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
1408 if (statusToggle && statusText) {
1409 statusToggle.addEventListener('change', function() {
1410 // Update display text
1411 statusText.textContent = this.checked ? 'Online' : 'Offline';
1412
1413 // Send the correct on/off value to the server
1414 if (window.mxchatSaveSetting) {
1415 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
1416 }
1417 });
1418 }
1419
1420 // Function to adjust the textarea height to content
1421 function adjustTextareaHeight() {
1422 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
1423 this.style.height = this.scrollHeight + 'px'; // Expand to content height
1424 }
1425
1426 // Function to reset the textarea height to initial
1427 function resetTextareaHeight() {
1428 this.style.height = ''; // Remove inline height, reverting to CSS default
1429 }
1430
1431 // Target the specific textarea by ID
1432 var $textarea = $('#system_prompt_instructions');
1433
1434 // Bind events
1435 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
1436 .on('blur', resetTextareaHeight); // Reset on blur
1437 });
1438
1439
1440
1441 document.addEventListener('DOMContentLoaded', function() {
1442 // Check if we're on the correct page before initializing
1443 const modal = document.getElementById('mxchat-action-modal');
1444
1445 // Only initialize if the modal exists on this page
1446 if (modal) {
1447 //console.log('MXChat Action Modal JS Loaded');
1448
1449 // Initialize the action modal functionality
1450 initStepBasedActionModal();
1451 }
1452
1453 // Function to initialize the step-based action modal
1454 function initStepBasedActionModal() {
1455 // We already checked for modal existence above, so no need to check again
1456
1457 const actionStep1 = document.getElementById('mxchat-action-step-1');
1458 const actionStep2 = document.getElementById('mxchat-action-step-2');
1459 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
1460 const searchInput = document.getElementById('action-type-search');
1461 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
1462 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
1463 const actionForm = document.getElementById('mxchat-action-form');
1464 const callbackInput = document.getElementById('callback_function');
1465 const actionIdField = document.getElementById('edit_action_id');
1466 const labelField = document.getElementById('intent_label');
1467 const phrasesField = document.getElementById('action_phrases');
1468 const formActionType = document.getElementById('form_action_type');
1469 const nonceContainer = document.getElementById('action-nonce-container');
1470 const thresholdSlider = document.getElementById('similarity_threshold');
1471 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1472
1473 // Rest of your initialization code remains the same...
1474
1475 // Log the structure of one action card for debugging
1476 if (actionCards.length > 0) {
1477 //console.log('First action card data attributes:', actionCards[0].dataset);
1478 //console.log('First action card HTML:', actionCards[0].outerHTML);
1479 }
1480
1481 // Add click event listeners to category buttons
1482 categoryButtons.forEach(button => {
1483 button.addEventListener('click', function() {
1484 //console.log('Category button clicked:', this.dataset.category);
1485
1486 // Remove active class from all buttons
1487 categoryButtons.forEach(btn => btn.classList.remove('active'));
1488
1489 // Add active class to clicked button
1490 this.classList.add('active');
1491
1492 // Get selected category
1493 const category = this.dataset.category;
1494
1495 // Filter action cards
1496 filterActionCards(category, searchInput.value);
1497 });
1498 });
1499
1500 // Add search functionality
1501 if (searchInput) {
1502 searchInput.addEventListener('input', function() {
1503 // Get active category
1504 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
1505 //console.log('Search input changed, active category:', activeCategory);
1506
1507 // Filter action cards
1508 filterActionCards(activeCategory, this.value);
1509 });
1510 }
1511
1512 // Add click event listeners to action cards
1513 actionCards.forEach(card => {
1514 card.addEventListener('click', function() {
1515 // Get the action data
1516 const isPro = this.dataset.pro === 'true';
1517 const isInstalled = this.dataset.installed === 'true';
1518 const addonName = this.dataset.addon || '';
1519 const actionValue = this.dataset.value;
1520 const actionLabel = this.dataset.label;
1521 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
1522 const actionDescription = this.querySelector('p').textContent;
1523
1524 // Pro check using the proper detection method
1525 const proIsActivated = typeof mxchatAdmin !== 'undefined' &&
1526 (mxchatAdmin.is_activated === '1' ||
1527 mxchatAdmin.is_activated === 'true' ||
1528 mxchatAdmin.is_activated === true);
1529
1530 // Handle different states
1531 if (isPro && !proIsActivated) {
1532 // Pro feature but no Pro license
1533 showProFeatureNotice();
1534 return;
1535 }
1536
1537 if (addonName && !isInstalled) {
1538 // Add-on required but not installed
1539 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '') || addonName + ' Add-on';
1540 showAddonRequiredNotice(addonDisplayName);
1541 return;
1542 }
1543
1544 // If we get here, the action is available - proceed as normal
1545 callbackInput.value = actionValue;
1546
1547 // Update the selected action display in step 2
1548 document.getElementById('selected-action-title').textContent = actionLabel;
1549 document.getElementById('selected-action-description').textContent = actionDescription;
1550 document.getElementById('selected-action-icon').innerHTML =
1551 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1552
1553 // Set a default label based on the action type (user can change it)
1554 if (!labelField.value) {
1555 labelField.value = actionLabel;
1556 }
1557
1558 // Move to step 2
1559 actionStep1.classList.remove('active');
1560 actionStep2.classList.add('active');
1561
1562 // Update modal title
1563 });
1564 });
1565
1566 // Back button functionality
1567 if (backToStep1Btn) {
1568 backToStep1Btn.addEventListener('click', function() {
1569 //console.log('Back button clicked');
1570 actionStep2.classList.remove('active');
1571 actionStep1.classList.add('active');
1572 });
1573 }
1574
1575 // Function to filter action cards by category and search term
1576 function filterActionCards(category, searchTerm) {
1577 searchTerm = searchTerm.toLowerCase().trim();
1578 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
1579
1580 let visibleCount = 0;
1581
1582 // Show all cards initially with animation
1583 actionCards.forEach((card, index) => {
1584 // Reset animation
1585 card.style.animation = 'none';
1586 // Trigger reflow
1587 void card.offsetWidth;
1588
1589 // Determine if card should be visible based on category and search term
1590 const cardCategory = card.dataset.category || '';
1591 const matchesCategory = category === 'all' || cardCategory === category;
1592
1593 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
1594 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
1595 const matchesSearch = searchTerm === '' ||
1596 cardTitle.includes(searchTerm) ||
1597 cardDesc.includes(searchTerm);
1598
1599 // Show/hide card with animation
1600 if (matchesCategory && matchesSearch) {
1601 card.style.display = 'flex';
1602 // Staggered animation for cards
1603 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
1604 visibleCount++;
1605 } else {
1606 card.style.display = 'none';
1607 }
1608 });
1609
1610 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
1611 }
1612
1613 // Function to show notice for Pro features
1614 function showProFeatureNotice() {
1615 //console.log('Showing Pro feature notice');
1616 // Check if we already have a notification container
1617 let noticeContainer = document.querySelector('.mxchat-pro-notice');
1618
1619 if (!noticeContainer) {
1620 // Create the notice container
1621 noticeContainer = document.createElement('div');
1622 noticeContainer.className = 'mxchat-pro-notice';
1623
1624 // Create content
1625 noticeContainer.innerHTML = `
1626 <div class="mxchat-pro-notice-content">
1627 <h3>MxChat Pro Feature</h3>
1628 <p>This action is available in the Pro version only.</p>
1629 <div class="mxchat-pro-notice-buttons">
1630 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
1631 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
1632 </div>
1633 </div>
1634 `;
1635
1636 // Append to body
1637 document.body.appendChild(noticeContainer);
1638
1639 // Add close functionality
1640 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
1641 closeButton.addEventListener('click', function() {
1642 noticeContainer.classList.remove('active');
1643 setTimeout(() => {
1644 noticeContainer.remove();
1645 }, 300);
1646 });
1647
1648 // Click outside to close
1649 noticeContainer.addEventListener('click', function(e) {
1650 if (e.target === noticeContainer) {
1651 closeButton.click();
1652 }
1653 });
1654
1655 // Show with animation
1656 setTimeout(() => {
1657 noticeContainer.classList.add('active');
1658 }, 10);
1659 } else {
1660 // If it already exists, just make it visible again
1661 noticeContainer.classList.add('active');
1662 }
1663 }
1664
1665 // Function to show notice for add-on requirements
1666 function showAddonRequiredNotice(addonName) {
1667 //console.log(`Showing add-on notice for: ${addonName}`);
1668 // Check if we already have a notification container
1669 let noticeContainer = document.querySelector('.mxchat-addon-notice');
1670
1671 if (!noticeContainer) {
1672 // Create the notice container
1673 noticeContainer = document.createElement('div');
1674 noticeContainer.className = 'mxchat-addon-notice';
1675
1676 // Create content
1677 noticeContainer.innerHTML = `
1678 <div class="mxchat-addon-notice-content">
1679 <span class="mxchat-addon-notice-icon">🧩</span>
1680 <h3>Add-on Required</h3>
1681 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
1682 <div class="mxchat-addon-notice-buttons">
1683 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
1684 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
1685 </div>
1686 </div>
1687 `;
1688
1689 // Append to body
1690 document.body.appendChild(noticeContainer);
1691
1692 // Add close functionality
1693 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
1694 closeButton.addEventListener('click', function() {
1695 noticeContainer.classList.remove('active');
1696 setTimeout(() => {
1697 noticeContainer.remove();
1698 }, 300);
1699 });
1700
1701 // Click outside to close
1702 noticeContainer.addEventListener('click', function(e) {
1703 if (e.target === noticeContainer) {
1704 closeButton.click();
1705 }
1706 });
1707
1708 // Show with animation
1709 setTimeout(() => {
1710 noticeContainer.classList.add('active');
1711 }, 10);
1712 } else {
1713 // If it already exists, update the content
1714 const addonNameElement = noticeContainer.querySelector('p strong');
1715 if (addonNameElement) {
1716 addonNameElement.textContent = addonName;
1717 }
1718
1719 // Make it visible again
1720 noticeContainer.classList.add('active');
1721 }
1722 }
1723
1724 // Form submission handling
1725 if (actionForm) {
1726 actionForm.addEventListener('submit', function() {
1727 //console.log('Form submitted');
1728 document.getElementById('mxchat-action-loading').style.display = 'flex';
1729 this.querySelector('button[type="submit"]').disabled = true;
1730 });
1731 }
1732 }
1733
1734 // Setup add action buttons (only if we're on the correct page)
1735 if (modal) {
1736 // Update the modal open function to support the step-based flow
1737 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
1738 //console.log('Modal opening, edit mode:', isEdit);
1739
1740 // No need to check again, we already verified modal exists
1741
1742 // Get form fields
1743 const actionIdField = document.getElementById('edit_action_id');
1744 const labelField = document.getElementById('intent_label');
1745 const phrasesField = document.getElementById('action_phrases');
1746 const formActionType = document.getElementById('form_action_type');
1747 const callbackInput = document.getElementById('callback_function');
1748 const saveButton = document.getElementById('mxchat-save-action-btn');
1749 const nonceContainer = document.getElementById('action-nonce-container');
1750 const thresholdSlider = document.getElementById('similarity_threshold');
1751 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1752 const actionStep1 = document.getElementById('mxchat-action-step-1');
1753 const actionStep2 = document.getElementById('mxchat-action-step-2');
1754 const searchInput = document.getElementById('action-type-search');
1755
1756 // Set up modal for edit or create
1757 if (isEdit) {
1758 saveButton.textContent = 'Update Action';
1759 formActionType.value = 'mxchat_edit_intent';
1760 actionIdField.value = actionId;
1761 labelField.value = label;
1762 phrasesField.value = phrases;
1763 callbackInput.value = callbackFunction;
1764 thresholdSlider.value = threshold; // Set the current threshold value
1765 thresholdDisplay.textContent = threshold + '%'; // Update display
1766
1767 // Update the nonce field for editing
1768 nonceContainer.innerHTML = ''; // Clear existing nonce
1769 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
1770 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
1771 }
1772
1773 // For editing, go directly to step 2 and update the selected action display
1774 actionStep1.classList.remove('active');
1775 actionStep2.classList.add('active');
1776
1777 // Find the matching action card to get its details
1778 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
1779 let foundCard = null;
1780
1781 actionCards.forEach(card => {
1782 if (card.dataset.value === callbackFunction) {
1783 foundCard = card;
1784 }
1785 });
1786
1787 if (foundCard) {
1788 //console.log('Found matching action card for:', callbackFunction);
1789 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
1790 const actionIconElement = foundCard.querySelector('.dashicons');
1791 const actionIcon = actionIconElement
1792 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
1793 : 'admin-generic';
1794 const actionDescription = foundCard.querySelector('p')?.textContent || '';
1795
1796 document.getElementById('selected-action-title').textContent = actionLabel;
1797 document.getElementById('selected-action-description').textContent = actionDescription;
1798 document.getElementById('selected-action-icon').innerHTML =
1799 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1800 } else {
1801 //console.log('No matching action card found for:', callbackFunction);
1802 // Fallback if we can't find the card
1803 document.getElementById('selected-action-title').textContent = label;
1804 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
1805 document.getElementById('selected-action-icon').innerHTML =
1806 `<span class="dashicons dashicons-admin-generic"></span>`;
1807 }
1808 } else {
1809 //console.log('Setting up create mode');
1810 saveButton.textContent = 'Save Action';
1811 formActionType.value = 'mxchat_add_intent';
1812 actionIdField.value = '';
1813 labelField.value = '';
1814 phrasesField.value = '';
1815 callbackInput.value = '';
1816 thresholdSlider.value = 85; // Default value for new actions
1817 thresholdDisplay.textContent = '85%'; // Default display
1818
1819 // Update the nonce field for adding
1820 nonceContainer.innerHTML = ''; // Clear existing nonce
1821 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
1822 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
1823 }
1824
1825 // For creating new, start at step 1
1826 actionStep1.classList.add('active');
1827 actionStep2.classList.remove('active');
1828 }
1829
1830 // Show modal with animation
1831 modal.style.display = 'flex';
1832 requestAnimationFrame(() => {
1833 modal.classList.add('active');
1834 });
1835
1836 // Set up close handlers
1837 const closeModal = () => {
1838 //console.log('Closing modal');
1839 modal.classList.remove('active');
1840 setTimeout(() => {
1841 modal.style.display = 'none';
1842 }, 300); // Match the CSS transition time
1843 };
1844
1845 // Close button handler
1846 const closeBtn = modal.querySelector('.mxchat-modal-close');
1847 if (closeBtn) {
1848 closeBtn.onclick = closeModal;
1849 }
1850
1851 // Cancel button handler
1852 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
1853 if (cancelBtns) {
1854 cancelBtns.forEach(btn => {
1855 btn.onclick = closeModal;
1856 });
1857 }
1858
1859 // Click outside modal to close
1860 modal.onclick = (e) => {
1861 if (e.target === modal) {
1862 closeModal();
1863 }
1864 };
1865
1866 // Escape key to close modal
1867 document.addEventListener('keydown', function(e) {
1868 if (e.key === 'Escape' && modal.classList.contains('active')) {
1869 closeModal();
1870 }
1871 }, { once: true });
1872
1873 // Focus appropriate field based on current step
1874 if (isEdit || actionStep2.classList.contains('active')) {
1875 if (labelField) labelField.focus();
1876 } else {
1877 if (searchInput) searchInput.focus();
1878 }
1879
1880 return closeModal; // Return close function for external use
1881 };
1882
1883 // Setup add action buttons
1884 const addActionBtn = document.getElementById('mxchat-add-action-btn');
1885 if (addActionBtn) {
1886 //console.log('Add action button found');
1887 addActionBtn.onclick = () => window.mxchatOpenActionModal();
1888 }
1889
1890 const createFirstAction = document.getElementById('mxchat-create-first-action');
1891 if (createFirstAction) {
1892 //console.log('Create first action button found');
1893 createFirstAction.onclick = () => window.mxchatOpenActionModal();
1894 }
1895
1896 // Setup edit buttons
1897 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
1898 //console.log('Edit buttons found:', editButtons.length);
1899 editButtons.forEach(button => {
1900 button.onclick = () => {
1901 const actionId = button.dataset.actionId;
1902 const phrases = button.dataset.phrases;
1903 const label = button.dataset.label;
1904 const threshold = button.dataset.threshold || 85;
1905 const callbackFunction = button.dataset.callbackFunction;
1906
1907 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction);
1908 };
1909 });
1910 }
1911 });
1912
1913 jQuery(document).ready(function($) {
1914 // Toggle custom post types container
1915 $('#mxchat-custom-post-types-toggle').on('click', function(e) {
1916 e.preventDefault();
1917
1918 $('#mxchat-custom-post-types-container').slideToggle(300);
1919
1920 // Rotate the toggle icon
1921 const $icon = $(this).find('.mxchat-accordion-icon');
1922 if ($('#mxchat-custom-post-types-container').is(':visible')) {
1923 $icon.css('transform', 'rotate(180deg)');
1924 $(this).closest('.mxchat-settings-accordion').addClass('active');
1925 } else {
1926 $icon.css('transform', 'rotate(0deg)');
1927 $(this).closest('.mxchat-settings-accordion').removeClass('active');
1928 }
1929 });
1930
1931 // If there are any selections made, auto-expand the container
1932 function autoExpandIfNeeded() {
1933 // Check if any checkbox in the container is checked
1934 const hasCheckedItems = $('#mxchat-custom-post-types-container input[type="checkbox"]:checked').length > 0;
1935
1936 if (hasCheckedItems) {
1937 $('#mxchat-custom-post-types-container').show();
1938 $('#mxchat-custom-post-types-toggle .mxchat-accordion-icon').css('transform', 'rotate(180deg)');
1939 $('.mxchat-settings-accordion').addClass('active');
1940 }
1941 }
1942
1943 // Run on page load
1944 autoExpandIfNeeded();
1945 });
1946
1947 document.addEventListener('DOMContentLoaded', function() {
1948 var viewSampleBtn = document.getElementById('mxchatViewSampleBtn');
1949 var modal = document.getElementById('mxchatSampleModal');
1950 var modalClose = document.getElementById('mxchatModalClose');
1951 var closeBtn = document.getElementById('mxchatCloseBtn');
1952 var copyBtn = document.getElementById('mxchatCopyBtn');
1953 var instructionsContent = document.querySelector('.mxchat-instructions-content');
1954 var modalContent = document.querySelector('.mxchat-instructions-modal-content');
1955
1956 if (!viewSampleBtn || !modal) {
1957 return;
1958 }
1959
1960 // Open modal
1961 viewSampleBtn.addEventListener('click', function(e) {
1962 e.preventDefault();
1963 e.stopPropagation();
1964 modal.classList.add('mxchat-instructions-show');
1965 });
1966
1967 // Close modal function
1968 function closeModal(e) {
1969 if (e) {
1970 e.preventDefault();
1971 e.stopPropagation();
1972 }
1973 modal.classList.remove('mxchat-instructions-show');
1974 }
1975
1976 // Close modal events
1977 if (modalClose) {
1978 modalClose.addEventListener('click', function(e) {
1979 closeModal(e);
1980 });
1981 }
1982
1983 if (closeBtn) {
1984 closeBtn.addEventListener('click', function(e) {
1985 closeModal(e);
1986 });
1987 }
1988
1989 // Close on backdrop click ONLY (not on hover)
1990 modal.addEventListener('click', function(e) {
1991 // Only close if clicking directly on the overlay, not on child elements
1992 if (e.target === modal) {
1993 closeModal(e);
1994 }
1995 });
1996
1997 // Prevent modal content clicks from closing the modal
1998 if (modalContent) {
1999 modalContent.addEventListener('click', function(e) {
2000 e.stopPropagation();
2001 });
2002 }
2003
2004 // Close on escape key
2005 document.addEventListener('keydown', function(e) {
2006 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) {
2007 closeModal();
2008 }
2009 });
2010
2011 // Copy functionality
2012 if (copyBtn && instructionsContent) {
2013 copyBtn.addEventListener('click', function(e) {
2014 e.preventDefault();
2015 e.stopPropagation();
2016
2017 var text = instructionsContent.textContent;
2018
2019 if (navigator.clipboard) {
2020 navigator.clipboard.writeText(text).then(function() {
2021 showCopySuccess();
2022 }).catch(function() {
2023 fallbackCopy(text);
2024 });
2025 } else {
2026 fallbackCopy(text);
2027 }
2028 });
2029 }
2030
2031 function fallbackCopy(text) {
2032 var textArea = document.createElement('textarea');
2033 textArea.value = text;
2034 textArea.style.position = 'fixed';
2035 textArea.style.left = '-999999px';
2036 textArea.style.top = '-999999px';
2037 document.body.appendChild(textArea);
2038 textArea.select();
2039 try {
2040 document.execCommand('copy');
2041 showCopySuccess();
2042 } catch (err) {
2043 console.error('Copy failed');
2044 }
2045 document.body.removeChild(textArea);
2046 }
2047
2048 function showCopySuccess() {
2049 var originalText = copyBtn.innerHTML;
2050 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!';
2051
2052 setTimeout(function() {
2053 copyBtn.innerHTML = originalText;
2054 }, 2000);
2055 }
2056 });