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

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

668 lines 25.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Simple debounce function implementation
2 function debounce(func, wait) {
3 let timeout;
4 return function executedFunction(...args) {
5 const later = () => {
6 clearTimeout(timeout);
7 func(...args);
8 };
9 clearTimeout(timeout);
10 timeout = setTimeout(later, wait);
11 };
12 }
13
14 // Helper function to open edit modal
15 function mxchatOpenEditModal(intentId, phrases) {
16 const modal = document.getElementById('mxchat-edit-modal');
17 if (!modal) return;
18
19 // Get form fields
20 const intentIdField = document.getElementById('edit_intent_id');
21 const phrasesField = document.getElementById('edit_phrases');
22
23 // Set values
24 intentIdField.value = intentId;
25 phrasesField.value = phrases;
26
27 // Show modal with animation
28 modal.style.display = 'flex';
29 requestAnimationFrame(() => {
30 modal.classList.add('active');
31 });
32
33 // Set up close handlers
34 const closeModal = () => {
35 modal.classList.remove('active');
36 setTimeout(() => {
37 modal.style.display = 'none';
38 }, 300); // Match the CSS transition time
39 };
40
41 // Close button handler
42 const closeBtn = modal.querySelector('.mxchat-modal-close');
43 if (closeBtn) {
44 closeBtn.onclick = closeModal;
45 }
46
47 // Cancel button handler
48 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
49 if (cancelBtn) {
50 cancelBtn.onclick = closeModal;
51 }
52
53 // Click outside modal to close
54 modal.onclick = (e) => {
55 if (e.target === modal) {
56 closeModal();
57 }
58 };
59
60 // Focus the textarea
61 phrasesField.focus();
62 }
63
64 // Initialize event listeners
65 document.addEventListener('DOMContentLoaded', () => {
66 // Set up edit button handlers
67 document.querySelectorAll('.mxchat-edit-button').forEach(button => {
68 button.onclick = () => {
69 const intentId = button.dataset.intentId;
70 const phrases = button.dataset.phrases;
71 mxchatOpenEditModal(intentId, phrases);
72 };
73 });
74 });
75
76 jQuery(document).ready(function($) {
77 // Ensure we have a debounce function (use lodash if available, otherwise use our implementation)
78 const useDebounce = (window._ && window._.debounce) ? window._.debounce : debounce;
79
80 // --- AJAX Auto-Save ---
81 const $autosaveSections = $('.mxchat-autosave-section');
82
83 if ($autosaveSections.length) {
84 // Handle real-time range slider value updates
85 $autosaveSections.find('input[type="range"]').on('input', function() {
86 const value = $(this).val();
87 $('#threshold_value').text(value);
88 });
89
90 // Handle all input changes (including range slider)
91 $autosaveSections.find('input, textarea, select').on('change', function() {
92 const $field = $(this);
93 const name = $field.attr('name');
94 let value;
95
96 // Handle different input types
97 if ($field.attr('type') === 'checkbox') {
98 value = $field.is(':checked') ? 'on' : 'off';
99 } else {
100 value = $field.val();
101 }
102
103 // Create feedback container
104 const feedbackContainer = $('<div class="feedback-container"></div>');
105 const spinner = $('<div class="saving-spinner"></div>');
106 const successIcon = $('<div class="success-icon">✔</div>');
107
108 // Position feedback container based on input type
109 if ($field.closest('.toggle-switch').length) {
110 $field.closest('td').append(feedbackContainer);
111 } else if ($field.closest('.mxchat-toggle-switch').length) {
112 $field.closest('.mxchat-toggle-container').append(feedbackContainer);
113 } else if ($field.closest('.slider-container').length) {
114 $field.closest('.slider-container').after(feedbackContainer);
115 } else {
116 $field.after(feedbackContainer);
117 }
118 feedbackContainer.append(spinner);
119
120 // Determine which AJAX action and nonce to use:
121 var ajaxAction, nonce;
122 // 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' ) {
126 ajaxAction = 'mxchat_save_prompts_setting';
127 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
128 } else {
129 // Otherwise, use the existing AJAX action.
130 ajaxAction = 'mxchat_save_setting';
131 nonce = mxchatAdmin.setting_nonce;
132 }
133
134 // AJAX save request
135 $.ajax({
136 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
137 type: 'POST',
138 data: {
139 action: ajaxAction,
140 name: name,
141 value: value,
142 _ajax_nonce: nonce
143 },
144 success: function(response) {
145 if (response.success) {
146 spinner.fadeOut(200, function() {
147 feedbackContainer.append(successIcon);
148 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
149 feedbackContainer.remove();
150 });
151 });
152 } else {
153 alert('Error saving: ' + (response.data?.message || 'Unknown error'));
154 if ($field.attr('type') === 'checkbox') {
155 $field.prop('checked', !$field.is(':checked'));
156 }
157 feedbackContainer.remove();
158 }
159 },
160 error: function() {
161 alert('An error occurred while saving.');
162 if ($field.attr('type') === 'checkbox') {
163 $field.prop('checked', !$field.is(':checked'));
164 }
165 feedbackContainer.remove();
166 }
167 });
168 });
169
170 // Initialize color pickers with debouncing
171 $autosaveSections.find('.my-color-field').each(function() {
172 const $colorField = $(this);
173
174 $(this).wpColorPicker({
175 change: useDebounce(function(event, ui) {
176 // Safety check - ensure we have a valid field and value
177 if (!$colorField || !$colorField.val()) {
178 console.warn('Color picker not ready');
179 return;
180 }
181
182 const name = $colorField.attr('name');
183 const value = $colorField.val();
184
185 if (!name || !value) {
186 console.warn('Missing required color picker values');
187 return;
188 }
189
190 // Create feedback container
191 const feedbackContainer = $('<div class="feedback-container"></div>');
192 const spinner = $('<div class="saving-spinner"></div>');
193 const successIcon = $('<div class="success-icon">✔</div>');
194
195 // Position feedback container
196 $colorField.closest('.wp-picker-container').after(feedbackContainer);
197 feedbackContainer.append(spinner);
198
199 // Determine AJAX action and nonce for color fields:
200 var ajaxAction, nonce;
201 if ( name.indexOf('mxchat_prompts_options') !== -1 ||
202 name === 'mxchat_auto_sync_posts' ||
203 name === 'mxchat_auto_sync_pages' ) {
204 ajaxAction = 'mxchat_save_prompts_setting';
205 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
206 } else {
207 ajaxAction = 'mxchat_save_setting';
208 nonce = mxchatAdmin.setting_nonce;
209 }
210
211 // AJAX save request
212 $.ajax({
213 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
214 type: 'POST',
215 data: {
216 action: ajaxAction,
217 name: name,
218 value: value,
219 _ajax_nonce: nonce
220 },
221 success: function(response) {
222 if (response.success) {
223 spinner.fadeOut(200, function() {
224 feedbackContainer.append(successIcon);
225 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
226 feedbackContainer.remove();
227 });
228 });
229 } else {
230 alert('Error saving: ' + (response.data?.message || 'Unknown error'));
231 feedbackContainer.remove();
232 }
233 },
234 error: function() {
235 alert('An error occurred while saving.');
236 feedbackContainer.remove();
237 }
238 });
239 }, 500)
240 });
241 });
242
243 // Reinitialize color pickers when switching tabs
244 $('.mxchat-tab-button').on('click.mxchat', function() {
245 setTimeout(function() {
246 $('.my-color-field:visible').wpColorPicker('close');
247 }, 100);
248 });
249 }
250
251 // Initialize tabs system
252 function initTabs() {
253 // Remove any existing handlers first
254 $('.mxchat-tab-button').off('click.mxchat');
255
256 // Add new click handlers
257 $('.mxchat-tab-button').on('click.mxchat', function(e) {
258 e.preventDefault();
259 e.stopPropagation();
260
261 var $this = $(this);
262
263 // Get tab ID from data-tab attribute
264 var tabId = $this.data('tab') || 'chatbot';
265
266 // Safety check for empty tabId
267 if (!tabId) {
268 console.warn('No tab identifier found');
269 return;
270 }
271
272 // Update tab buttons
273 $('.mxchat-tab-button').removeClass('active');
274 $this.addClass('active');
275
276 // Update content areas - with safety check
277 $('.mxchat-tab-content').removeClass('active');
278 var $targetTab = $('#' + tabId);
279 if ($targetTab.length) {
280 $targetTab.addClass('active');
281
282 // Store active tab
283 try {
284 localStorage.setItem('mxchat_active_tab', tabId);
285 } catch (e) {
286 console.warn('LocalStorage not available:', e);
287 }
288 } else {
289 console.warn('Tab content #' + tabId + ' not found');
290 }
291 });
292 }
293
294 // Initialize tabs and handle events
295 initTabs();
296 $(document).on('widget-added widget-updated postbox-toggled', initTabs);
297
298 // Activate initial tab
299 try {
300 var savedTab = localStorage.getItem('mxchat_active_tab');
301 if (savedTab && $('#' + savedTab).length > 0) {
302 $('.mxchat-tab-button[data-tab="' + savedTab + '"]').trigger('click.mxchat');
303 } else {
304 $('.mxchat-tab-button').first().trigger('click.mxchat');
305 }
306 } catch (e) {
307 $('.mxchat-tab-button').first().trigger('click.mxchat');
308 }
309
310 // Attach edit modal event handler
311 $(document).on('click', '.mxchat-edit-button', function() {
312 const intentId = $(this).data('intent-id');
313 const phrases = $(this).data('phrases');
314 mxchatOpenEditModal(intentId, phrases);
315 });
316
317 // Toggle visibility handlers
318 function toggleVisibility(selector) {
319 $(selector).on('click', function() {
320 var inputField = $(this).prev('input');
321 if (inputField.attr('type') === 'password') {
322 inputField.attr('type', 'text');
323 $(this).text('Hide');
324 } else {
325 inputField.attr('type', 'password');
326 $(this).text('Show');
327 }
328 });
329 }
330
331 // Initialize all toggle visibility buttons
332 [
333 '#toggleApiKeyVisibility',
334 '#toggleWooCommerceSecretVisibility',
335 '#toggleVoyageAPIKeyVisibility',
336 '#toggleLoopsApiKeyVisibility',
337 '#toggleXaiApiKeyVisibility',
338 '#toggleClaudeApiKeyVisibility',
339 '#toggleBraveApiKeyVisibility',
340 '#toggleWebhookUrlVisibility',
341 '#toggleSecretKeyVisibility',
342 '#toggleBotTokenVisibility',
343 '#toggleDeepSeekApiKeyVisibility'
344 ].forEach(toggleVisibility);
345
346 // Handle API key visibility based on model selection
347 // Handle API key visibility based on model selection
348 function setupAPIKeyVisibility() {
349 // Cache the selectors
350 const $chatModelSelect = $('#model');
351 const $embeddingModelSelect = $('#embedding_model');
352
353 // First, locate and mark the API key rows
354 setupAPIKeyRows();
355
356 // Initial setup based on current selections
357 updateApiKeyVisibility();
358
359 // Listen for changes to the model selectors
360 $chatModelSelect.on('change', updateApiKeyVisibility);
361 $embeddingModelSelect.on('change', updateApiKeyVisibility);
362
363 /**
364 * Locate and mark rows that contain API key fields
365 */
366 function setupAPIKeyRows() {
367 // Find key rows by their field IDs
368 const providerMap = {
369 'api_key': 'openai',
370 'xai_api_key': 'xai',
371 'claude_api_key': 'claude',
372 'deepseek_api_key': 'deepseek',
373 'voyage_api_key': 'voyage'
374 };
375
376 $.each(providerMap, function(fieldId, provider) {
377 const $field = $('#' + fieldId);
378 if ($field.length) {
379 const $row = $field.closest('tr');
380 $row.addClass('mxchat-setting-row');
381 $row.attr('data-provider', provider);
382 }
383 });
384 }
385
386 /**
387 * Updates the visibility of API key fields based on current model selections
388 */
389 function updateApiKeyVisibility() {
390 const chatModel = $chatModelSelect.val();
391 const embeddingModel = $embeddingModelSelect.val();
392
393 // Determine which providers are needed
394 const isOpenAIChat = chatModel && chatModel.startsWith('gpt-');
395 const isXAI = chatModel && chatModel.startsWith('grok-');
396 const isClaude = chatModel && chatModel.startsWith('claude-');
397 const isDeepSeek = chatModel && chatModel.startsWith('deepseek-');
398
399 const isOpenAIEmbedding = embeddingModel && embeddingModel.startsWith('text-embedding-');
400 const isVoyage = embeddingModel && embeddingModel.startsWith('voyage-');
401
402 // Update API key visibility for each provider
403 updateWrapperVisibility('openai', isOpenAIChat || isOpenAIEmbedding);
404 updateWrapperVisibility('xai', isXAI);
405 updateWrapperVisibility('claude', isClaude);
406 updateWrapperVisibility('deepseek', isDeepSeek);
407 updateWrapperVisibility('voyage', isVoyage);
408
409 // Update provider-specific notices for OpenAI
410 if (isOpenAIChat && isOpenAIEmbedding) {
411 $('div[data-provider="openai"] .api-key-notice').text(
412 'Required for your selected chat model and embedding model. Important: You must add credits before use.'
413 );
414 } else if (isOpenAIChat) {
415 $('div[data-provider="openai"] .api-key-notice').text(
416 'Required for your selected chat model. Important: You must add credits before use.'
417 );
418 } else if (isOpenAIEmbedding) {
419 $('div[data-provider="openai"] .api-key-notice').text(
420 'Required for your selected embedding model. Important: You must add credits before use.'
421 );
422 }
423 }
424
425 /**
426 * Updates visibility of a specific provider's API key wrapper
427 */
428 function updateWrapperVisibility(provider, isVisible) {
429 const $row = $('tr.mxchat-setting-row[data-provider="' + provider + '"]');
430
431 if (!$row.length) {
432 console.warn('API key row not found for provider: ' + provider);
433 return;
434 }
435
436 if (isVisible) {
437 $row.show();
438 if (!$row.hasClass('highlighted')) {
439 $row.addClass('highlighted');
440 setTimeout(() => {
441 $row.removeClass('highlighted');
442 }, 1500);
443 }
444 } else {
445 $row.hide();
446 }
447 }
448 }
449
450 // Initialize API key visibility
451 setupAPIKeyVisibility();
452
453 // Add Intent Form Submission
454 $('#mxchat-add-intent-form').on('submit', function(event) {
455 $('#mxchat-intent-loading').show();
456 $('#mxchat-intent-loading-text').show();
457 $(this).find('button[type="submit"]').hide();
458 });
459
460 // Inline Edit Functionality
461 $('.edit-button').on('click', function() {
462 var row = $(this).closest('tr');
463 row.find('.content-view, .url-view').hide();
464 row.find('.content-edit, .url-edit').show();
465 row.find('.edit-button').hide();
466 row.find('.save-button').show();
467 });
468
469 // Save button handler
470 $('.save-button').on('click', function() {
471 var button = $(this);
472 var row = button.closest('tr');
473 var id = button.data('id');
474 var newContent = row.find('.content-edit').val();
475 var newUrl = row.find('.url-edit').val();
476
477 button.prop('disabled', true);
478 button.text('Saving...');
479
480 $.ajax({
481 url: mxchatAdmin.ajax_url,
482 type: 'POST',
483 data: {
484 action: 'mxchat_save_inline_prompt',
485 id: id,
486 article_content: newContent,
487 article_url: newUrl,
488 _ajax_nonce: mxchatAdmin.inline_edit_nonce
489 },
490 success: function(response) {
491 button.prop('disabled', false);
492 button.text('Save');
493
494 if (response.success) {
495 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
496 if (newUrl) {
497 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank">' + newUrl + '</a>');
498 } else {
499 row.find('.url-view').html('N/A');
500 }
501
502 row.find('.content-edit, .url-edit').hide();
503 row.find('.content-view, .url-view').show();
504 row.find('.save-button').hide();
505 row.find('.edit-button').show();
506 } else {
507 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
508 }
509 },
510 error: function() {
511 button.prop('disabled', false);
512 button.text('Save');
513 alert('An error occurred while saving.');
514 }
515 });
516 });
517
518 // Activation handling
519 const form = $('#mxchat-activation-form');
520 const spinner = $('#mxchat-activation-spinner');
521 const submitButton = $('#activate_license_button');
522 const licenseStatus = $('#mxchat-license-status');
523
524 if (form.length && licenseStatus.length && submitButton.length) {
525 function handleActivationResponse(response) {
526 spinner.hide();
527 if (response.success) {
528 licenseStatus.text('Active');
529 licenseStatus.removeClass('inactive').addClass('active');
530 form.hide();
531 } else {
532 licenseStatus.text('Inactive');
533 alert(response.data || 'Activation failed. Please check your input.');
534 submitButton.prop('disabled', false);
535 }
536 }
537
538 form.on('submit', function(event) {
539 event.preventDefault();
540 spinner.show();
541 submitButton.prop('disabled', true);
542
543 var formData = {
544 action: 'mxchat_activate_license',
545 mxchat_pro_email: $('#mxchat_pro_email').val(),
546 mxchat_activation_key: $('#mxchat_activation_key').val(),
547 security: mxchatAdmin.license_nonce
548 };
549
550 $.post(mxchatAdmin.ajax_url, formData, function(response) {
551 handleActivationResponse(response);
552 }).fail(function() {
553 alert('Server error. Please try again.');
554 spinner.hide();
555 submitButton.prop('disabled', false);
556 });
557 });
558 }
559
560 // Questions handling
561 $('.mxchat-add-question').on('click', function () {
562 const container = $('#mxchat-additional-questions-container');
563 const questionCount = container.find('.mxchat-question-row').length + 4;
564 const questionIndex = container.find('.mxchat-question-row').length;
565
566 const newQuestion = `
567 <div class="mxchat-question-row">
568 <input type="text"
569 name="additional_popular_questions[]"
570 placeholder="Enter Additional Popular Question ${questionCount}"
571 class="regular-text mxchat-question-input"
572 data-question-index="${questionIndex}" />
573 <button type="button" class="button mxchat-remove-question"
574 aria-label="Remove question">Remove</button>
575 </div>
576 `;
577 container.append(newQuestion);
578 });
579
580 $(document).on('click', '.mxchat-remove-question', function () {
581 $(this).closest('.mxchat-question-row').remove();
582 saveQuestions();
583 });
584
585 $(document).on('change', '.mxchat-question-input', function() {
586 saveQuestions();
587 });
588
589 function saveQuestions() {
590 const questions = [];
591 $('.mxchat-question-input').each(function() {
592 const value = $(this).val().trim();
593 if (value) {
594 questions.push(value);
595 }
596 });
597
598 const feedbackContainer = $('<div class="feedback-container"></div>');
599 const spinner = $('<div class="saving-spinner"></div>');
600 const successIcon = $('<div class="success-icon">✔</div>');
601
602 // Append feedback after the add button
603 $('.mxchat-add-question').after(feedbackContainer);
604 feedbackContainer.append(spinner);
605
606 // Save via AJAX
607 $.ajax({
608 url: mxchatAdmin.ajax_url,
609 type: 'POST',
610 data: {
611 action: 'mxchat_save_setting',
612 name: 'additional_popular_questions',
613 value: JSON.stringify(questions),
614 _ajax_nonce: mxchatAdmin.setting_nonce
615 },
616 success: function(response) {
617 if (response.success) {
618 spinner.fadeOut(200, function() {
619 feedbackContainer.append(successIcon);
620 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
621 feedbackContainer.remove();
622 });
623 });
624 } else {
625 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
626 feedbackContainer.remove();
627 }
628 },
629 error: function() {
630 alert('An error occurred while saving questions.');
631 feedbackContainer.remove();
632 }
633 });
634 }
635
636 // Live agent status handler
637 const statusToggle = document.getElementById('live_agent_status');
638 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
639 if (statusToggle && statusText) {
640 statusToggle.addEventListener('change', function() {
641 // Update display text
642 statusText.textContent = this.checked ? 'Online' : 'Offline';
643
644 // Send the correct on/off value to the server
645 if (window.mxchatSaveSetting) {
646 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
647 }
648 });
649 }
650
651 // Function to adjust the textarea height to content
652 function adjustTextareaHeight() {
653 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
654 this.style.height = this.scrollHeight + 'px'; // Expand to content height
655 }
656
657 // Function to reset the textarea height to initial
658 function resetTextareaHeight() {
659 this.style.height = ''; // Remove inline height, reverting to CSS default
660 }
661
662 // Target the specific textarea by ID
663 var $textarea = $('#system_prompt_instructions');
664
665 // Bind events
666 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
667 .on('blur', resetTextareaHeight); // Reset on blur
668 });