PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.1.7
MxChat – AI Chatbot & Content Generation for WordPress v2.1.7
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
← All changes | js/mxchat-admin.js +1590 -99 2.0.52.1.7 View file →
@@ -10,9 +10,9 @@
10 10 timeout = setTimeout(later, wait);
11 11 };
12 12 }
13 13
14 -// Helper function to open edit modal
14 +// Helper function to open edit modal for intents/actions
15 15 function mxchatOpenEditModal(intentId, phrases) {
16 16 const modal = document.getElementById('mxchat-edit-modal');
17 17 if (!modal) return;
18 18
@@ -60,11 +60,122 @@
60 60 // Focus the textarea
61 61 phrasesField.focus();
62 62 }
63 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 +
64 175 // Initialize event listeners
65 176 document.addEventListener('DOMContentLoaded', () => {
66 - // Set up edit button handlers
177 + // Set up edit button handlers for intents
67 178 document.querySelectorAll('.mxchat-edit-button').forEach(button => {
68 179 button.onclick = () => {
69 180 const intentId = button.dataset.intentId;
70 181 const phrases = button.dataset.phrases;
@@ -70,8 +181,158 @@
70 181 const phrases = button.dataset.phrases;
71 182 mxchatOpenEditModal(intentId, phrases);
72 183 };
73 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 + });
74 335 });
75 336
76 337 jQuery(document).ready(function($) {
77 338 // Ensure we have a debounce function (use lodash if available, otherwise use our implementation)
@@ -78,10 +339,21 @@
78 339 const useDebounce = (window._ && window._.debounce) ? window._.debounce : debounce;
79 340
80 341 // --- AJAX Auto-Save ---
81 342 const $autosaveSections = $('.mxchat-autosave-section');
343 +
344 + // Track whether fields have been modified by user
345 + const userModifiedFields = new Set();
82 346
83 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 +
84 356 // Handle real-time range slider value updates
85 357 $autosaveSections.find('input[type="range"]').on('input', function() {
86 358 const value = $(this).val();
87 359 $('#threshold_value').text(value);
@@ -90,8 +362,30 @@
90 362 // Handle all input changes (including range slider)
91 363 $autosaveSections.find('input, textarea, select').on('change', function() {
92 364 const $field = $(this);
93 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 +
94 388 let value;
95 389
96 390 // Handle different input types
97 391 if ($field.attr('type') === 'checkbox') {
@@ -119,11 +413,12 @@
119 413
120 414 // Determine which AJAX action and nonce to use:
121 415 var ajaxAction, nonce;
122 416 // Use the new AJAX action for submenu fields:
123 - if ( name.indexOf('mxchat_prompts_options') !== -1 ||
124 - name === 'mxchat_auto_sync_posts' ||
125 - name === 'mxchat_auto_sync_pages' ) {
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
126 421 ajaxAction = 'mxchat_save_prompts_setting';
127 422 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
128 423 } else {
129 424 // Otherwise, use the existing AJAX action.
@@ -148,21 +443,52 @@
148 443 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
149 444 feedbackContainer.remove();
150 445 });
151 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 + }
152 452 } else {
153 - alert('Error saving: ' + (response.data?.message || 'Unknown error'));
154 - if ($field.attr('type') === 'checkbox') {
155 - $field.prop('checked', !$field.is(':checked'));
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;
156 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
157 479 feedbackContainer.remove();
158 480 }
159 481 },
160 - error: function() {
161 - alert('An error occurred while saving.');
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
162 487 if ($field.attr('type') === 'checkbox') {
163 488 $field.prop('checked', !$field.is(':checked'));
164 489 }
490 +
165 491 feedbackContainer.remove();
166 492 }
167 493 });
168 494 });
@@ -174,9 +500,9 @@
174 500 $(this).wpColorPicker({
175 501 change: useDebounce(function(event, ui) {
176 502 // Safety check - ensure we have a valid field and value
177 503 if (!$colorField || !$colorField.val()) {
178 - console.warn('Color picker not ready');
504 + //console.warn('Color picker not ready');
179 505 return;
180 506 }
181 507
182 508 const name = $colorField.attr('name');
@@ -182,9 +508,9 @@
182 508 const name = $colorField.attr('name');
183 509 const value = $colorField.val();
184 510
185 511 if (!name || !value) {
186 - console.warn('Missing required color picker values');
512 + //console.warn('Missing required color picker values');
187 513 return;
188 514 }
189 515
190 516 // Create feedback container
@@ -195,20 +521,22 @@
195 521 // Position feedback container
196 522 $colorField.closest('.wp-picker-container').after(feedbackContainer);
197 523 feedbackContainer.append(spinner);
198 524
199 - // Determine AJAX action and nonce for color fields:
525 + // Determine which AJAX action and nonce to use:
200 526 var ajaxAction, nonce;
201 - if ( name.indexOf('mxchat_prompts_options') !== -1 ||
202 - name === 'mxchat_auto_sync_posts' ||
203 - name === 'mxchat_auto_sync_pages' ) {
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
204 532 ajaxAction = 'mxchat_save_prompts_setting';
205 533 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
206 534 } else {
535 + // Otherwise, use the existing AJAX action.
207 536 ajaxAction = 'mxchat_save_setting';
208 537 nonce = mxchatAdmin.setting_nonce;
209 538 }
210 -
211 539 // AJAX save request
212 540 $.ajax({
213 541 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
214 542 type: 'POST',
@@ -240,9 +568,9 @@
240 568 });
241 569 });
242 570
243 571 // Reinitialize color pickers when switching tabs
244 - $('.mxchat-nav-tab').on('click.mxchat', function() {
572 + $('.mxchat-tab-button').on('click.mxchat', function() {
245 573 setTimeout(function() {
246 574 $('.my-color-field:visible').wpColorPicker('close');
247 575 }, 100);
248 576 });
@@ -247,107 +575,569 @@
247 575 }, 100);
248 576 });
249 577 }
250 578
251 - // Initialize tabs system
252 - function initTabs() {
253 - // Remove any existing handlers first
254 - $('.mxchat-nav-tab').off('click.mxchat');
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();
255 588
256 - // Add new click handlers
257 - $('.mxchat-nav-tab').on('click.mxchat', function(e) {
258 - e.preventDefault();
259 - e.stopPropagation();
260 -
261 - var $this = $(this);
262 -
263 - // Get tab ID - try href first, fallback to data-tab, then to default
264 - var tabId = $this.attr('href');
265 - if (tabId) {
266 - tabId = tabId.replace('#', '');
267 - } else {
268 - tabId = $this.data('tab') || 'chatbot';
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);
269 696 }
270 -
271 - // Safety check for empty tabId
272 - if (!tabId) {
273 - console.warn('No tab identifier found');
274 - return;
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);
275 759 }
276 -
277 - // Update tabs
278 - $('.mxchat-nav-tab').removeClass('mxchat-nav-tab-active');
279 - $this.addClass('mxchat-nav-tab-active');
280 -
281 - // Update content areas - with safety check
282 - $('.mxchat-tab-content').removeClass('active').hide();
283 - var $targetTab = $('#' + tabId);
284 - if ($targetTab.length) {
285 - $targetTab.addClass('active').show();
286 -
287 - // Store active tab
288 - try {
289 - localStorage.setItem('mxchat_active_tab', tabId);
290 - } catch (e) {
291 - console.warn('LocalStorage not available:', e);
292 - }
293 - } else {
294 - console.warn('Tab content #' + tabId + ' not found');
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]);
295 863 }
296 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 + });
297 890 }
298 891
299 - // Initialize tabs and handle events
300 - initTabs();
301 - $(document).on('widget-added widget-updated postbox-toggled', initTabs);
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 +}
302 901
303 - // Activate initial tab
304 - try {
305 - var savedTab = localStorage.getItem('mxchat_active_tab');
306 - if (savedTab && $('#' + savedTab).length > 0) {
307 - $('.mxchat-nav-tab[href="#' + savedTab + '"]').trigger('click.mxchat');
308 - } else {
309 - $('.mxchat-nav-tab').first().trigger('click.mxchat');
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();
310 937 }
311 - } catch (e) {
312 - $('.mxchat-nav-tab').first().trigger('click.mxchat');
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;
313 948 }
314 949
315 - // Attach edit modal event handler
316 - $(document).on('click', '.mxchat-edit-button', function() {
317 - const intentId = $(this).data('intent-id');
318 - const phrases = $(this).data('phrases');
319 - mxchatOpenEditModal(intentId, phrases);
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'
320 955 });
321 956
322 - // Toggle visibility handlers
323 - function toggleVisibility(selector) {
324 - $(selector).on('click', function() {
325 - var inputField = $(this).prev('input');
326 - if (inputField.attr('type') === 'password') {
327 - inputField.attr('type', 'text');
328 - $(this).text('Hide');
329 - } else {
330 - inputField.attr('type', 'password');
331 - $(this).text('Show');
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]);
332 1039 }
333 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 + });
334 1075 }
335 1076
336 - // Initialize all toggle visibility buttons
337 - [
338 - '#toggleApiKeyVisibility',
339 - '#toggleWooCommerceSecretVisibility',
340 - '#toggleLoopsApiKeyVisibility',
341 - '#toggleXaiApiKeyVisibility',
342 - '#toggleClaudeApiKeyVisibility',
343 - '#toggleBraveApiKeyVisibility',
344 - '#toggleWebhookUrlVisibility',
345 - '#toggleSecretKeyVisibility',
346 - '#toggleBotTokenVisibility',
347 - '#toggleDeepSeekApiKeyVisibility'
348 - ].forEach(toggleVisibility);
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 + });
349 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 +
350 1140 // Add Intent Form Submission
351 1141 $('#mxchat-add-intent-form').on('submit', function(event) {
352 1142 $('#mxchat-intent-loading').show();
353 1143 $('#mxchat-intent-loading-text').show();
@@ -561,10 +1351,711 @@
561 1351
562 1352 // Bind events
563 1353 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
564 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');
565 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 + }
566 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 + }
567 2058 });
568 2059
569 2060
570 2061