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

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