PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.1.5
MxChat – AI Chatbot & Content Generation for WordPress v2.1.5
3.2.22 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 All 153 releases
← All changes | js/mxchat-admin.js +1539 -57 2.0.32.1.5 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)
@@ -119,11 +380,12 @@
119 380
120 381 // Determine which AJAX action and nonce to use:
121 382 var ajaxAction, nonce;
122 383 // 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' ) {
384 + if (name.indexOf('mxchat_prompts_options') !== -1 ||
385 + name === 'mxchat_auto_sync_posts' ||
386 + name === 'mxchat_auto_sync_pages' ||
387 + name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields
126 388 ajaxAction = 'mxchat_save_prompts_setting';
127 389 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
128 390 } else {
129 391 // Otherwise, use the existing AJAX action.
@@ -148,21 +410,52 @@
148 410 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
149 411 feedbackContainer.remove();
150 412 });
151 413 });
414 +
415 + // Check if the response contains a "no changes" message and log it
416 + if (response.data && response.data.message === 'No changes detected') {
417 + //console.log('No changes detected for field:', name);
418 + }
152 419 } else {
153 - alert('Error saving: ' + (response.data?.message || 'Unknown error'));
154 - if ($field.attr('type') === 'checkbox') {
155 - $field.prop('checked', !$field.is(':checked'));
420 + // Only show alert for actual errors, not for "no changes"
421 + let errorMessage = response.data?.message || 'Unknown error';
422 +
423 + // Don't display an alert for "no changes" message
424 + if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
425 + alert('Error saving: ' + errorMessage);
426 + } else {
427 + // Still provide visual feedback that no changes were needed
428 + spinner.fadeOut(200, function() {
429 + feedbackContainer.append(successIcon);
430 + successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
431 + feedbackContainer.remove();
432 + });
433 + });
434 + //console.log('No changes detected for field:', name);
435 + return;
156 436 }
437 +
438 + // Only revert checkbox state if it was an actual error
439 + if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
440 + if ($field.attr('type') === 'checkbox') {
441 + $field.prop('checked', !$field.is(':checked'));
442 + }
443 + }
444 +
445 + // Always clean up the feedback container
157 446 feedbackContainer.remove();
158 447 }
159 448 },
160 - error: function() {
161 - alert('An error occurred while saving.');
449 + error: function(xhr, textStatus, error) {
450 + console.error('AJAX Error:', textStatus, error);
451 + alert('An error occurred while saving. Please try again.');
452 +
453 + // Revert checkbox state on error
162 454 if ($field.attr('type') === 'checkbox') {
163 455 $field.prop('checked', !$field.is(':checked'));
164 456 }
457 +
165 458 feedbackContainer.remove();
166 459 }
167 460 });
168 461 });
@@ -195,20 +488,22 @@
195 488 // Position feedback container
196 489 $colorField.closest('.wp-picker-container').after(feedbackContainer);
197 490 feedbackContainer.append(spinner);
198 491
199 - // Determine AJAX action and nonce for color fields:
492 + // Determine which AJAX action and nonce to use:
200 493 var ajaxAction, nonce;
201 - if ( name.indexOf('mxchat_prompts_options') !== -1 ||
202 - name === 'mxchat_auto_sync_posts' ||
203 - name === 'mxchat_auto_sync_pages' ) {
494 + // Use the new AJAX action for submenu fields:
495 + if (name.indexOf('mxchat_prompts_options') !== -1 ||
496 + name === 'mxchat_auto_sync_posts' ||
497 + name === 'mxchat_auto_sync_pages' ||
498 + name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields
204 499 ajaxAction = 'mxchat_save_prompts_setting';
205 500 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
206 501 } else {
502 + // Otherwise, use the existing AJAX action.
207 503 ajaxAction = 'mxchat_save_setting';
208 504 nonce = mxchatAdmin.setting_nonce;
209 505 }
210 -
211 506 // AJAX save request
212 507 $.ajax({
213 508 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
214 509 type: 'POST',
@@ -240,9 +535,9 @@
240 535 });
241 536 });
242 537
243 538 // Reinitialize color pickers when switching tabs
244 - $('.mxchat-nav-tab').on('click.mxchat', function() {
539 + $('.mxchat-tab-button').on('click.mxchat', function() {
245 540 setTimeout(function() {
246 541 $('.my-color-field:visible').wpColorPicker('close');
247 542 }, 100);
248 543 });
@@ -250,24 +545,19 @@
250 545
251 546 // Initialize tabs system
252 547 function initTabs() {
253 548 // Remove any existing handlers first
254 - $('.mxchat-nav-tab').off('click.mxchat');
549 + $('.mxchat-tab-button').off('click.mxchat');
255 550
256 551 // Add new click handlers
257 - $('.mxchat-nav-tab').on('click.mxchat', function(e) {
552 + $('.mxchat-tab-button').on('click.mxchat', function(e) {
258 553 e.preventDefault();
259 554 e.stopPropagation();
260 555
261 556 var $this = $(this);
262 557
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';
269 - }
558 + // Get tab ID from data-tab attribute
559 + var tabId = $this.data('tab') || 'chatbot';
270 560
271 561 // Safety check for empty tabId
272 562 if (!tabId) {
273 563 console.warn('No tab identifier found');
@@ -273,17 +563,17 @@
273 563 console.warn('No tab identifier found');
274 564 return;
275 565 }
276 566
277 - // Update tabs
278 - $('.mxchat-nav-tab').removeClass('mxchat-nav-tab-active');
279 - $this.addClass('mxchat-nav-tab-active');
567 + // Update tab buttons
568 + $('.mxchat-tab-button').removeClass('active');
569 + $this.addClass('active');
280 570
281 571 // Update content areas - with safety check
282 - $('.mxchat-tab-content').removeClass('active').hide();
572 + $('.mxchat-tab-content').removeClass('active');
283 573 var $targetTab = $('#' + tabId);
284 574 if ($targetTab.length) {
285 - $targetTab.addClass('active').show();
575 + $targetTab.addClass('active');
286 576
287 577 // Store active tab
288 578 try {
289 579 localStorage.setItem('mxchat_active_tab', tabId);
@@ -303,14 +593,14 @@
303 593 // Activate initial tab
304 594 try {
305 595 var savedTab = localStorage.getItem('mxchat_active_tab');
306 596 if (savedTab && $('#' + savedTab).length > 0) {
307 - $('.mxchat-nav-tab[href="#' + savedTab + '"]').trigger('click.mxchat');
597 + $('.mxchat-tab-button[data-tab="' + savedTab + '"]').trigger('click.mxchat');
308 598 } else {
309 - $('.mxchat-nav-tab').first().trigger('click.mxchat');
599 + $('.mxchat-tab-button').first().trigger('click.mxchat');
310 600 }
311 601 } catch (e) {
312 - $('.mxchat-nav-tab').first().trigger('click.mxchat');
602 + $('.mxchat-tab-button').first().trigger('click.mxchat');
313 603 }
314 604
315 605 // Attach edit modal event handler
316 606 $(document).on('click', '.mxchat-edit-button', function() {
@@ -318,36 +608,512 @@
318 608 const phrases = $(this).data('phrases');
319 609 mxchatOpenEditModal(intentId, phrases);
320 610 });
321 611
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');
612 +// Toggle visibility handlers
613 +function toggleVisibility(selector) {
614 + $(selector).on('click', function() {
615 + var inputField = $(this).prev('input');
616 + if (inputField.attr('type') === 'password') {
617 + inputField.attr('type', 'text');
618 + $(this).text('Hide');
619 + } else {
620 + inputField.attr('type', 'password');
621 + $(this).text('Show');
622 + }
623 + });
624 +}
625 +
626 +// Initialize all toggle visibility buttons
627 +[
628 + '#toggleApiKeyVisibility',
629 + '#toggleWooCommerceSecretVisibility',
630 + '#toggleVoyageAPIKeyVisibility',
631 + '#toggleLoopsApiKeyVisibility',
632 + '#toggleXaiApiKeyVisibility',
633 + '#toggleClaudeApiKeyVisibility',
634 + '#toggleBraveApiKeyVisibility',
635 + '#toggleWebhookUrlVisibility',
636 + '#toggleSecretKeyVisibility',
637 + '#toggleBotTokenVisibility',
638 + '#toggleDeepSeekApiKeyVisibility',
639 + '#toggleGeminiApiKeyVisibility' // Added Gemini toggle
640 +].forEach(toggleVisibility);
641 +
642 +// Handle API key visibility based on model selection
643 +function setupAPIKeyVisibility() {
644 + // Cache the selectors
645 + const $chatModelSelect = $('#model');
646 + const $embeddingModelSelect = $('#embedding_model');
647 +
648 + // First, locate and mark the API key rows
649 + setupAPIKeyRows();
650 +
651 + // Initial setup based on current selections
652 + updateApiKeyVisibility();
653 +
654 + // Listen for changes to the model selectors
655 + $chatModelSelect.on('change', updateApiKeyVisibility);
656 + $embeddingModelSelect.on('change', updateApiKeyVisibility);
657 +
658 + /**
659 + * Locate and mark rows that contain API key fields
660 + */
661 + function setupAPIKeyRows() {
662 + // Find key rows by their field IDs
663 + const providerMap = {
664 + 'api_key': 'openai',
665 + 'xai_api_key': 'xai',
666 + 'claude_api_key': 'claude',
667 + 'deepseek_api_key': 'deepseek',
668 + 'voyage_api_key': 'voyage',
669 + 'gemini_api_key': 'gemini' // Added Gemini API key mapping
670 + };
671 +
672 + $.each(providerMap, function(fieldId, provider) {
673 + const $field = $('#' + fieldId);
674 + if ($field.length) {
675 + const $row = $field.closest('tr');
676 + $row.addClass('mxchat-setting-row');
677 + $row.attr('data-provider', provider);
332 678 }
333 679 });
334 680 }
335 681
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);
682 + /**
683 + * Updates the visibility of API key fields based on current model selections
684 + */
685 + function updateApiKeyVisibility() {
686 + const chatModel = $chatModelSelect.val();
687 + const embeddingModel = $embeddingModelSelect.val();
688 +
689 + // Determine which providers are needed
690 + const isOpenAIChat = chatModel && chatModel.startsWith('gpt-');
691 + const isXAI = chatModel && chatModel.startsWith('grok-');
692 + const isClaude = chatModel && chatModel.startsWith('claude-');
693 + const isDeepSeek = chatModel && chatModel.startsWith('deepseek-');
694 + const isGemini = chatModel && chatModel.startsWith('gemini-'); // Added Gemini detection
695 +
696 + const isOpenAIEmbedding = embeddingModel && embeddingModel.startsWith('text-embedding-');
697 + const isVoyage = embeddingModel && embeddingModel.startsWith('voyage-');
698 +
699 + // Update API key visibility for each provider
700 + updateWrapperVisibility('openai', isOpenAIChat || isOpenAIEmbedding);
701 + updateWrapperVisibility('xai', isXAI);
702 + updateWrapperVisibility('claude', isClaude);
703 + updateWrapperVisibility('deepseek', isDeepSeek);
704 + updateWrapperVisibility('voyage', isVoyage);
705 + updateWrapperVisibility('gemini', isGemini); // Added Gemini visibility update
706 +
707 + // Update provider-specific notices for OpenAI
708 + if (isOpenAIChat && isOpenAIEmbedding) {
709 + $('div[data-provider="openai"] .api-key-notice').text(
710 + 'Required for your selected chat model and embedding model. Important: You must add credits before use.'
711 + );
712 + } else if (isOpenAIChat) {
713 + $('div[data-provider="openai"] .api-key-notice').text(
714 + 'Required for your selected chat model. Important: You must add credits before use.'
715 + );
716 + } else if (isOpenAIEmbedding) {
717 + $('div[data-provider="openai"] .api-key-notice').text(
718 + 'Required for your selected embedding model. Important: You must add credits before use.'
719 + );
720 + }
721 + }
349 722
723 + /**
724 + * Updates visibility of a specific provider's API key wrapper
725 + */
726 + function updateWrapperVisibility(provider, isVisible) {
727 + const $row = $('tr.mxchat-setting-row[data-provider="' + provider + '"]');
728 +
729 + if (!$row.length) {
730 + console.warn('API key row not found for provider: ' + provider);
731 + return;
732 + }
733 +
734 + if (isVisible) {
735 + $row.show();
736 + if (!$row.hasClass('highlighted')) {
737 + $row.addClass('highlighted');
738 + setTimeout(() => {
739 + $row.removeClass('highlighted');
740 + }, 1500);
741 + }
742 + } else {
743 + $row.hide();
744 + }
745 + }
746 +}
747 +
748 +// Add this to your JavaScript file
749 +function setupMxChatModelSelector() {
750 + const $modelSelect = $('#model');
751 + const $modelSelectorButton = $('<button>', {
752 + type: 'button',
753 + id: 'mxchat_model_selector_btn',
754 + class: 'button-primary mxchat-model-selector-btn',
755 + text: 'Select AI Model'
756 + });
757 +
758 + // Replace the select dropdown with a button
759 + $modelSelect.hide().after($modelSelectorButton);
760 +
761 + // Update button text to show currently selected model
762 + function updateButtonText() {
763 + const selectedModel = $modelSelect.val();
764 + const selectedModelText = $modelSelect.find('option:selected').text();
765 + $modelSelectorButton.text(selectedModelText);
766 + }
767 +
768 + // Initialize button text
769 + updateButtonText();
770 +
771 + // Create and append modal HTML
772 + const modelSelectorModal = `
773 + <div id="mxchat_model_selector_modal" class="mxchat-model-selector-modal">
774 + <div class="mxchat-model-selector-modal-content">
775 + <div class="mxchat-model-selector-modal-header">
776 + <h3>Select AI Model</h3>
777 + <span class="mxchat-model-selector-modal-close">&times;</span>
778 + </div>
779 + <div class="mxchat-model-selector-modal-body">
780 + <div class="mxchat-model-selector-search-container">
781 + <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
782 + </div>
783 + <div class="mxchat-model-selector-categories">
784 + <button class="mxchat-model-category-btn active" data-category="all">All</button>
785 + <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
786 + <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
787 + <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
788 + <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
789 + <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
790 + </div>
791 + <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
792 + </div>
793 + <div class="mxchat-model-selector-modal-footer">
794 + <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
795 + </div>
796 + </div>
797 + </div>
798 + `;
799 +
800 + $('body').append(modelSelectorModal);
801 +
802 + // Populate models grid
803 + function populateModelsGrid(filter = '', category = 'all') {
804 + const $grid = $('#mxchat_models_grid');
805 + $grid.empty();
806 +
807 + const models = {
808 + gemini: [
809 + { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Next-Gen features, speed & multimodal generation' },
810 + { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Cost-efficient with low latency' },
811 + { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Complex reasoning tasks requiring more intelligence' },
812 + { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Fast and versatile performance' },
813 + ],
814 + openai: [
815 + { value: 'gpt-4o', label: 'GPT-4o', description: 'Recommended for most use cases' },
816 + { value: 'gpt-4o-mini', label: 'GPT-4o Mini', description: 'Fast and lightweight' },
817 + { value: 'gpt-4-turbo', label: 'GPT-4 Turbo', description: 'High-performance model' },
818 + { value: 'gpt-4', label: 'GPT-4', description: 'High intelligence model' },
819 + { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', description: 'Affordable and fast' },
820 + ],
821 + claude: [
822 + { value: 'claude-3-7-sonnet-20250219', label: 'Claude 3.7 Sonnet', description: 'Most intelligent Claude model' },
823 + { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet', description: 'Intelligent and balanced' },
824 + { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus', description: 'Highly complex tasks' },
825 + { value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', description: 'Balanced performance' },
826 + { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', description: 'Fastest Claude model' },
827 + ],
828 + xai: [
829 + { value: 'grok-beta', label: 'grok-beta', description: 'Early beta version' },
830 + { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' },
831 + ],
832 + deepseek: [
833 + { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
834 + ],
835 + };
836 +
837 + let allModels = [];
838 + Object.keys(models).forEach(key => {
839 + if (category === 'all' || category === key) {
840 + allModels = allModels.concat(models[key]);
841 + }
842 + });
843 +
844 + // Filter by search term if present
845 + if (filter) {
846 + const lowerFilter = filter.toLowerCase();
847 + allModels = allModels.filter(model =>
848 + model.label.toLowerCase().includes(lowerFilter) ||
849 + model.description.toLowerCase().includes(lowerFilter)
850 + );
851 + }
852 +
853 + // Create model cards
854 + allModels.forEach(model => {
855 + const isSelected = $modelSelect.val() === model.value;
856 + const $modelCard = $(`
857 + <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
858 + <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
859 + <div class="mxchat-model-selector-info">
860 + <h4 class="mxchat-model-selector-title">${model.label}</h4>
861 + <p class="mxchat-model-selector-description">${model.description}</p>
862 + </div>
863 + ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
864 + </div>
865 + `);
866 + $grid.append($modelCard);
867 + });
868 + }
869 +
870 + // Helper function to get icon for each model
871 + function getModelIcon(modelValue) {
872 + if (modelValue.startsWith('gemini-')) return '<span class="dashicons dashicons-google mxchat-model-icon-gemini"></span>';
873 + if (modelValue.startsWith('gpt-')) return '<span class="dashicons dashicons-superhero mxchat-model-icon-openai"></span>';
874 + if (modelValue.startsWith('claude-')) return '<span class="dashicons dashicons-buddicons-buddypress-logo mxchat-model-icon-claude"></span>';
875 + if (modelValue.startsWith('grok-')) return '<span class="dashicons dashicons-twitter mxchat-model-icon-xai"></span>';
876 + if (modelValue.startsWith('deepseek-')) return '<span class="dashicons dashicons-visibility mxchat-model-icon-deepseek"></span>';
877 + return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
878 + }
879 +
880 + // Event handlers
881 + $modelSelectorButton.on('click', function() {
882 + $('#mxchat_model_selector_modal').show();
883 + populateModelsGrid('', 'all');
884 + });
885 +
886 + $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
887 + $('#mxchat_model_selector_modal').hide();
888 + });
889 +
890 + $('.mxchat-model-category-btn').on('click', function() {
891 + $('.mxchat-model-category-btn').removeClass('active');
892 + $(this).addClass('active');
893 + const category = $(this).data('category');
894 + const searchTerm = $('#mxchat_model_search_input').val();
895 + populateModelsGrid(searchTerm, category);
896 + });
897 +
898 + $('#mxchat_model_search_input').on('input', function() {
899 + const searchTerm = $(this).val();
900 + const activeCategory = $('.mxchat-model-category-btn.active').data('category');
901 + populateModelsGrid(searchTerm, activeCategory);
902 + });
903 +
904 + $(document).on('click', '.mxchat-model-selector-card', function() {
905 + const modelValue = $(this).data('value');
906 + $modelSelect.val(modelValue).trigger('change');
907 + updateButtonText();
908 + $('#mxchat_model_selector_modal').hide();
909 + });
910 +
911 + // Close modal when clicking outside
912 + $(window).on('click', function(event) {
913 + if ($(event.target).is('#mxchat_model_selector_modal')) {
914 + $('#mxchat_model_selector_modal').hide();
915 + }
916 + });
917 +}
918 +
919 +// Embedding model selector - completely separate from chat model selector
920 +function setupMxChatEmbeddingModelSelector() {
921 + const $embeddingModelSelect = $('#embedding_model');
922 +
923 + // Skip if the element doesn't exist on the page
924 + if ($embeddingModelSelect.length === 0) {
925 + return;
926 + }
927 +
928 + const $embeddingModelSelectorButton = $('<button>', {
929 + type: 'button',
930 + id: 'mxchat_embedding_model_selector_btn',
931 + class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
932 + text: 'Select Embedding Model'
933 + });
934 +
935 + // Replace the select dropdown with a button
936 + $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
937 +
938 + // Update button text to show currently selected model
939 + function updateButtonText() {
940 + const selectedModel = $embeddingModelSelect.val();
941 + const selectedModelText = $embeddingModelSelect.find('option:selected').text();
942 + $embeddingModelSelectorButton.text(selectedModelText);
943 + }
944 +
945 + // Initialize button text
946 + updateButtonText();
947 +
948 + // Create a unique ID for the modal to avoid conflicts
949 + const embeddingModalId = 'mxchat_embedding_model_selector_modal';
950 +
951 + // Create and append modal HTML with unique IDs
952 + const embeddingModelSelectorModal = `
953 + <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
954 + <div class="mxchat-embedding-model-selector-modal-content">
955 + <div class="mxchat-embedding-model-selector-modal-header">
956 + <h3>Select Embedding Model</h3>
957 + <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
958 + </div>
959 + <div class="mxchat-embedding-model-selector-modal-body">
960 + <div class="mxchat-embedding-model-selector-search-container">
961 + <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
962 + </div>
963 + <div class="mxchat-embedding-model-selector-categories">
964 + <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
965 + <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
966 + <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
967 + </div>
968 + <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
969 + </div>
970 + <div class="mxchat-embedding-model-selector-modal-footer">
971 + <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
972 + </div>
973 + </div>
974 + </div>
975 + `;
976 +
977 + // Use jQuery's append to ensure it doesn't clash with existing modals
978 + $('body').append(embeddingModelSelectorModal);
979 +
980 + // Populate models grid
981 + function populateEmbeddingModelsGrid(filter = '', category = 'all') {
982 + const $grid = $('#mxchat_embedding_models_grid');
983 + $grid.empty();
984 +
985 +// Define embedding models with descriptions and context lengths
986 +const models = {
987 + openai: [
988 + {
989 + value: 'text-embedding-3-small',
990 + label: 'TE3 Small',
991 + description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
992 + },
993 + {
994 + value: 'text-embedding-ada-002',
995 + label: 'Ada 2',
996 + description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
997 + },
998 + {
999 + value: 'text-embedding-3-large',
1000 + label: 'TE3 Large',
1001 + description: 'High-performance embeddings (3072 dimensions, 8K context)'
1002 + }
1003 + ],
1004 + voyage: [
1005 + {
1006 + value: 'voyage-3-large',
1007 + label: 'Voyage-3 Large',
1008 + description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1009 + }
1010 + ]
1011 +};
1012 +
1013 + let allModels = [];
1014 + Object.keys(models).forEach(key => {
1015 + if (category === 'all' || category === key) {
1016 + allModels = allModels.concat(models[key]);
1017 + }
1018 + });
1019 +
1020 + // Filter by search term if present
1021 + if (filter) {
1022 + const lowerFilter = filter.toLowerCase();
1023 + allModels = allModels.filter(model =>
1024 + model.label.toLowerCase().includes(lowerFilter) ||
1025 + model.description.toLowerCase().includes(lowerFilter)
1026 + );
1027 + }
1028 +
1029 + // Create model cards
1030 + allModels.forEach(model => {
1031 + const isSelected = $embeddingModelSelect.val() === model.value;
1032 + const providerClass = model.value.startsWith('voyage-') ? 'mxchat-embedding-model-provider-voyage' : 'mxchat-embedding-model-provider-openai';
1033 +
1034 + const $modelCard = $(`
1035 + <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1036 + <div class="mxchat-embedding-model-selector-icon">
1037 + ${model.value.startsWith('voyage-') ?
1038 + '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>' :
1039 + '<span class="dashicons dashicons-superhero mxchat-embedding-model-icon-openai"></span>'
1040 + }
1041 + </div>
1042 + <div class="mxchat-embedding-model-selector-info">
1043 + <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1044 + <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1045 + </div>
1046 + ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1047 + </div>
1048 + `);
1049 + $grid.append($modelCard);
1050 + });
1051 + }
1052 +
1053 + // Event handlers - use namespaced events to avoid conflicts
1054 + $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1055 + e.stopPropagation(); // Prevent event bubbling
1056 + $('#' + embeddingModalId).show();
1057 + populateEmbeddingModelsGrid('', 'all');
1058 + });
1059 +
1060 + $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1061 + e.stopPropagation(); // Prevent event bubbling
1062 + $('#' + embeddingModalId).hide();
1063 + });
1064 +
1065 + $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1066 + e.stopPropagation(); // Prevent event bubbling
1067 + $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1068 + $(this).addClass('active');
1069 + const category = $(this).data('category');
1070 + const searchTerm = $('#mxchat_embedding_model_search_input').val();
1071 + populateEmbeddingModelsGrid(searchTerm, category);
1072 + });
1073 +
1074 + $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1075 + const searchTerm = $(this).val();
1076 + const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1077 + populateEmbeddingModelsGrid(searchTerm, activeCategory);
1078 + });
1079 +
1080 + // Use a direct selector to avoid conflicts with other card elements
1081 + $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1082 + e.stopPropagation(); // Prevent event bubbling
1083 + const modelValue = $(this).data('value');
1084 +
1085 + // Important: Only update this specific select element
1086 + $embeddingModelSelect.val(modelValue);
1087 +
1088 + // Manually trigger change only on this element
1089 + const changeEvent = new Event('change', { bubbles: true });
1090 + $embeddingModelSelect[0].dispatchEvent(changeEvent);
1091 +
1092 + // Update button text
1093 + updateButtonText();
1094 +
1095 + // Hide modal
1096 + $('#' + embeddingModalId).hide();
1097 + });
1098 +
1099 + // Close modal when clicking outside - use namespaced events
1100 + $(window).on('click.embeddingModelSelector', function(event) {
1101 + if ($(event.target).is('#' + embeddingModalId)) {
1102 + $('#' + embeddingModalId).hide();
1103 + }
1104 + });
1105 +}
1106 +
1107 +// Call this function after the DOM is fully loaded
1108 +$(document).ready(function() {
1109 + setupMxChatModelSelector();
1110 + setupMxChatEmbeddingModelSelector();
1111 +});
1112 +
1113 + // Initialize API key visibility
1114 + setupAPIKeyVisibility();
1115 +
350 1116 // Add Intent Form Submission
351 1117 $('#mxchat-add-intent-form').on('submit', function(event) {
352 1118 $('#mxchat-intent-loading').show();
353 1119 $('#mxchat-intent-loading-text').show();
@@ -544,9 +1310,725 @@
544 1310 }
545 1311 });
546 1312 }
547 1313
1314 + // Function to adjust the textarea height to content
1315 + function adjustTextareaHeight() {
1316 + this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
1317 + this.style.height = this.scrollHeight + 'px'; // Expand to content height
1318 + }
1319 +
1320 + // Function to reset the textarea height to initial
1321 + function resetTextareaHeight() {
1322 + this.style.height = ''; // Remove inline height, reverting to CSS default
1323 + }
1324 +
1325 + // Target the specific textarea by ID
1326 + var $textarea = $('#system_prompt_instructions');
1327 +
1328 + // Bind events
1329 + $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
1330 + .on('blur', resetTextareaHeight); // Reset on blur
1331 +});
1332 +
1333 +document.addEventListener('DOMContentLoaded', function() {
1334 + // Check if we're on the correct page before initializing
1335 + const modal = document.getElementById('mxchat-action-modal');
548 1336
1337 + // Only initialize if the modal exists on this page
1338 + if (modal) {
1339 + //console.log('MXChat Action Modal JS Loaded');
1340 +
1341 + // Initialize the action modal functionality
1342 + initStepBasedActionModal();
1343 + }
1344 +
1345 + // Function to initialize the step-based action modal
1346 + function initStepBasedActionModal() {
1347 + // We already checked for modal existence above, so no need to check again
1348 +
1349 + const actionStep1 = document.getElementById('mxchat-action-step-1');
1350 + const actionStep2 = document.getElementById('mxchat-action-step-2');
1351 + const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
1352 + const searchInput = document.getElementById('action-type-search');
1353 + const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
1354 + const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
1355 + const actionForm = document.getElementById('mxchat-action-form');
1356 + const callbackInput = document.getElementById('callback_function');
1357 + const actionIdField = document.getElementById('edit_action_id');
1358 + const labelField = document.getElementById('intent_label');
1359 + const phrasesField = document.getElementById('action_phrases');
1360 + const formActionType = document.getElementById('form_action_type');
1361 + const nonceContainer = document.getElementById('action-nonce-container');
1362 + const thresholdSlider = document.getElementById('similarity_threshold');
1363 + const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1364 +
1365 + // Rest of your initialization code remains the same...
1366 +
1367 + // Log the structure of one action card for debugging
1368 + if (actionCards.length > 0) {
1369 + //console.log('First action card data attributes:', actionCards[0].dataset);
1370 + //console.log('First action card HTML:', actionCards[0].outerHTML);
1371 + }
1372 +
1373 + // Add click event listeners to category buttons
1374 + categoryButtons.forEach(button => {
1375 + button.addEventListener('click', function() {
1376 + //console.log('Category button clicked:', this.dataset.category);
1377 +
1378 + // Remove active class from all buttons
1379 + categoryButtons.forEach(btn => btn.classList.remove('active'));
1380 +
1381 + // Add active class to clicked button
1382 + this.classList.add('active');
1383 +
1384 + // Get selected category
1385 + const category = this.dataset.category;
1386 +
1387 + // Filter action cards
1388 + filterActionCards(category, searchInput.value);
1389 + });
1390 + });
1391 +
1392 + // Add search functionality
1393 + if (searchInput) {
1394 + searchInput.addEventListener('input', function() {
1395 + // Get active category
1396 + const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
1397 + //console.log('Search input changed, active category:', activeCategory);
1398 +
1399 + // Filter action cards
1400 + filterActionCards(activeCategory, this.value);
1401 + });
1402 + }
1403 +
1404 + // Add click event listeners to action cards
1405 + actionCards.forEach(card => {
1406 + card.addEventListener('click', function() {
1407 + // Get the action data
1408 + const isPro = this.dataset.pro === 'true';
1409 + const isInstalled = this.dataset.installed === 'true';
1410 + const addonName = this.dataset.addon || '';
1411 + const actionValue = this.dataset.value;
1412 + const actionLabel = this.dataset.label;
1413 + const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
1414 + const actionDescription = this.querySelector('p').textContent;
1415 +
1416 + // Pro check using the proper detection method
1417 + const proIsActivated = typeof mxchatAdmin !== 'undefined' &&
1418 + (mxchatAdmin.is_activated === '1' ||
1419 + mxchatAdmin.is_activated === 'true' ||
1420 + mxchatAdmin.is_activated === true);
1421 +
1422 + // Handle different states
1423 + if (isPro && !proIsActivated) {
1424 + // Pro feature but no Pro license
1425 + showProFeatureNotice();
1426 + return;
1427 + }
1428 +
1429 + if (addonName && !isInstalled) {
1430 + // Add-on required but not installed
1431 + const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '') || addonName + ' Add-on';
1432 + showAddonRequiredNotice(addonDisplayName);
1433 + return;
1434 + }
1435 +
1436 + // If we get here, the action is available - proceed as normal
1437 + callbackInput.value = actionValue;
1438 +
1439 + // Update the selected action display in step 2
1440 + document.getElementById('selected-action-title').textContent = actionLabel;
1441 + document.getElementById('selected-action-description').textContent = actionDescription;
1442 + document.getElementById('selected-action-icon').innerHTML =
1443 + `<span class="dashicons dashicons-${actionIcon}"></span>`;
1444 +
1445 + // Set a default label based on the action type (user can change it)
1446 + if (!labelField.value) {
1447 + labelField.value = actionLabel;
1448 + }
1449 +
1450 + // Move to step 2
1451 + actionStep1.classList.remove('active');
1452 + actionStep2.classList.add('active');
1453 +
1454 + // Update modal title
1455 + });
1456 + });
1457 +
1458 + // Back button functionality
1459 + if (backToStep1Btn) {
1460 + backToStep1Btn.addEventListener('click', function() {
1461 + //console.log('Back button clicked');
1462 + actionStep2.classList.remove('active');
1463 + actionStep1.classList.add('active');
1464 + });
1465 + }
1466 +
1467 + // Function to filter action cards by category and search term
1468 + function filterActionCards(category, searchTerm) {
1469 + searchTerm = searchTerm.toLowerCase().trim();
1470 + //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
1471 +
1472 + let visibleCount = 0;
1473 +
1474 + // Show all cards initially with animation
1475 + actionCards.forEach((card, index) => {
1476 + // Reset animation
1477 + card.style.animation = 'none';
1478 + // Trigger reflow
1479 + void card.offsetWidth;
1480 +
1481 + // Determine if card should be visible based on category and search term
1482 + const cardCategory = card.dataset.category || '';
1483 + const matchesCategory = category === 'all' || cardCategory === category;
1484 +
1485 + const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
1486 + const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
1487 + const matchesSearch = searchTerm === '' ||
1488 + cardTitle.includes(searchTerm) ||
1489 + cardDesc.includes(searchTerm);
1490 +
1491 + // Show/hide card with animation
1492 + if (matchesCategory && matchesSearch) {
1493 + card.style.display = 'flex';
1494 + // Staggered animation for cards
1495 + card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
1496 + visibleCount++;
1497 + } else {
1498 + card.style.display = 'none';
1499 + }
1500 + });
1501 +
1502 + //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
1503 + }
1504 +
1505 + // Function to show notice for Pro features
1506 + function showProFeatureNotice() {
1507 + //console.log('Showing Pro feature notice');
1508 + // Check if we already have a notification container
1509 + let noticeContainer = document.querySelector('.mxchat-pro-notice');
1510 +
1511 + if (!noticeContainer) {
1512 + // Create the notice container
1513 + noticeContainer = document.createElement('div');
1514 + noticeContainer.className = 'mxchat-pro-notice';
1515 +
1516 + // Create content
1517 + noticeContainer.innerHTML = `
1518 + <div class="mxchat-pro-notice-content">
1519 + <h3>MxChat Pro Feature</h3>
1520 + <p>This action is available in the Pro version only.</p>
1521 + <div class="mxchat-pro-notice-buttons">
1522 + <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
1523 + <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
1524 + </div>
1525 + </div>
1526 + `;
1527 +
1528 + // Append to body
1529 + document.body.appendChild(noticeContainer);
1530 +
1531 + // Add close functionality
1532 + const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
1533 + closeButton.addEventListener('click', function() {
1534 + noticeContainer.classList.remove('active');
1535 + setTimeout(() => {
1536 + noticeContainer.remove();
1537 + }, 300);
1538 + });
1539 +
1540 + // Click outside to close
1541 + noticeContainer.addEventListener('click', function(e) {
1542 + if (e.target === noticeContainer) {
1543 + closeButton.click();
1544 + }
1545 + });
1546 +
1547 + // Show with animation
1548 + setTimeout(() => {
1549 + noticeContainer.classList.add('active');
1550 + }, 10);
1551 + } else {
1552 + // If it already exists, just make it visible again
1553 + noticeContainer.classList.add('active');
1554 + }
1555 + }
1556 +
1557 + // Function to show notice for add-on requirements
1558 + function showAddonRequiredNotice(addonName) {
1559 + //console.log(`Showing add-on notice for: ${addonName}`);
1560 + // Check if we already have a notification container
1561 + let noticeContainer = document.querySelector('.mxchat-addon-notice');
1562 +
1563 + if (!noticeContainer) {
1564 + // Create the notice container
1565 + noticeContainer = document.createElement('div');
1566 + noticeContainer.className = 'mxchat-addon-notice';
1567 +
1568 + // Create content
1569 + noticeContainer.innerHTML = `
1570 + <div class="mxchat-addon-notice-content">
1571 + <span class="mxchat-addon-notice-icon">🧩</span>
1572 + <h3>Add-on Required</h3>
1573 + <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
1574 + <div class="mxchat-addon-notice-buttons">
1575 + <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
1576 + <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
1577 + </div>
1578 + </div>
1579 + `;
1580 +
1581 + // Append to body
1582 + document.body.appendChild(noticeContainer);
1583 +
1584 + // Add close functionality
1585 + const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
1586 + closeButton.addEventListener('click', function() {
1587 + noticeContainer.classList.remove('active');
1588 + setTimeout(() => {
1589 + noticeContainer.remove();
1590 + }, 300);
1591 + });
1592 +
1593 + // Click outside to close
1594 + noticeContainer.addEventListener('click', function(e) {
1595 + if (e.target === noticeContainer) {
1596 + closeButton.click();
1597 + }
1598 + });
1599 +
1600 + // Show with animation
1601 + setTimeout(() => {
1602 + noticeContainer.classList.add('active');
1603 + }, 10);
1604 + } else {
1605 + // If it already exists, update the content
1606 + const addonNameElement = noticeContainer.querySelector('p strong');
1607 + if (addonNameElement) {
1608 + addonNameElement.textContent = addonName;
1609 + }
1610 +
1611 + // Make it visible again
1612 + noticeContainer.classList.add('active');
1613 + }
1614 + }
1615 +
1616 + // Form submission handling
1617 + if (actionForm) {
1618 + actionForm.addEventListener('submit', function() {
1619 + //console.log('Form submitted');
1620 + document.getElementById('mxchat-action-loading').style.display = 'flex';
1621 + this.querySelector('button[type="submit"]').disabled = true;
1622 + });
1623 + }
1624 + }
1625 +
1626 + // Setup add action buttons (only if we're on the correct page)
1627 + if (modal) {
1628 + // Update the modal open function to support the step-based flow
1629 + window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
1630 + //console.log('Modal opening, edit mode:', isEdit);
1631 +
1632 + // No need to check again, we already verified modal exists
1633 +
1634 + // Get form fields
1635 + const actionIdField = document.getElementById('edit_action_id');
1636 + const labelField = document.getElementById('intent_label');
1637 + const phrasesField = document.getElementById('action_phrases');
1638 + const formActionType = document.getElementById('form_action_type');
1639 + const callbackInput = document.getElementById('callback_function');
1640 + const saveButton = document.getElementById('mxchat-save-action-btn');
1641 + const nonceContainer = document.getElementById('action-nonce-container');
1642 + const thresholdSlider = document.getElementById('similarity_threshold');
1643 + const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1644 + const actionStep1 = document.getElementById('mxchat-action-step-1');
1645 + const actionStep2 = document.getElementById('mxchat-action-step-2');
1646 + const searchInput = document.getElementById('action-type-search');
1647 +
1648 + // Set up modal for edit or create
1649 + if (isEdit) {
1650 + saveButton.textContent = 'Update Action';
1651 + formActionType.value = 'mxchat_edit_intent';
1652 + actionIdField.value = actionId;
1653 + labelField.value = label;
1654 + phrasesField.value = phrases;
1655 + callbackInput.value = callbackFunction;
1656 + thresholdSlider.value = threshold; // Set the current threshold value
1657 + thresholdDisplay.textContent = threshold + '%'; // Update display
1658 +
1659 + // Update the nonce field for editing
1660 + nonceContainer.innerHTML = ''; // Clear existing nonce
1661 + if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
1662 + nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
1663 + }
1664 +
1665 + // For editing, go directly to step 2 and update the selected action display
1666 + actionStep1.classList.remove('active');
1667 + actionStep2.classList.add('active');
1668 +
1669 + // Find the matching action card to get its details
1670 + const actionCards = document.querySelectorAll('.mxchat-action-type-card');
1671 + let foundCard = null;
1672 +
1673 + actionCards.forEach(card => {
1674 + if (card.dataset.value === callbackFunction) {
1675 + foundCard = card;
1676 + }
1677 + });
1678 +
1679 + if (foundCard) {
1680 + //console.log('Found matching action card for:', callbackFunction);
1681 + const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
1682 + const actionIconElement = foundCard.querySelector('.dashicons');
1683 + const actionIcon = actionIconElement
1684 + ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
1685 + : 'admin-generic';
1686 + const actionDescription = foundCard.querySelector('p')?.textContent || '';
1687 +
1688 + document.getElementById('selected-action-title').textContent = actionLabel;
1689 + document.getElementById('selected-action-description').textContent = actionDescription;
1690 + document.getElementById('selected-action-icon').innerHTML =
1691 + `<span class="dashicons dashicons-${actionIcon}"></span>`;
1692 + } else {
1693 + //console.log('No matching action card found for:', callbackFunction);
1694 + // Fallback if we can't find the card
1695 + document.getElementById('selected-action-title').textContent = label;
1696 + document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
1697 + document.getElementById('selected-action-icon').innerHTML =
1698 + `<span class="dashicons dashicons-admin-generic"></span>`;
1699 + }
1700 + } else {
1701 + //console.log('Setting up create mode');
1702 + saveButton.textContent = 'Save Action';
1703 + formActionType.value = 'mxchat_add_intent';
1704 + actionIdField.value = '';
1705 + labelField.value = '';
1706 + phrasesField.value = '';
1707 + callbackInput.value = '';
1708 + thresholdSlider.value = 85; // Default value for new actions
1709 + thresholdDisplay.textContent = '85%'; // Default display
1710 +
1711 + // Update the nonce field for adding
1712 + nonceContainer.innerHTML = ''; // Clear existing nonce
1713 + if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
1714 + nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
1715 + }
1716 +
1717 + // For creating new, start at step 1
1718 + actionStep1.classList.add('active');
1719 + actionStep2.classList.remove('active');
1720 + }
1721 +
1722 + // Show modal with animation
1723 + modal.style.display = 'flex';
1724 + requestAnimationFrame(() => {
1725 + modal.classList.add('active');
1726 + });
1727 +
1728 + // Set up close handlers
1729 + const closeModal = () => {
1730 + //console.log('Closing modal');
1731 + modal.classList.remove('active');
1732 + setTimeout(() => {
1733 + modal.style.display = 'none';
1734 + }, 300); // Match the CSS transition time
1735 + };
1736 +
1737 + // Close button handler
1738 + const closeBtn = modal.querySelector('.mxchat-modal-close');
1739 + if (closeBtn) {
1740 + closeBtn.onclick = closeModal;
1741 + }
1742 +
1743 + // Cancel button handler
1744 + const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
1745 + if (cancelBtns) {
1746 + cancelBtns.forEach(btn => {
1747 + btn.onclick = closeModal;
1748 + });
1749 + }
1750 +
1751 + // Click outside modal to close
1752 + modal.onclick = (e) => {
1753 + if (e.target === modal) {
1754 + closeModal();
1755 + }
1756 + };
1757 +
1758 + // Escape key to close modal
1759 + document.addEventListener('keydown', function(e) {
1760 + if (e.key === 'Escape' && modal.classList.contains('active')) {
1761 + closeModal();
1762 + }
1763 + }, { once: true });
1764 +
1765 + // Focus appropriate field based on current step
1766 + if (isEdit || actionStep2.classList.contains('active')) {
1767 + if (labelField) labelField.focus();
1768 + } else {
1769 + if (searchInput) searchInput.focus();
1770 + }
1771 +
1772 + return closeModal; // Return close function for external use
1773 + };
1774 +
1775 + // Setup add action buttons
1776 + const addActionBtn = document.getElementById('mxchat-add-action-btn');
1777 + if (addActionBtn) {
1778 + //console.log('Add action button found');
1779 + addActionBtn.onclick = () => window.mxchatOpenActionModal();
1780 + }
1781 +
1782 + const createFirstAction = document.getElementById('mxchat-create-first-action');
1783 + if (createFirstAction) {
1784 + //console.log('Create first action button found');
1785 + createFirstAction.onclick = () => window.mxchatOpenActionModal();
1786 + }
1787 +
1788 + // Setup edit buttons
1789 + const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
1790 + //console.log('Edit buttons found:', editButtons.length);
1791 + editButtons.forEach(button => {
1792 + button.onclick = () => {
1793 + const actionId = button.dataset.actionId;
1794 + const phrases = button.dataset.phrases;
1795 + const label = button.dataset.label;
1796 + const threshold = button.dataset.threshold || 85;
1797 + const callbackFunction = button.dataset.callbackFunction;
1798 +
1799 + window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction);
1800 + };
1801 + });
1802 + }
549 1803 });
550 1804
1805 +jQuery(document).ready(function($) {
1806 + // Toggle custom post types container
1807 + $('#mxchat-custom-post-types-toggle').on('click', function(e) {
1808 + e.preventDefault();
1809 +
1810 + $('#mxchat-custom-post-types-container').slideToggle(300);
1811 +
1812 + // Rotate the toggle icon
1813 + const $icon = $(this).find('.mxchat-accordion-icon');
1814 + if ($('#mxchat-custom-post-types-container').is(':visible')) {
1815 + $icon.css('transform', 'rotate(180deg)');
1816 + $(this).closest('.mxchat-settings-accordion').addClass('active');
1817 + } else {
1818 + $icon.css('transform', 'rotate(0deg)');
1819 + $(this).closest('.mxchat-settings-accordion').removeClass('active');
1820 + }
1821 + });
1822 +
1823 + // If there are any selections made, auto-expand the container
1824 + function autoExpandIfNeeded() {
1825 + // Check if any checkbox in the container is checked
1826 + const hasCheckedItems = $('#mxchat-custom-post-types-container input[type="checkbox"]:checked').length > 0;
1827 +
1828 + if (hasCheckedItems) {
1829 + $('#mxchat-custom-post-types-container').show();
1830 + $('#mxchat-custom-post-types-toggle .mxchat-accordion-icon').css('transform', 'rotate(180deg)');
1831 + $('.mxchat-settings-accordion').addClass('active');
1832 + }
1833 + }
1834 +
1835 + // Run on page load
1836 + autoExpandIfNeeded();
1837 +});
551 1838
552 -
1839 +jQuery(document).ready(function($) {
1840 + // Check if we're on the right admin page with status updating
1841 + if ($('.mxchat-status-card').length > 0) {
1842 + // Initialize AJAX status updates
1843 + initStatusUpdates();
1844 + }
1845 +
1846 + function initStatusUpdates() {
1847 + // Get the refresh interval (default to 5 seconds if not set)
1848 + const refreshInterval = parseInt(mxchatAdmin.status_refresh_interval || 5000);
1849 +
1850 + // Check if the status cards exist
1851 + const hasSitemapStatus = $('.mxchat-status-card').length > 0;
1852 +
1853 + if (hasSitemapStatus) {
1854 + // Start the interval for automatic updates
1855 + const updateIntervalId = setInterval(function() {
1856 + fetchStatusUpdates();
1857 + }, refreshInterval);
1858 +
1859 + // Attach event listener to stop button to clear the interval
1860 + $('.mxchat-stop-form').on('submit', function() {
1861 + clearInterval(updateIntervalId);
1862 + });
1863 +
1864 + // Do an initial fetch
1865 + fetchStatusUpdates();
1866 + }
1867 + }
1868 +
1869 + function fetchStatusUpdates() {
1870 + // If user is actively viewing the failed URLs, don't refresh as frequently
1871 + const $details = $('.mxchat-failed-urls-container details');
1872 + const isUserViewing = $details.length > 0 && $details.prop('open');
1873 +
1874 + // If details are open, we'll refresh at a slower rate (or not at all)
1875 + if (isUserViewing) {
1876 + // Optional: Skip this refresh completely when details are open
1877 + // return;
1878 +
1879 + // Alternative: Update less frequently when details are open
1880 + setTimeout(function() {
1881 + performStatusUpdate();
1882 + }, 5000); // Slow down updates to every 5 seconds when details are open
1883 + } else {
1884 + performStatusUpdate();
1885 + }
1886 + }
1887 +
1888 + function performStatusUpdate() {
1889 + $.ajax({
1890 + url: ajaxurl,
1891 + type: 'POST',
1892 + data: {
1893 + action: 'mxchat_get_status_updates',
1894 + nonce: mxchatAdmin.status_nonce
1895 + },
1896 + success: function(response) {
1897 + if (response && response.is_processing) {
1898 + updateStatusUI(response);
1899 + }
1900 + },
1901 + error: function(xhr, status, error) {
1902 + console.error('Status update failed:', error);
1903 + }
1904 + });
1905 + }
1906 +
1907 + function updateStatusUI(data) {
1908 + // Update sitemap status if available
1909 + if (data.sitemap_status) {
1910 + // Update basic info
1911 + const sitemapStatus = data.sitemap_status;
1912 + $('.mxchat-progress-fill').css('width', sitemapStatus.percentage + '%');
1913 + $('.mxchat-status-details p').text(
1914 + 'Progress: ' + sitemapStatus.processed_urls + ' of ' +
1915 + sitemapStatus.total_urls + ' URLs (' + sitemapStatus.percentage + '%)'
1916 + );
1917 +
1918 + // Check if details is already open before updating
1919 + const isDetailsOpen = $('.mxchat-failed-urls-container details').prop('open');
1920 +
1921 + // Update errors display
1922 + const $errorContainer = $('.mxchat-error-notice');
1923 + if ($errorContainer.length === 0 && (sitemapStatus.error || sitemapStatus.last_error || sitemapStatus.failed_urls_list.length > 0)) {
1924 + // Create error container if it doesn't exist
1925 + $('.mxchat-status-details').append('<div class="mxchat-error-notice"></div>');
1926 + }
1927 +
1928 + // Update or create error notices
1929 + const $newErrorContainer = $('.mxchat-error-notice');
1930 + if ($newErrorContainer.length > 0) {
1931 + let errorHTML = '';
1932 +
1933 + if (sitemapStatus.error) {
1934 + errorHTML += '<p class="error">' + sitemapStatus.error + '</p>';
1935 + }
1936 +
1937 + if (sitemapStatus.last_error) {
1938 + errorHTML += '<p class="last-error">Last error: ' + sitemapStatus.last_error + '</p>';
1939 + }
1940 +
1941 + // Add failed URLs list
1942 + if (sitemapStatus.failed_urls_list && sitemapStatus.failed_urls_list.length > 0) {
1943 + errorHTML += '<div class="mxchat-failed-urls-container">';
1944 + errorHTML += '<h4>Failed URLs (' + sitemapStatus.failed_urls_list.length + ')</h4>';
1945 +
1946 + // Set the 'open' attribute based on previous state
1947 + errorHTML += '<details' + (isDetailsOpen ? ' open' : '') + '>';
1948 + errorHTML += '<summary>Show Failed URLs</summary>';
1949 + errorHTML += '<div class="mxchat-failed-urls-list">';
1950 +
1951 + // Sort by most recent first
1952 + const sortedFailedUrls = [...sitemapStatus.failed_urls_list].sort((a, b) => b.time - a.time);
1953 +
1954 + // Limit to at most 50 URLs to display
1955 + const displayUrls = sortedFailedUrls.slice(0, 50);
1956 +
1957 + displayUrls.forEach(item => {
1958 + const timeAgo = formatTimeAgo(item.time);
1959 + errorHTML += '<div class="mxchat-failed-url-item">';
1960 + errorHTML += '<span class="mxchat-failed-url-address">' +
1961 + '<a href="' + item.url + '" target="_blank">' + truncateUrl(item.url) + '</a></span>';
1962 + errorHTML += '<span class="mxchat-failed-url-error">' + item.error + '</span>';
1963 + errorHTML += '<span class="mxchat-failed-url-time">' + timeAgo + '</span>';
1964 + errorHTML += '</div>';
1965 + });
1966 +
1967 + if (sitemapStatus.failed_urls_list.length > 50) {
1968 + errorHTML += '<div class="mxchat-failed-urls-more">+ ' +
1969 + (sitemapStatus.failed_urls_list.length - 50) +
1970 + ' more failed URLs not shown</div>';
1971 + }
1972 +
1973 + errorHTML += '</div>'; // End of failed-urls-list
1974 + errorHTML += '</details>';
1975 + errorHTML += '</div>'; // End of failed-urls-container
1976 + }
1977 +
1978 + $newErrorContainer.html(errorHTML);
1979 +
1980 + // Additionally, add a click handler to pause refreshes when viewing details
1981 + $('.mxchat-failed-urls-container details').on('toggle', function() {
1982 + if (this.open) {
1983 + // User opened the details - set a flag
1984 + $(this).data('user-opened', true);
1985 + } else {
1986 + // User closed the details - remove the flag
1987 + $(this).data('user-opened', false);
1988 + }
1989 + });
1990 + }
1991 + }
1992 + }
1993 +
1994 + // Helper function to format time ago
1995 + function formatTimeAgo(timestamp) {
1996 + const now = Math.floor(Date.now() / 1000);
1997 + const seconds = now - timestamp;
1998 +
1999 + if (seconds < 60) {
2000 + return seconds + ' seconds ago';
2001 + } else if (seconds < 3600) {
2002 + return Math.floor(seconds / 60) + ' minutes ago';
2003 + } else if (seconds < 86400) {
2004 + return Math.floor(seconds / 3600) + ' hours ago';
2005 + } else {
2006 + return Math.floor(seconds / 86400) + ' days ago';
2007 + }
2008 + }
2009 +
2010 + // Helper function to truncate long URLs
2011 + function truncateUrl(url) {
2012 + const maxLength = 50;
2013 + if (url.length <= maxLength) return url;
2014 +
2015 + // Remove protocol
2016 + let displayUrl = url.replace(/^https?:\/\//, '');
2017 +
2018 + if (displayUrl.length <= maxLength) return displayUrl;
2019 +
2020 + // Keep the domain and truncate the path
2021 + const domainMatch = displayUrl.match(/^([^\/]+)\//);
2022 + if (domainMatch) {
2023 + const domain = domainMatch[1];
2024 + const path = displayUrl.substring(domain.length);
2025 +
2026 + if (path.length > 10) {
2027 + return domain + path.substring(0, maxLength - domain.length - 3) + '...';
2028 + }
2029 + }
2030 +
2031 + // Final fallback for very long strings
2032 + return displayUrl.substring(0, maxLength - 3) + '...';
2033 + }
2034 +});