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

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

2,062 lines 95.4 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 // Removed localStorage saving functionality
610 } else {
611 //console.warn('Tab content #' + tabId + ' not found');
612 }
613 });
614 }
615
616 // Initialize tabs and handle events
617 initTabs();
618 $(document).on('widget-added widget-updated postbox-toggled', initTabs);
619
620 // Always activate the first tab (Chatbot)
621 $('.mxchat-tab-button').first().trigger('click.mxchat');
622
623 // Attach edit modal event handler
624 $(document).on('click', '.mxchat-edit-button', function() {
625 const intentId = $(this).data('intent-id');
626 const phrases = $(this).data('phrases');
627 mxchatOpenEditModal(intentId, phrases);
628 });
629
630 // Toggle visibility handlers
631 function toggleVisibility(selector) {
632 $(selector).on('click', function() {
633 var inputField = $(this).prev('input');
634 if (inputField.attr('type') === 'password') {
635 inputField.attr('type', 'text');
636 $(this).text('Hide');
637 } else {
638 inputField.attr('type', 'password');
639 $(this).text('Show');
640 }
641 });
642 }
643
644 // Initialize all toggle visibility buttons
645 [
646 '#toggleApiKeyVisibility',
647 '#toggleWooCommerceSecretVisibility',
648 '#toggleVoyageAPIKeyVisibility',
649 '#toggleLoopsApiKeyVisibility',
650 '#toggleXaiApiKeyVisibility',
651 '#toggleClaudeApiKeyVisibility',
652 '#toggleBraveApiKeyVisibility',
653 '#toggleWebhookUrlVisibility',
654 '#toggleSecretKeyVisibility',
655 '#toggleBotTokenVisibility',
656 '#toggleDeepSeekApiKeyVisibility',
657 '#toggleGeminiApiKeyVisibility' // Added Gemini toggle
658 ].forEach(toggleVisibility);
659
660 // Handle API key visibility based on model selection
661 function setupAPIKeyVisibility() {
662 // Cache the selectors
663 const $chatModelSelect = $('#model');
664 const $embeddingModelSelect = $('#embedding_model');
665
666 // First, locate and mark the API key rows
667 setupAPIKeyRows();
668
669 // Initial setup based on current selections
670 updateApiKeyVisibility();
671
672 // Listen for changes to the model selectors
673 $chatModelSelect.on('change', updateApiKeyVisibility);
674 $embeddingModelSelect.on('change', updateApiKeyVisibility);
675
676 /**
677 * Locate and mark rows that contain API key fields
678 */
679 function setupAPIKeyRows() {
680 // Find key rows by their field IDs
681 const providerMap = {
682 'api_key': 'openai',
683 'xai_api_key': 'xai',
684 'claude_api_key': 'claude',
685 'deepseek_api_key': 'deepseek',
686 'voyage_api_key': 'voyage',
687 'gemini_api_key': 'gemini' // Added Gemini API key mapping
688 };
689
690 $.each(providerMap, function(fieldId, provider) {
691 const $field = $('#' + fieldId);
692 if ($field.length) {
693 const $row = $field.closest('tr');
694 $row.addClass('mxchat-setting-row');
695 $row.attr('data-provider', provider);
696 }
697 });
698 }
699
700 /**
701 * Updates the visibility of API key fields based on current model selections
702 */
703 function updateApiKeyVisibility() {
704 const chatModel = $chatModelSelect.val();
705 const embeddingModel = $embeddingModelSelect.val();
706
707 // Determine which providers are needed
708 const isOpenAIChat = chatModel && chatModel.startsWith('gpt-');
709 const isXAI = chatModel && chatModel.startsWith('grok-');
710 const isClaude = chatModel && chatModel.startsWith('claude-');
711 const isDeepSeek = chatModel && chatModel.startsWith('deepseek-');
712 const isGemini = chatModel && chatModel.startsWith('gemini-'); // Added Gemini detection
713
714 const isOpenAIEmbedding = embeddingModel && embeddingModel.startsWith('text-embedding-');
715 const isVoyage = embeddingModel && embeddingModel.startsWith('voyage-');
716
717 // Update API key visibility for each provider
718 updateWrapperVisibility('openai', isOpenAIChat || isOpenAIEmbedding);
719 updateWrapperVisibility('xai', isXAI);
720 updateWrapperVisibility('claude', isClaude);
721 updateWrapperVisibility('deepseek', isDeepSeek);
722 updateWrapperVisibility('voyage', isVoyage);
723 updateWrapperVisibility('gemini', isGemini); // Added Gemini visibility update
724
725 // Update provider-specific notices for OpenAI
726 if (isOpenAIChat && isOpenAIEmbedding) {
727 $('div[data-provider="openai"] .api-key-notice').text(
728 'Required for your selected chat model and embedding model. Important: You must add credits before use.'
729 );
730 } else if (isOpenAIChat) {
731 $('div[data-provider="openai"] .api-key-notice').text(
732 'Required for your selected chat model. Important: You must add credits before use.'
733 );
734 } else if (isOpenAIEmbedding) {
735 $('div[data-provider="openai"] .api-key-notice').text(
736 'Required for your selected embedding model. Important: You must add credits before use.'
737 );
738 }
739 }
740
741 /**
742 * Updates visibility of a specific provider's API key wrapper
743 */
744 function updateWrapperVisibility(provider, isVisible) {
745 const $row = $('tr.mxchat-setting-row[data-provider="' + provider + '"]');
746
747 if (!$row.length) {
748 //console.warn('API key row not found for provider: ' + provider);
749 return;
750 }
751
752 if (isVisible) {
753 $row.show();
754 if (!$row.hasClass('highlighted')) {
755 $row.addClass('highlighted');
756 setTimeout(() => {
757 $row.removeClass('highlighted');
758 }, 1500);
759 }
760 } else {
761 $row.hide();
762 }
763 }
764 }
765
766 // Add this to your JavaScript file
767 function setupMxChatModelSelector() {
768 const $modelSelect = $('#model');
769 const $modelSelectorButton = $('<button>', {
770 type: 'button',
771 id: 'mxchat_model_selector_btn',
772 class: 'button-primary mxchat-model-selector-btn',
773 text: 'Select AI Model'
774 });
775
776 // Replace the select dropdown with a button
777 $modelSelect.hide().after($modelSelectorButton);
778
779 // Update button text to show currently selected model
780 function updateButtonText() {
781 const selectedModel = $modelSelect.val();
782 const selectedModelText = $modelSelect.find('option:selected').text();
783 $modelSelectorButton.text(selectedModelText);
784 }
785
786 // Initialize button text
787 updateButtonText();
788
789 // Create and append modal HTML
790 const modelSelectorModal = `
791 <div id="mxchat_model_selector_modal" class="mxchat-model-selector-modal">
792 <div class="mxchat-model-selector-modal-content">
793 <div class="mxchat-model-selector-modal-header">
794 <h3>Select AI Model</h3>
795 <span class="mxchat-model-selector-modal-close">&times;</span>
796 </div>
797 <div class="mxchat-model-selector-modal-body">
798 <div class="mxchat-model-selector-search-container">
799 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
800 </div>
801 <div class="mxchat-model-selector-categories">
802 <button class="mxchat-model-category-btn active" data-category="all">All</button>
803 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
804 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
805 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
806 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
807 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
808 </div>
809 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
810 </div>
811 <div class="mxchat-model-selector-modal-footer">
812 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
813 </div>
814 </div>
815 </div>
816 `;
817
818 $('body').append(modelSelectorModal);
819
820 // Populate models grid
821 function populateModelsGrid(filter = '', category = 'all') {
822 const $grid = $('#mxchat_models_grid');
823 $grid.empty();
824
825 const models = {
826 gemini: [
827 { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Next-Gen features, speed & multimodal generation' },
828 { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Cost-efficient with low latency' },
829 { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Complex reasoning tasks requiring more intelligence' },
830 { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Fast and versatile performance' },
831 ],
832 openai: [
833 { value: 'gpt-4.1-2025-04-14', label: 'GPT-4.1', description: 'Flagship model for complex tasks' },
834 { value: 'gpt-4o', label: 'GPT-4o', description: 'Recommended for most use cases' },
835 { value: 'gpt-4o-mini', label: 'GPT-4o Mini', description: 'Fast and lightweight' },
836 { value: 'gpt-4-turbo', label: 'GPT-4 Turbo', description: 'High-performance model' },
837 { value: 'gpt-4', label: 'GPT-4', description: 'High intelligence model' },
838 { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', description: 'Affordable and fast' },
839 ],
840 claude: [
841 { value: 'claude-3-7-sonnet-20250219', label: 'Claude 3.7 Sonnet', description: 'Most intelligent Claude model' },
842 { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet', description: 'Intelligent and balanced' },
843 { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus', description: 'Highly complex tasks' },
844 { value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', description: 'Balanced performance' },
845 { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', description: 'Fastest Claude model' },
846 ],
847 xai: [
848 { value: 'grok-3-beta', label: 'Grok-3', description: 'Powerful model with 131K context' },
849 { value: 'grok-3-fast-beta', label: 'Grok-3 Fast', description: 'High performance with faster responses' },
850 { value: 'grok-3-mini-beta', label: 'Grok-3 Mini', description: 'Affordable model with good performance' },
851 { value: 'grok-3-mini-fast-beta', label: 'Grok-3 Mini Fast', description: 'Quick and cost-effective' },
852 { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' },
853 ],
854 deepseek: [
855 { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
856 ],
857 };
858
859 let allModels = [];
860 Object.keys(models).forEach(key => {
861 if (category === 'all' || category === key) {
862 allModels = allModels.concat(models[key]);
863 }
864 });
865
866 // Filter by search term if present
867 if (filter) {
868 const lowerFilter = filter.toLowerCase();
869 allModels = allModels.filter(model =>
870 model.label.toLowerCase().includes(lowerFilter) ||
871 model.description.toLowerCase().includes(lowerFilter)
872 );
873 }
874
875 // Create model cards
876 allModels.forEach(model => {
877 const isSelected = $modelSelect.val() === model.value;
878 const $modelCard = $(`
879 <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
880 <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
881 <div class="mxchat-model-selector-info">
882 <h4 class="mxchat-model-selector-title">${model.label}</h4>
883 <p class="mxchat-model-selector-description">${model.description}</p>
884 </div>
885 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
886 </div>
887 `);
888 $grid.append($modelCard);
889 });
890 }
891
892 // Helper function to get icon for each model
893 function getModelIcon(modelValue) {
894 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>';
895 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>';
896 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>';
897 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>';
898 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>';
899 return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
900 }
901
902 // Event handlers
903 $modelSelectorButton.on('click', function() {
904 $('#mxchat_model_selector_modal').show();
905 populateModelsGrid('', 'all');
906 });
907
908 $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
909 $('#mxchat_model_selector_modal').hide();
910 });
911
912 $('.mxchat-model-category-btn').on('click', function() {
913 $('.mxchat-model-category-btn').removeClass('active');
914 $(this).addClass('active');
915 const category = $(this).data('category');
916 const searchTerm = $('#mxchat_model_search_input').val();
917 populateModelsGrid(searchTerm, category);
918 });
919
920 $('#mxchat_model_search_input').on('input', function() {
921 const searchTerm = $(this).val();
922 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
923 populateModelsGrid(searchTerm, activeCategory);
924 });
925
926 $(document).on('click', '.mxchat-model-selector-card', function() {
927 const modelValue = $(this).data('value');
928 $modelSelect.val(modelValue).trigger('change');
929 updateButtonText();
930 $('#mxchat_model_selector_modal').hide();
931 });
932
933 // Close modal when clicking outside
934 $(window).on('click', function(event) {
935 if ($(event.target).is('#mxchat_model_selector_modal')) {
936 $('#mxchat_model_selector_modal').hide();
937 }
938 });
939 }
940
941 // Embedding model selector - completely separate from chat model selector
942 function setupMxChatEmbeddingModelSelector() {
943 const $embeddingModelSelect = $('#embedding_model');
944
945 // Skip if the element doesn't exist on the page
946 if ($embeddingModelSelect.length === 0) {
947 return;
948 }
949
950 const $embeddingModelSelectorButton = $('<button>', {
951 type: 'button',
952 id: 'mxchat_embedding_model_selector_btn',
953 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
954 text: 'Select Embedding Model'
955 });
956
957 // Replace the select dropdown with a button
958 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
959
960 // Update button text to show currently selected model
961 function updateButtonText() {
962 const selectedModel = $embeddingModelSelect.val();
963 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
964 $embeddingModelSelectorButton.text(selectedModelText);
965 }
966
967 // Initialize button text
968 updateButtonText();
969
970 // Create a unique ID for the modal to avoid conflicts
971 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
972
973 // Create and append modal HTML with unique IDs
974 const embeddingModelSelectorModal = `
975 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
976 <div class="mxchat-embedding-model-selector-modal-content">
977 <div class="mxchat-embedding-model-selector-modal-header">
978 <h3>Select Embedding Model</h3>
979 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
980 </div>
981 <div class="mxchat-embedding-model-selector-modal-body">
982 <div class="mxchat-embedding-model-selector-search-container">
983 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
984 </div>
985 <div class="mxchat-embedding-model-selector-categories">
986 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
987 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
988 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
989 </div>
990 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
991 </div>
992 <div class="mxchat-embedding-model-selector-modal-footer">
993 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
994 </div>
995 </div>
996 </div>
997 `;
998
999 // Use jQuery's append to ensure it doesn't clash with existing modals
1000 $('body').append(embeddingModelSelectorModal);
1001
1002 // Populate models grid
1003 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1004 const $grid = $('#mxchat_embedding_models_grid');
1005 $grid.empty();
1006
1007 // Define embedding models with descriptions and context lengths
1008 const models = {
1009 openai: [
1010 {
1011 value: 'text-embedding-3-small',
1012 label: 'TE3 Small',
1013 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1014 },
1015 {
1016 value: 'text-embedding-ada-002',
1017 label: 'Ada 2',
1018 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1019 },
1020 {
1021 value: 'text-embedding-3-large',
1022 label: 'TE3 Large',
1023 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1024 }
1025 ],
1026 voyage: [
1027 {
1028 value: 'voyage-3-large',
1029 label: 'Voyage-3 Large',
1030 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1031 }
1032 ]
1033 };
1034
1035 let allModels = [];
1036 Object.keys(models).forEach(key => {
1037 if (category === 'all' || category === key) {
1038 allModels = allModels.concat(models[key]);
1039 }
1040 });
1041
1042 // Filter by search term if present
1043 if (filter) {
1044 const lowerFilter = filter.toLowerCase();
1045 allModels = allModels.filter(model =>
1046 model.label.toLowerCase().includes(lowerFilter) ||
1047 model.description.toLowerCase().includes(lowerFilter)
1048 );
1049 }
1050
1051 // Create model cards
1052 allModels.forEach(model => {
1053 const isSelected = $embeddingModelSelect.val() === model.value;
1054 const providerClass = model.value.startsWith('voyage-') ? 'mxchat-embedding-model-provider-voyage' : 'mxchat-embedding-model-provider-openai';
1055
1056 const $modelCard = $(`
1057 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1058 <div class="mxchat-embedding-model-selector-icon">
1059 ${model.value.startsWith('voyage-') ?
1060 '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>' :
1061 '<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>'
1062 }
1063 </div>
1064 <div class="mxchat-embedding-model-selector-info">
1065 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1066 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1067 </div>
1068 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1069 </div>
1070 `);
1071
1072
1073 $grid.append($modelCard);
1074 });
1075 }
1076
1077 // Event handlers - use namespaced events to avoid conflicts
1078 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1079 e.stopPropagation(); // Prevent event bubbling
1080 $('#' + embeddingModalId).show();
1081 populateEmbeddingModelsGrid('', 'all');
1082 });
1083
1084 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1085 e.stopPropagation(); // Prevent event bubbling
1086 $('#' + embeddingModalId).hide();
1087 });
1088
1089 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1090 e.stopPropagation(); // Prevent event bubbling
1091 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1092 $(this).addClass('active');
1093 const category = $(this).data('category');
1094 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1095 populateEmbeddingModelsGrid(searchTerm, category);
1096 });
1097
1098 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1099 const searchTerm = $(this).val();
1100 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1101 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1102 });
1103
1104 // Use a direct selector to avoid conflicts with other card elements
1105 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1106 e.stopPropagation(); // Prevent event bubbling
1107 const modelValue = $(this).data('value');
1108
1109 // Important: Only update this specific select element
1110 $embeddingModelSelect.val(modelValue);
1111
1112 // Manually trigger change only on this element
1113 const changeEvent = new Event('change', { bubbles: true });
1114 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1115
1116 // Update button text
1117 updateButtonText();
1118
1119 // Hide modal
1120 $('#' + embeddingModalId).hide();
1121 });
1122
1123 // Close modal when clicking outside - use namespaced events
1124 $(window).on('click.embeddingModelSelector', function(event) {
1125 if ($(event.target).is('#' + embeddingModalId)) {
1126 $('#' + embeddingModalId).hide();
1127 }
1128 });
1129 }
1130
1131 // Call this function after the DOM is fully loaded
1132 $(document).ready(function() {
1133 setupMxChatModelSelector();
1134 setupMxChatEmbeddingModelSelector();
1135 });
1136
1137 // Initialize API key visibility
1138 setupAPIKeyVisibility();
1139
1140 // Add Intent Form Submission
1141 $('#mxchat-add-intent-form').on('submit', function(event) {
1142 $('#mxchat-intent-loading').show();
1143 $('#mxchat-intent-loading-text').show();
1144 $(this).find('button[type="submit"]').hide();
1145 });
1146
1147 // Inline Edit Functionality
1148 $('.edit-button').on('click', function() {
1149 var row = $(this).closest('tr');
1150 row.find('.content-view, .url-view').hide();
1151 row.find('.content-edit, .url-edit').show();
1152 row.find('.edit-button').hide();
1153 row.find('.save-button').show();
1154 });
1155
1156 // Save button handler
1157 $('.save-button').on('click', function() {
1158 var button = $(this);
1159 var row = button.closest('tr');
1160 var id = button.data('id');
1161 var newContent = row.find('.content-edit').val();
1162 var newUrl = row.find('.url-edit').val();
1163
1164 button.prop('disabled', true);
1165 button.text('Saving...');
1166
1167 $.ajax({
1168 url: mxchatAdmin.ajax_url,
1169 type: 'POST',
1170 data: {
1171 action: 'mxchat_save_inline_prompt',
1172 id: id,
1173 article_content: newContent,
1174 article_url: newUrl,
1175 _ajax_nonce: mxchatAdmin.inline_edit_nonce
1176 },
1177 success: function(response) {
1178 button.prop('disabled', false);
1179 button.text('Save');
1180
1181 if (response.success) {
1182 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1183 if (newUrl) {
1184 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank">' + newUrl + '</a>');
1185 } else {
1186 row.find('.url-view').html('N/A');
1187 }
1188
1189 row.find('.content-edit, .url-edit').hide();
1190 row.find('.content-view, .url-view').show();
1191 row.find('.save-button').hide();
1192 row.find('.edit-button').show();
1193 } else {
1194 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1195 }
1196 },
1197 error: function() {
1198 button.prop('disabled', false);
1199 button.text('Save');
1200 alert('An error occurred while saving.');
1201 }
1202 });
1203 });
1204
1205 // Activation handling
1206 const form = $('#mxchat-activation-form');
1207 const spinner = $('#mxchat-activation-spinner');
1208 const submitButton = $('#activate_license_button');
1209 const licenseStatus = $('#mxchat-license-status');
1210
1211 if (form.length && licenseStatus.length && submitButton.length) {
1212 function handleActivationResponse(response) {
1213 spinner.hide();
1214 if (response.success) {
1215 licenseStatus.text('Active');
1216 licenseStatus.removeClass('inactive').addClass('active');
1217 form.hide();
1218 } else {
1219 licenseStatus.text('Inactive');
1220 alert(response.data || 'Activation failed. Please check your input.');
1221 submitButton.prop('disabled', false);
1222 }
1223 }
1224
1225 form.on('submit', function(event) {
1226 event.preventDefault();
1227 spinner.show();
1228 submitButton.prop('disabled', true);
1229
1230 var formData = {
1231 action: 'mxchat_activate_license',
1232 mxchat_pro_email: $('#mxchat_pro_email').val(),
1233 mxchat_activation_key: $('#mxchat_activation_key').val(),
1234 security: mxchatAdmin.license_nonce
1235 };
1236
1237 $.post(mxchatAdmin.ajax_url, formData, function(response) {
1238 handleActivationResponse(response);
1239 }).fail(function() {
1240 alert('Server error. Please try again.');
1241 spinner.hide();
1242 submitButton.prop('disabled', false);
1243 });
1244 });
1245 }
1246
1247 // Questions handling
1248 $('.mxchat-add-question').on('click', function () {
1249 const container = $('#mxchat-additional-questions-container');
1250 const questionCount = container.find('.mxchat-question-row').length + 4;
1251 const questionIndex = container.find('.mxchat-question-row').length;
1252
1253 const newQuestion = `
1254 <div class="mxchat-question-row">
1255 <input type="text"
1256 name="additional_popular_questions[]"
1257 placeholder="Enter Additional Popular Question ${questionCount}"
1258 class="regular-text mxchat-question-input"
1259 data-question-index="${questionIndex}" />
1260 <button type="button" class="button mxchat-remove-question"
1261 aria-label="Remove question">Remove</button>
1262 </div>
1263 `;
1264 container.append(newQuestion);
1265 });
1266
1267 $(document).on('click', '.mxchat-remove-question', function () {
1268 $(this).closest('.mxchat-question-row').remove();
1269 saveQuestions();
1270 });
1271
1272 $(document).on('change', '.mxchat-question-input', function() {
1273 saveQuestions();
1274 });
1275
1276 function saveQuestions() {
1277 const questions = [];
1278 $('.mxchat-question-input').each(function() {
1279 const value = $(this).val().trim();
1280 if (value) {
1281 questions.push(value);
1282 }
1283 });
1284
1285 const feedbackContainer = $('<div class="feedback-container"></div>');
1286 const spinner = $('<div class="saving-spinner"></div>');
1287 const successIcon = $('<div class="success-icon">✔</div>');
1288
1289 // Append feedback after the add button
1290 $('.mxchat-add-question').after(feedbackContainer);
1291 feedbackContainer.append(spinner);
1292
1293 // Save via AJAX
1294 $.ajax({
1295 url: mxchatAdmin.ajax_url,
1296 type: 'POST',
1297 data: {
1298 action: 'mxchat_save_setting',
1299 name: 'additional_popular_questions',
1300 value: JSON.stringify(questions),
1301 _ajax_nonce: mxchatAdmin.setting_nonce
1302 },
1303 success: function(response) {
1304 if (response.success) {
1305 spinner.fadeOut(200, function() {
1306 feedbackContainer.append(successIcon);
1307 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
1308 feedbackContainer.remove();
1309 });
1310 });
1311 } else {
1312 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
1313 feedbackContainer.remove();
1314 }
1315 },
1316 error: function() {
1317 alert('An error occurred while saving questions.');
1318 feedbackContainer.remove();
1319 }
1320 });
1321 }
1322
1323 // Live agent status handler
1324 const statusToggle = document.getElementById('live_agent_status');
1325 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
1326 if (statusToggle && statusText) {
1327 statusToggle.addEventListener('change', function() {
1328 // Update display text
1329 statusText.textContent = this.checked ? 'Online' : 'Offline';
1330
1331 // Send the correct on/off value to the server
1332 if (window.mxchatSaveSetting) {
1333 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
1334 }
1335 });
1336 }
1337
1338 // Function to adjust the textarea height to content
1339 function adjustTextareaHeight() {
1340 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
1341 this.style.height = this.scrollHeight + 'px'; // Expand to content height
1342 }
1343
1344 // Function to reset the textarea height to initial
1345 function resetTextareaHeight() {
1346 this.style.height = ''; // Remove inline height, reverting to CSS default
1347 }
1348
1349 // Target the specific textarea by ID
1350 var $textarea = $('#system_prompt_instructions');
1351
1352 // Bind events
1353 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
1354 .on('blur', resetTextareaHeight); // Reset on blur
1355 });
1356
1357 document.addEventListener('DOMContentLoaded', function() {
1358 // Check if we're on the correct page before initializing
1359 const modal = document.getElementById('mxchat-action-modal');
1360
1361 // Only initialize if the modal exists on this page
1362 if (modal) {
1363 //console.log('MXChat Action Modal JS Loaded');
1364
1365 // Initialize the action modal functionality
1366 initStepBasedActionModal();
1367 }
1368
1369 // Function to initialize the step-based action modal
1370 function initStepBasedActionModal() {
1371 // We already checked for modal existence above, so no need to check again
1372
1373 const actionStep1 = document.getElementById('mxchat-action-step-1');
1374 const actionStep2 = document.getElementById('mxchat-action-step-2');
1375 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
1376 const searchInput = document.getElementById('action-type-search');
1377 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
1378 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
1379 const actionForm = document.getElementById('mxchat-action-form');
1380 const callbackInput = document.getElementById('callback_function');
1381 const actionIdField = document.getElementById('edit_action_id');
1382 const labelField = document.getElementById('intent_label');
1383 const phrasesField = document.getElementById('action_phrases');
1384 const formActionType = document.getElementById('form_action_type');
1385 const nonceContainer = document.getElementById('action-nonce-container');
1386 const thresholdSlider = document.getElementById('similarity_threshold');
1387 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1388
1389 // Rest of your initialization code remains the same...
1390
1391 // Log the structure of one action card for debugging
1392 if (actionCards.length > 0) {
1393 //console.log('First action card data attributes:', actionCards[0].dataset);
1394 //console.log('First action card HTML:', actionCards[0].outerHTML);
1395 }
1396
1397 // Add click event listeners to category buttons
1398 categoryButtons.forEach(button => {
1399 button.addEventListener('click', function() {
1400 //console.log('Category button clicked:', this.dataset.category);
1401
1402 // Remove active class from all buttons
1403 categoryButtons.forEach(btn => btn.classList.remove('active'));
1404
1405 // Add active class to clicked button
1406 this.classList.add('active');
1407
1408 // Get selected category
1409 const category = this.dataset.category;
1410
1411 // Filter action cards
1412 filterActionCards(category, searchInput.value);
1413 });
1414 });
1415
1416 // Add search functionality
1417 if (searchInput) {
1418 searchInput.addEventListener('input', function() {
1419 // Get active category
1420 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
1421 //console.log('Search input changed, active category:', activeCategory);
1422
1423 // Filter action cards
1424 filterActionCards(activeCategory, this.value);
1425 });
1426 }
1427
1428 // Add click event listeners to action cards
1429 actionCards.forEach(card => {
1430 card.addEventListener('click', function() {
1431 // Get the action data
1432 const isPro = this.dataset.pro === 'true';
1433 const isInstalled = this.dataset.installed === 'true';
1434 const addonName = this.dataset.addon || '';
1435 const actionValue = this.dataset.value;
1436 const actionLabel = this.dataset.label;
1437 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
1438 const actionDescription = this.querySelector('p').textContent;
1439
1440 // Pro check using the proper detection method
1441 const proIsActivated = typeof mxchatAdmin !== 'undefined' &&
1442 (mxchatAdmin.is_activated === '1' ||
1443 mxchatAdmin.is_activated === 'true' ||
1444 mxchatAdmin.is_activated === true);
1445
1446 // Handle different states
1447 if (isPro && !proIsActivated) {
1448 // Pro feature but no Pro license
1449 showProFeatureNotice();
1450 return;
1451 }
1452
1453 if (addonName && !isInstalled) {
1454 // Add-on required but not installed
1455 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '') || addonName + ' Add-on';
1456 showAddonRequiredNotice(addonDisplayName);
1457 return;
1458 }
1459
1460 // If we get here, the action is available - proceed as normal
1461 callbackInput.value = actionValue;
1462
1463 // Update the selected action display in step 2
1464 document.getElementById('selected-action-title').textContent = actionLabel;
1465 document.getElementById('selected-action-description').textContent = actionDescription;
1466 document.getElementById('selected-action-icon').innerHTML =
1467 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1468
1469 // Set a default label based on the action type (user can change it)
1470 if (!labelField.value) {
1471 labelField.value = actionLabel;
1472 }
1473
1474 // Move to step 2
1475 actionStep1.classList.remove('active');
1476 actionStep2.classList.add('active');
1477
1478 // Update modal title
1479 });
1480 });
1481
1482 // Back button functionality
1483 if (backToStep1Btn) {
1484 backToStep1Btn.addEventListener('click', function() {
1485 //console.log('Back button clicked');
1486 actionStep2.classList.remove('active');
1487 actionStep1.classList.add('active');
1488 });
1489 }
1490
1491 // Function to filter action cards by category and search term
1492 function filterActionCards(category, searchTerm) {
1493 searchTerm = searchTerm.toLowerCase().trim();
1494 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
1495
1496 let visibleCount = 0;
1497
1498 // Show all cards initially with animation
1499 actionCards.forEach((card, index) => {
1500 // Reset animation
1501 card.style.animation = 'none';
1502 // Trigger reflow
1503 void card.offsetWidth;
1504
1505 // Determine if card should be visible based on category and search term
1506 const cardCategory = card.dataset.category || '';
1507 const matchesCategory = category === 'all' || cardCategory === category;
1508
1509 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
1510 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
1511 const matchesSearch = searchTerm === '' ||
1512 cardTitle.includes(searchTerm) ||
1513 cardDesc.includes(searchTerm);
1514
1515 // Show/hide card with animation
1516 if (matchesCategory && matchesSearch) {
1517 card.style.display = 'flex';
1518 // Staggered animation for cards
1519 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
1520 visibleCount++;
1521 } else {
1522 card.style.display = 'none';
1523 }
1524 });
1525
1526 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
1527 }
1528
1529 // Function to show notice for Pro features
1530 function showProFeatureNotice() {
1531 //console.log('Showing Pro feature notice');
1532 // Check if we already have a notification container
1533 let noticeContainer = document.querySelector('.mxchat-pro-notice');
1534
1535 if (!noticeContainer) {
1536 // Create the notice container
1537 noticeContainer = document.createElement('div');
1538 noticeContainer.className = 'mxchat-pro-notice';
1539
1540 // Create content
1541 noticeContainer.innerHTML = `
1542 <div class="mxchat-pro-notice-content">
1543 <h3>MxChat Pro Feature</h3>
1544 <p>This action is available in the Pro version only.</p>
1545 <div class="mxchat-pro-notice-buttons">
1546 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
1547 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
1548 </div>
1549 </div>
1550 `;
1551
1552 // Append to body
1553 document.body.appendChild(noticeContainer);
1554
1555 // Add close functionality
1556 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
1557 closeButton.addEventListener('click', function() {
1558 noticeContainer.classList.remove('active');
1559 setTimeout(() => {
1560 noticeContainer.remove();
1561 }, 300);
1562 });
1563
1564 // Click outside to close
1565 noticeContainer.addEventListener('click', function(e) {
1566 if (e.target === noticeContainer) {
1567 closeButton.click();
1568 }
1569 });
1570
1571 // Show with animation
1572 setTimeout(() => {
1573 noticeContainer.classList.add('active');
1574 }, 10);
1575 } else {
1576 // If it already exists, just make it visible again
1577 noticeContainer.classList.add('active');
1578 }
1579 }
1580
1581 // Function to show notice for add-on requirements
1582 function showAddonRequiredNotice(addonName) {
1583 //console.log(`Showing add-on notice for: ${addonName}`);
1584 // Check if we already have a notification container
1585 let noticeContainer = document.querySelector('.mxchat-addon-notice');
1586
1587 if (!noticeContainer) {
1588 // Create the notice container
1589 noticeContainer = document.createElement('div');
1590 noticeContainer.className = 'mxchat-addon-notice';
1591
1592 // Create content
1593 noticeContainer.innerHTML = `
1594 <div class="mxchat-addon-notice-content">
1595 <span class="mxchat-addon-notice-icon">🧩</span>
1596 <h3>Add-on Required</h3>
1597 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
1598 <div class="mxchat-addon-notice-buttons">
1599 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
1600 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
1601 </div>
1602 </div>
1603 `;
1604
1605 // Append to body
1606 document.body.appendChild(noticeContainer);
1607
1608 // Add close functionality
1609 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
1610 closeButton.addEventListener('click', function() {
1611 noticeContainer.classList.remove('active');
1612 setTimeout(() => {
1613 noticeContainer.remove();
1614 }, 300);
1615 });
1616
1617 // Click outside to close
1618 noticeContainer.addEventListener('click', function(e) {
1619 if (e.target === noticeContainer) {
1620 closeButton.click();
1621 }
1622 });
1623
1624 // Show with animation
1625 setTimeout(() => {
1626 noticeContainer.classList.add('active');
1627 }, 10);
1628 } else {
1629 // If it already exists, update the content
1630 const addonNameElement = noticeContainer.querySelector('p strong');
1631 if (addonNameElement) {
1632 addonNameElement.textContent = addonName;
1633 }
1634
1635 // Make it visible again
1636 noticeContainer.classList.add('active');
1637 }
1638 }
1639
1640 // Form submission handling
1641 if (actionForm) {
1642 actionForm.addEventListener('submit', function() {
1643 //console.log('Form submitted');
1644 document.getElementById('mxchat-action-loading').style.display = 'flex';
1645 this.querySelector('button[type="submit"]').disabled = true;
1646 });
1647 }
1648 }
1649
1650 // Setup add action buttons (only if we're on the correct page)
1651 if (modal) {
1652 // Update the modal open function to support the step-based flow
1653 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
1654 //console.log('Modal opening, edit mode:', isEdit);
1655
1656 // No need to check again, we already verified modal exists
1657
1658 // Get form fields
1659 const actionIdField = document.getElementById('edit_action_id');
1660 const labelField = document.getElementById('intent_label');
1661 const phrasesField = document.getElementById('action_phrases');
1662 const formActionType = document.getElementById('form_action_type');
1663 const callbackInput = document.getElementById('callback_function');
1664 const saveButton = document.getElementById('mxchat-save-action-btn');
1665 const nonceContainer = document.getElementById('action-nonce-container');
1666 const thresholdSlider = document.getElementById('similarity_threshold');
1667 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1668 const actionStep1 = document.getElementById('mxchat-action-step-1');
1669 const actionStep2 = document.getElementById('mxchat-action-step-2');
1670 const searchInput = document.getElementById('action-type-search');
1671
1672 // Set up modal for edit or create
1673 if (isEdit) {
1674 saveButton.textContent = 'Update Action';
1675 formActionType.value = 'mxchat_edit_intent';
1676 actionIdField.value = actionId;
1677 labelField.value = label;
1678 phrasesField.value = phrases;
1679 callbackInput.value = callbackFunction;
1680 thresholdSlider.value = threshold; // Set the current threshold value
1681 thresholdDisplay.textContent = threshold + '%'; // Update display
1682
1683 // Update the nonce field for editing
1684 nonceContainer.innerHTML = ''; // Clear existing nonce
1685 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
1686 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
1687 }
1688
1689 // For editing, go directly to step 2 and update the selected action display
1690 actionStep1.classList.remove('active');
1691 actionStep2.classList.add('active');
1692
1693 // Find the matching action card to get its details
1694 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
1695 let foundCard = null;
1696
1697 actionCards.forEach(card => {
1698 if (card.dataset.value === callbackFunction) {
1699 foundCard = card;
1700 }
1701 });
1702
1703 if (foundCard) {
1704 //console.log('Found matching action card for:', callbackFunction);
1705 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
1706 const actionIconElement = foundCard.querySelector('.dashicons');
1707 const actionIcon = actionIconElement
1708 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
1709 : 'admin-generic';
1710 const actionDescription = foundCard.querySelector('p')?.textContent || '';
1711
1712 document.getElementById('selected-action-title').textContent = actionLabel;
1713 document.getElementById('selected-action-description').textContent = actionDescription;
1714 document.getElementById('selected-action-icon').innerHTML =
1715 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1716 } else {
1717 //console.log('No matching action card found for:', callbackFunction);
1718 // Fallback if we can't find the card
1719 document.getElementById('selected-action-title').textContent = label;
1720 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
1721 document.getElementById('selected-action-icon').innerHTML =
1722 `<span class="dashicons dashicons-admin-generic"></span>`;
1723 }
1724 } else {
1725 //console.log('Setting up create mode');
1726 saveButton.textContent = 'Save Action';
1727 formActionType.value = 'mxchat_add_intent';
1728 actionIdField.value = '';
1729 labelField.value = '';
1730 phrasesField.value = '';
1731 callbackInput.value = '';
1732 thresholdSlider.value = 85; // Default value for new actions
1733 thresholdDisplay.textContent = '85%'; // Default display
1734
1735 // Update the nonce field for adding
1736 nonceContainer.innerHTML = ''; // Clear existing nonce
1737 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
1738 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
1739 }
1740
1741 // For creating new, start at step 1
1742 actionStep1.classList.add('active');
1743 actionStep2.classList.remove('active');
1744 }
1745
1746 // Show modal with animation
1747 modal.style.display = 'flex';
1748 requestAnimationFrame(() => {
1749 modal.classList.add('active');
1750 });
1751
1752 // Set up close handlers
1753 const closeModal = () => {
1754 //console.log('Closing modal');
1755 modal.classList.remove('active');
1756 setTimeout(() => {
1757 modal.style.display = 'none';
1758 }, 300); // Match the CSS transition time
1759 };
1760
1761 // Close button handler
1762 const closeBtn = modal.querySelector('.mxchat-modal-close');
1763 if (closeBtn) {
1764 closeBtn.onclick = closeModal;
1765 }
1766
1767 // Cancel button handler
1768 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
1769 if (cancelBtns) {
1770 cancelBtns.forEach(btn => {
1771 btn.onclick = closeModal;
1772 });
1773 }
1774
1775 // Click outside modal to close
1776 modal.onclick = (e) => {
1777 if (e.target === modal) {
1778 closeModal();
1779 }
1780 };
1781
1782 // Escape key to close modal
1783 document.addEventListener('keydown', function(e) {
1784 if (e.key === 'Escape' && modal.classList.contains('active')) {
1785 closeModal();
1786 }
1787 }, { once: true });
1788
1789 // Focus appropriate field based on current step
1790 if (isEdit || actionStep2.classList.contains('active')) {
1791 if (labelField) labelField.focus();
1792 } else {
1793 if (searchInput) searchInput.focus();
1794 }
1795
1796 return closeModal; // Return close function for external use
1797 };
1798
1799 // Setup add action buttons
1800 const addActionBtn = document.getElementById('mxchat-add-action-btn');
1801 if (addActionBtn) {
1802 //console.log('Add action button found');
1803 addActionBtn.onclick = () => window.mxchatOpenActionModal();
1804 }
1805
1806 const createFirstAction = document.getElementById('mxchat-create-first-action');
1807 if (createFirstAction) {
1808 //console.log('Create first action button found');
1809 createFirstAction.onclick = () => window.mxchatOpenActionModal();
1810 }
1811
1812 // Setup edit buttons
1813 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
1814 //console.log('Edit buttons found:', editButtons.length);
1815 editButtons.forEach(button => {
1816 button.onclick = () => {
1817 const actionId = button.dataset.actionId;
1818 const phrases = button.dataset.phrases;
1819 const label = button.dataset.label;
1820 const threshold = button.dataset.threshold || 85;
1821 const callbackFunction = button.dataset.callbackFunction;
1822
1823 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction);
1824 };
1825 });
1826 }
1827 });
1828
1829 jQuery(document).ready(function($) {
1830 // Toggle custom post types container
1831 $('#mxchat-custom-post-types-toggle').on('click', function(e) {
1832 e.preventDefault();
1833
1834 $('#mxchat-custom-post-types-container').slideToggle(300);
1835
1836 // Rotate the toggle icon
1837 const $icon = $(this).find('.mxchat-accordion-icon');
1838 if ($('#mxchat-custom-post-types-container').is(':visible')) {
1839 $icon.css('transform', 'rotate(180deg)');
1840 $(this).closest('.mxchat-settings-accordion').addClass('active');
1841 } else {
1842 $icon.css('transform', 'rotate(0deg)');
1843 $(this).closest('.mxchat-settings-accordion').removeClass('active');
1844 }
1845 });
1846
1847 // If there are any selections made, auto-expand the container
1848 function autoExpandIfNeeded() {
1849 // Check if any checkbox in the container is checked
1850 const hasCheckedItems = $('#mxchat-custom-post-types-container input[type="checkbox"]:checked').length > 0;
1851
1852 if (hasCheckedItems) {
1853 $('#mxchat-custom-post-types-container').show();
1854 $('#mxchat-custom-post-types-toggle .mxchat-accordion-icon').css('transform', 'rotate(180deg)');
1855 $('.mxchat-settings-accordion').addClass('active');
1856 }
1857 }
1858
1859 // Run on page load
1860 autoExpandIfNeeded();
1861 });
1862
1863 jQuery(document).ready(function($) {
1864 // Check if we're on the right admin page with status updating
1865 if ($('.mxchat-status-card').length > 0) {
1866 // Initialize AJAX status updates
1867 initStatusUpdates();
1868 }
1869
1870 function initStatusUpdates() {
1871 // Get the refresh interval (default to 5 seconds if not set)
1872 const refreshInterval = parseInt(mxchatAdmin.status_refresh_interval || 5000);
1873
1874 // Check if the status cards exist
1875 const hasSitemapStatus = $('.mxchat-status-card').length > 0;
1876
1877 if (hasSitemapStatus) {
1878 // Start the interval for automatic updates
1879 const updateIntervalId = setInterval(function() {
1880 fetchStatusUpdates();
1881 }, refreshInterval);
1882
1883 // Attach event listener to stop button to clear the interval
1884 $('.mxchat-stop-form').on('submit', function() {
1885 clearInterval(updateIntervalId);
1886 });
1887
1888 // Do an initial fetch
1889 fetchStatusUpdates();
1890 }
1891 }
1892
1893 function fetchStatusUpdates() {
1894 // If user is actively viewing the failed URLs, don't refresh as frequently
1895 const $details = $('.mxchat-failed-urls-container details');
1896 const isUserViewing = $details.length > 0 && $details.prop('open');
1897
1898 // If details are open, we'll refresh at a slower rate (or not at all)
1899 if (isUserViewing) {
1900 // Optional: Skip this refresh completely when details are open
1901 // return;
1902
1903 // Alternative: Update less frequently when details are open
1904 setTimeout(function() {
1905 performStatusUpdate();
1906 }, 5000); // Slow down updates to every 5 seconds when details are open
1907 } else {
1908 performStatusUpdate();
1909 }
1910 }
1911
1912 function performStatusUpdate() {
1913 $.ajax({
1914 url: ajaxurl,
1915 type: 'POST',
1916 data: {
1917 action: 'mxchat_get_status_updates',
1918 nonce: mxchatAdmin.status_nonce
1919 },
1920 success: function(response) {
1921 if (response && response.is_processing) {
1922 updateStatusUI(response);
1923 }
1924 },
1925 error: function(xhr, status, error) {
1926 console.error('Status update failed:', error);
1927 }
1928 });
1929 }
1930
1931 function updateStatusUI(data) {
1932 // Update sitemap status if available
1933 if (data.sitemap_status) {
1934 // Update basic info
1935 const sitemapStatus = data.sitemap_status;
1936 $('.mxchat-progress-fill').css('width', sitemapStatus.percentage + '%');
1937 $('.mxchat-status-details p').text(
1938 'Progress: ' + sitemapStatus.processed_urls + ' of ' +
1939 sitemapStatus.total_urls + ' URLs (' + sitemapStatus.percentage + '%)'
1940 );
1941
1942 // Check if details is already open before updating
1943 const isDetailsOpen = $('.mxchat-failed-urls-container details').prop('open');
1944
1945 // Update errors display
1946 const $errorContainer = $('.mxchat-error-notice');
1947 if ($errorContainer.length === 0 && (sitemapStatus.error || sitemapStatus.last_error || sitemapStatus.failed_urls_list.length > 0)) {
1948 // Create error container if it doesn't exist
1949 $('.mxchat-status-details').append('<div class="mxchat-error-notice"></div>');
1950 }
1951
1952 // Update or create error notices
1953 const $newErrorContainer = $('.mxchat-error-notice');
1954 if ($newErrorContainer.length > 0) {
1955 let errorHTML = '';
1956
1957 if (sitemapStatus.error) {
1958 errorHTML += '<p class="error">' + sitemapStatus.error + '</p>';
1959 }
1960
1961 if (sitemapStatus.last_error) {
1962 errorHTML += '<p class="last-error">Last error: ' + sitemapStatus.last_error + '</p>';
1963 }
1964
1965 // Add failed URLs list
1966 if (sitemapStatus.failed_urls_list && sitemapStatus.failed_urls_list.length > 0) {
1967 errorHTML += '<div class="mxchat-failed-urls-container">';
1968 errorHTML += '<h4>Failed URLs (' + sitemapStatus.failed_urls_list.length + ')</h4>';
1969
1970 // Set the 'open' attribute based on previous state
1971 errorHTML += '<details' + (isDetailsOpen ? ' open' : '') + '>';
1972 errorHTML += '<summary>Show Failed URLs</summary>';
1973 errorHTML += '<div class="mxchat-failed-urls-list">';
1974
1975 // Sort by most recent first
1976 const sortedFailedUrls = [...sitemapStatus.failed_urls_list].sort((a, b) => b.time - a.time);
1977
1978 // Limit to at most 50 URLs to display
1979 const displayUrls = sortedFailedUrls.slice(0, 50);
1980
1981 displayUrls.forEach(item => {
1982 const timeAgo = formatTimeAgo(item.time);
1983 errorHTML += '<div class="mxchat-failed-url-item">';
1984 errorHTML += '<span class="mxchat-failed-url-address">' +
1985 '<a href="' + item.url + '" target="_blank">' + truncateUrl(item.url) + '</a></span>';
1986 errorHTML += '<span class="mxchat-failed-url-error">' + item.error + '</span>';
1987 errorHTML += '<span class="mxchat-failed-url-time">' + timeAgo + '</span>';
1988 errorHTML += '</div>';
1989 });
1990
1991 if (sitemapStatus.failed_urls_list.length > 50) {
1992 errorHTML += '<div class="mxchat-failed-urls-more">+ ' +
1993 (sitemapStatus.failed_urls_list.length - 50) +
1994 ' more failed URLs not shown</div>';
1995 }
1996
1997 errorHTML += '</div>'; // End of failed-urls-list
1998 errorHTML += '</details>';
1999 errorHTML += '</div>'; // End of failed-urls-container
2000 }
2001
2002 $newErrorContainer.html(errorHTML);
2003
2004 // Additionally, add a click handler to pause refreshes when viewing details
2005 $('.mxchat-failed-urls-container details').on('toggle', function() {
2006 if (this.open) {
2007 // User opened the details - set a flag
2008 $(this).data('user-opened', true);
2009 } else {
2010 // User closed the details - remove the flag
2011 $(this).data('user-opened', false);
2012 }
2013 });
2014 }
2015 }
2016 }
2017
2018 // Helper function to format time ago
2019 function formatTimeAgo(timestamp) {
2020 const now = Math.floor(Date.now() / 1000);
2021 const seconds = now - timestamp;
2022
2023 if (seconds < 60) {
2024 return seconds + ' seconds ago';
2025 } else if (seconds < 3600) {
2026 return Math.floor(seconds / 60) + ' minutes ago';
2027 } else if (seconds < 86400) {
2028 return Math.floor(seconds / 3600) + ' hours ago';
2029 } else {
2030 return Math.floor(seconds / 86400) + ' days ago';
2031 }
2032 }
2033
2034 // Helper function to truncate long URLs
2035 function truncateUrl(url) {
2036 const maxLength = 50;
2037 if (url.length <= maxLength) return url;
2038
2039 // Remove protocol
2040 let displayUrl = url.replace(/^https?:\/\//, '');
2041
2042 if (displayUrl.length <= maxLength) return displayUrl;
2043
2044 // Keep the domain and truncate the path
2045 const domainMatch = displayUrl.match(/^([^\/]+)\//);
2046 if (domainMatch) {
2047 const domain = domainMatch[1];
2048 const path = displayUrl.substring(domain.length);
2049
2050 if (path.length > 10) {
2051 return domain + path.substring(0, maxLength - domain.length - 3) + '...';
2052 }
2053 }
2054
2055 // Final fallback for very long strings
2056 return displayUrl.substring(0, maxLength - 3) + '...';
2057 }
2058 });
2059
2060
2061
2062