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

2,034 lines 89.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Simple debounce function implementation
2 function debounce(func, wait) {
3 let timeout;
4 return function executedFunction(...args) {
5 const later = () => {
6 clearTimeout(timeout);
7 func(...args);
8 };
9 clearTimeout(timeout);
10 timeout = setTimeout(later, wait);
11 };
12 }
13
14 // Helper function to open edit modal for intents/actions
15 function mxchatOpenEditModal(intentId, phrases) {
16 const modal = document.getElementById('mxchat-edit-modal');
17 if (!modal) return;
18
19 // Get form fields
20 const intentIdField = document.getElementById('edit_intent_id');
21 const phrasesField = document.getElementById('edit_phrases');
22
23 // Set values
24 intentIdField.value = intentId;
25 phrasesField.value = phrases;
26
27 // Show modal with animation
28 modal.style.display = 'flex';
29 requestAnimationFrame(() => {
30 modal.classList.add('active');
31 });
32
33 // Set up close handlers
34 const closeModal = () => {
35 modal.classList.remove('active');
36 setTimeout(() => {
37 modal.style.display = 'none';
38 }, 300); // Match the CSS transition time
39 };
40
41 // Close button handler
42 const closeBtn = modal.querySelector('.mxchat-modal-close');
43 if (closeBtn) {
44 closeBtn.onclick = closeModal;
45 }
46
47 // Cancel button handler
48 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
49 if (cancelBtn) {
50 cancelBtn.onclick = closeModal;
51 }
52
53 // Click outside modal to close
54 modal.onclick = (e) => {
55 if (e.target === modal) {
56 closeModal();
57 }
58 };
59
60 // Focus the textarea
61 phrasesField.focus();
62 }
63
64 // Updated mxchatOpenActionModal function to integrate with the new selector
65 function mxchatOpenActionModal(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
66 const modal = document.getElementById('mxchat-action-modal');
67 if (!modal) return;
68
69 // Get form fields
70 const actionIdField = document.getElementById('edit_action_id');
71 const labelField = document.getElementById('intent_label');
72 const phrasesField = document.getElementById('action_phrases');
73 const formActionType = document.getElementById('form_action_type');
74 const callbackGroup = document.getElementById('callback_selection_group');
75 const callbackSelect = document.getElementById('callback_function');
76 const saveButton = document.getElementById('mxchat-save-action-btn');
77 const nonceContainer = document.getElementById('action-nonce-container');
78 const thresholdSlider = document.getElementById('similarity_threshold');
79 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
80
81 // Set up modal for edit or create
82 if (isEdit) {
83 saveButton.textContent = 'Update Action';
84 formActionType.value = 'mxchat_edit_intent';
85 actionIdField.value = actionId;
86 labelField.value = label;
87 phrasesField.value = phrases;
88 callbackGroup.style.display = 'none'; // Hide callback selection when editing
89 thresholdSlider.value = threshold; // Set the current threshold value
90 thresholdDisplay.textContent = threshold + '%'; // Update display
91
92 // Remove the required attribute when editing
93 callbackSelect.removeAttribute('required');
94
95 // Update the nonce field for editing
96 nonceContainer.innerHTML = ''; // Clear existing nonce
97 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
98 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
99 }
100 } else {
101 saveButton.textContent = 'Save Action';
102 formActionType.value = 'mxchat_add_intent';
103 actionIdField.value = '';
104 labelField.value = '';
105 phrasesField.value = '';
106 callbackGroup.style.display = 'block'; // Show callback selection when creating
107 thresholdSlider.value = 85; // Default value for new actions
108 thresholdDisplay.textContent = '85%'; // Default display
109
110 // Ensure the required attribute is present when adding
111 callbackSelect.setAttribute('required', 'required');
112
113 // Update the nonce field for adding
114 nonceContainer.innerHTML = ''; // Clear existing nonce
115 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
116 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
117 }
118 }
119
120 // Show modal with animation
121 modal.style.display = 'flex';
122 requestAnimationFrame(() => {
123 modal.classList.add('active');
124 });
125
126 // Set up close handlers
127 const closeModal = () => {
128 modal.classList.remove('active');
129 setTimeout(() => {
130 modal.style.display = 'none';
131 }, 300); // Match the CSS transition time
132 };
133
134 // Close button handler
135 const closeBtn = modal.querySelector('.mxchat-modal-close');
136 if (closeBtn) {
137 closeBtn.onclick = closeModal;
138 }
139
140 // Cancel button handler
141 const cancelBtn = modal.querySelector('.mxchat-modal-cancel');
142 if (cancelBtn) {
143 cancelBtn.onclick = closeModal;
144 }
145
146 // Click outside modal to close
147 modal.onclick = (e) => {
148 if (e.target === modal) {
149 closeModal();
150 }
151 };
152
153 // Escape key to close modal
154 document.addEventListener('keydown', function(e) {
155 if (e.key === 'Escape' && modal.classList.contains('active')) {
156 closeModal();
157 }
158 }, { once: true });
159
160 // Focus the first field
161 labelField.focus();
162
163 // Dispatch an event for the action type selector to catch
164 const event = new CustomEvent('mxchatModalOpened', {
165 detail: {
166 isEdit: isEdit,
167 callbackFunction: callbackFunction || (isEdit ? callbackSelect.value : '')
168 }
169 });
170 document.dispatchEvent(event);
171
172 return closeModal; // Return close function for external use
173 }
174
175 // Initialize event listeners
176 document.addEventListener('DOMContentLoaded', () => {
177 // Set up edit button handlers for intents
178 document.querySelectorAll('.mxchat-edit-button').forEach(button => {
179 button.onclick = () => {
180 const intentId = button.dataset.intentId;
181 const phrases = button.dataset.phrases;
182 mxchatOpenEditModal(intentId, phrases);
183 };
184 });
185
186 // Set up edit button handlers for actions (new functionality)
187 document.querySelectorAll('.mxchat-action-card .mxchat-edit-button').forEach(button => {
188 button.onclick = () => {
189 const actionId = button.dataset.actionId;
190 const phrases = button.dataset.phrases;
191 const label = button.dataset.label;
192 const threshold = button.dataset.threshold || 85;
193 const callbackFunction = button.dataset.callbackFunction; // Add this data attribute
194 mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction);
195 };
196 });
197
198 // Set up add new action buttons (new functionality)
199 const addActionBtn = document.getElementById('mxchat-add-action-btn');
200 if (addActionBtn) {
201 addActionBtn.onclick = () => mxchatOpenActionModal();
202 }
203
204 const createFirstAction = document.getElementById('mxchat-create-first-action');
205 if (createFirstAction) {
206 createFirstAction.onclick = () => mxchatOpenActionModal();
207 }
208
209 // Setup category-specific new action buttons (new functionality)
210 document.querySelectorAll('.mxchat-new-action-button').forEach(button => {
211 button.onclick = () => {
212 const category = button.closest('.mxchat-new-action-card').dataset.category;
213 const closeModal = mxchatOpenActionModal();
214
215 // Pre-select the appropriate callback based on category
216 if (category) {
217 const callbackSelect = document.getElementById('callback_function');
218 if (callbackSelect) {
219 setTimeout(() => {
220 // Map categories to default callbacks
221 const categoryToCallback = {
222 'data_collection': 'mxchat_handle_form_collection',
223 'integrations': 'mxchat_handle_slack_message',
224 'custom_actions': 'mxchat_handle_custom_action',
225 'recommendations': 'mxchat_handle_product_recommendations'
226 // Add more mappings as needed
227 };
228
229 if (categoryToCallback[category]) {
230 callbackSelect.value = categoryToCallback[category];
231 }
232 }, 100);
233 }
234 }
235 };
236 });
237
238 // Handle action toggle switches (new functionality)
239 document.querySelectorAll('.mxchat-action-toggle').forEach(toggle => {
240 toggle.onchange = function() {
241 const actionId = this.dataset.actionId;
242 const isEnabled = this.checked;
243
244 // Show loading indicator
245 const loadingEl = document.getElementById('mxchat-action-loading');
246 if (loadingEl) loadingEl.style.display = 'flex';
247
248 // Send AJAX request to update status
249 fetch(ajaxurl, {
250 method: 'POST',
251 headers: {
252 'Content-Type': 'application/x-www-form-urlencoded',
253 },
254 body: new URLSearchParams({
255 action: 'mxchat_toggle_action',
256 intent_id: actionId,
257 enabled: isEnabled ? 1 : 0,
258 nonce: mxchatAdmin.toggle_action_nonce // Use the correct nonce
259 })
260 })
261 .then(response => response.json())
262 .then(data => {
263 if (!data.success) {
264 alert('Failed to update action status: ' + (data.data?.message || 'Unknown error'));
265 this.checked = !isEnabled; // Revert the toggle
266 }
267 })
268 .catch(error => {
269 console.error('Error:', error);
270 alert('Server error. Please try again.');
271 this.checked = !isEnabled; // Revert the toggle
272 })
273 .finally(() => {
274 if (loadingEl) loadingEl.style.display = 'none';
275 });
276 };
277 });
278
279 // Handle threshold sliders in action cards (new functionality)
280 document.querySelectorAll('.mxchat-threshold-slider').forEach(slider => {
281 slider.oninput = function() {
282 const actionId = this.id.replace('intent_threshold_', '');
283 document.getElementById('threshold_output_' + actionId).textContent = this.value + '%';
284 };
285 });
286
287 // Handle threshold save buttons in action cards (new functionality)
288 document.querySelectorAll('.mxchat-threshold-save').forEach(button => {
289 button.onclick = function(e) {
290 e.preventDefault();
291 const form = this.closest('form');
292 const intentId = form.querySelector('input[name="intent_id"]').value;
293 const threshold = form.querySelector('input[name="intent_threshold"]').value;
294 const nonce = form.querySelector('input[name="_wpnonce"]').value;
295
296 // Show loading indicator
297 const loadingEl = document.getElementById('mxchat-action-loading');
298 if (loadingEl) loadingEl.style.display = 'flex';
299
300 // Send AJAX request
301 fetch(ajaxurl, {
302 method: 'POST',
303 headers: {
304 'Content-Type': 'application/x-www-form-urlencoded',
305 },
306 body: new URLSearchParams({
307 action: 'mxchat_update_intent_threshold',
308 intent_id: intentId,
309 intent_threshold: threshold,
310 _wpnonce: nonce
311 })
312 })
313 .then(response => response.json())
314 .then(data => {
315 if (data.success) {
316 // Visual feedback of success
317 const card = this.closest('.mxchat-action-card');
318 card.style.background = 'rgba(120, 115, 245, 0.1)';
319 setTimeout(() => {
320 card.style.background = 'white';
321 }, 300);
322 } else {
323 alert('Failed to update threshold: ' + (data.data?.message || 'Unknown error'));
324 }
325 })
326 .catch(error => {
327 console.error('Error:', error);
328 alert('Server error. Please try again.');
329 })
330 .finally(() => {
331 if (loadingEl) loadingEl.style.display = 'none';
332 });
333 };
334 });
335 });
336
337 jQuery(document).ready(function($) {
338 // Ensure we have a debounce function (use lodash if available, otherwise use our implementation)
339 const useDebounce = (window._ && window._.debounce) ? window._.debounce : debounce;
340
341 // --- AJAX Auto-Save ---
342 const $autosaveSections = $('.mxchat-autosave-section');
343
344 if ($autosaveSections.length) {
345 // Handle real-time range slider value updates
346 $autosaveSections.find('input[type="range"]').on('input', function() {
347 const value = $(this).val();
348 $('#threshold_value').text(value);
349 });
350
351 // Handle all input changes (including range slider)
352 $autosaveSections.find('input, textarea, select').on('change', function() {
353 const $field = $(this);
354 const name = $field.attr('name');
355 let value;
356
357 // Handle different input types
358 if ($field.attr('type') === 'checkbox') {
359 value = $field.is(':checked') ? 'on' : 'off';
360 } else {
361 value = $field.val();
362 }
363
364 // Create feedback container
365 const feedbackContainer = $('<div class="feedback-container"></div>');
366 const spinner = $('<div class="saving-spinner"></div>');
367 const successIcon = $('<div class="success-icon">✔</div>');
368
369 // Position feedback container based on input type
370 if ($field.closest('.toggle-switch').length) {
371 $field.closest('td').append(feedbackContainer);
372 } else if ($field.closest('.mxchat-toggle-switch').length) {
373 $field.closest('.mxchat-toggle-container').append(feedbackContainer);
374 } else if ($field.closest('.slider-container').length) {
375 $field.closest('.slider-container').after(feedbackContainer);
376 } else {
377 $field.after(feedbackContainer);
378 }
379 feedbackContainer.append(spinner);
380
381 // Determine which AJAX action and nonce to use:
382 var ajaxAction, nonce;
383 // Use the new AJAX action for submenu fields:
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
388 ajaxAction = 'mxchat_save_prompts_setting';
389 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
390 } else {
391 // Otherwise, use the existing AJAX action.
392 ajaxAction = 'mxchat_save_setting';
393 nonce = mxchatAdmin.setting_nonce;
394 }
395
396 // AJAX save request
397 $.ajax({
398 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
399 type: 'POST',
400 data: {
401 action: ajaxAction,
402 name: name,
403 value: value,
404 _ajax_nonce: nonce
405 },
406 success: function(response) {
407 if (response.success) {
408 spinner.fadeOut(200, function() {
409 feedbackContainer.append(successIcon);
410 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
411 feedbackContainer.remove();
412 });
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 }
419 } else {
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;
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
446 feedbackContainer.remove();
447 }
448 },
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
454 if ($field.attr('type') === 'checkbox') {
455 $field.prop('checked', !$field.is(':checked'));
456 }
457
458 feedbackContainer.remove();
459 }
460 });
461 });
462
463 // Initialize color pickers with debouncing
464 $autosaveSections.find('.my-color-field').each(function() {
465 const $colorField = $(this);
466
467 $(this).wpColorPicker({
468 change: useDebounce(function(event, ui) {
469 // Safety check - ensure we have a valid field and value
470 if (!$colorField || !$colorField.val()) {
471 console.warn('Color picker not ready');
472 return;
473 }
474
475 const name = $colorField.attr('name');
476 const value = $colorField.val();
477
478 if (!name || !value) {
479 console.warn('Missing required color picker values');
480 return;
481 }
482
483 // Create feedback container
484 const feedbackContainer = $('<div class="feedback-container"></div>');
485 const spinner = $('<div class="saving-spinner"></div>');
486 const successIcon = $('<div class="success-icon">✔</div>');
487
488 // Position feedback container
489 $colorField.closest('.wp-picker-container').after(feedbackContainer);
490 feedbackContainer.append(spinner);
491
492 // Determine which AJAX action and nonce to use:
493 var ajaxAction, nonce;
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
499 ajaxAction = 'mxchat_save_prompts_setting';
500 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
501 } else {
502 // Otherwise, use the existing AJAX action.
503 ajaxAction = 'mxchat_save_setting';
504 nonce = mxchatAdmin.setting_nonce;
505 }
506 // AJAX save request
507 $.ajax({
508 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
509 type: 'POST',
510 data: {
511 action: ajaxAction,
512 name: name,
513 value: value,
514 _ajax_nonce: nonce
515 },
516 success: function(response) {
517 if (response.success) {
518 spinner.fadeOut(200, function() {
519 feedbackContainer.append(successIcon);
520 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
521 feedbackContainer.remove();
522 });
523 });
524 } else {
525 alert('Error saving: ' + (response.data?.message || 'Unknown error'));
526 feedbackContainer.remove();
527 }
528 },
529 error: function() {
530 alert('An error occurred while saving.');
531 feedbackContainer.remove();
532 }
533 });
534 }, 500)
535 });
536 });
537
538 // Reinitialize color pickers when switching tabs
539 $('.mxchat-tab-button').on('click.mxchat', function() {
540 setTimeout(function() {
541 $('.my-color-field:visible').wpColorPicker('close');
542 }, 100);
543 });
544 }
545
546 // Initialize tabs system
547 function initTabs() {
548 // Remove any existing handlers first
549 $('.mxchat-tab-button').off('click.mxchat');
550
551 // Add new click handlers
552 $('.mxchat-tab-button').on('click.mxchat', function(e) {
553 e.preventDefault();
554 e.stopPropagation();
555
556 var $this = $(this);
557
558 // Get tab ID from data-tab attribute
559 var tabId = $this.data('tab') || 'chatbot';
560
561 // Safety check for empty tabId
562 if (!tabId) {
563 console.warn('No tab identifier found');
564 return;
565 }
566
567 // Update tab buttons
568 $('.mxchat-tab-button').removeClass('active');
569 $this.addClass('active');
570
571 // Update content areas - with safety check
572 $('.mxchat-tab-content').removeClass('active');
573 var $targetTab = $('#' + tabId);
574 if ($targetTab.length) {
575 $targetTab.addClass('active');
576
577 // Store active tab
578 try {
579 localStorage.setItem('mxchat_active_tab', tabId);
580 } catch (e) {
581 console.warn('LocalStorage not available:', e);
582 }
583 } else {
584 console.warn('Tab content #' + tabId + ' not found');
585 }
586 });
587 }
588
589 // Initialize tabs and handle events
590 initTabs();
591 $(document).on('widget-added widget-updated postbox-toggled', initTabs);
592
593 // Activate initial tab
594 try {
595 var savedTab = localStorage.getItem('mxchat_active_tab');
596 if (savedTab && $('#' + savedTab).length > 0) {
597 $('.mxchat-tab-button[data-tab="' + savedTab + '"]').trigger('click.mxchat');
598 } else {
599 $('.mxchat-tab-button').first().trigger('click.mxchat');
600 }
601 } catch (e) {
602 $('.mxchat-tab-button').first().trigger('click.mxchat');
603 }
604
605 // Attach edit modal event handler
606 $(document).on('click', '.mxchat-edit-button', function() {
607 const intentId = $(this).data('intent-id');
608 const phrases = $(this).data('phrases');
609 mxchatOpenEditModal(intentId, phrases);
610 });
611
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);
678 }
679 });
680 }
681
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 }
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
1116 // Add Intent Form Submission
1117 $('#mxchat-add-intent-form').on('submit', function(event) {
1118 $('#mxchat-intent-loading').show();
1119 $('#mxchat-intent-loading-text').show();
1120 $(this).find('button[type="submit"]').hide();
1121 });
1122
1123 // Inline Edit Functionality
1124 $('.edit-button').on('click', function() {
1125 var row = $(this).closest('tr');
1126 row.find('.content-view, .url-view').hide();
1127 row.find('.content-edit, .url-edit').show();
1128 row.find('.edit-button').hide();
1129 row.find('.save-button').show();
1130 });
1131
1132 // Save button handler
1133 $('.save-button').on('click', function() {
1134 var button = $(this);
1135 var row = button.closest('tr');
1136 var id = button.data('id');
1137 var newContent = row.find('.content-edit').val();
1138 var newUrl = row.find('.url-edit').val();
1139
1140 button.prop('disabled', true);
1141 button.text('Saving...');
1142
1143 $.ajax({
1144 url: mxchatAdmin.ajax_url,
1145 type: 'POST',
1146 data: {
1147 action: 'mxchat_save_inline_prompt',
1148 id: id,
1149 article_content: newContent,
1150 article_url: newUrl,
1151 _ajax_nonce: mxchatAdmin.inline_edit_nonce
1152 },
1153 success: function(response) {
1154 button.prop('disabled', false);
1155 button.text('Save');
1156
1157 if (response.success) {
1158 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1159 if (newUrl) {
1160 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank">' + newUrl + '</a>');
1161 } else {
1162 row.find('.url-view').html('N/A');
1163 }
1164
1165 row.find('.content-edit, .url-edit').hide();
1166 row.find('.content-view, .url-view').show();
1167 row.find('.save-button').hide();
1168 row.find('.edit-button').show();
1169 } else {
1170 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1171 }
1172 },
1173 error: function() {
1174 button.prop('disabled', false);
1175 button.text('Save');
1176 alert('An error occurred while saving.');
1177 }
1178 });
1179 });
1180
1181 // Activation handling
1182 const form = $('#mxchat-activation-form');
1183 const spinner = $('#mxchat-activation-spinner');
1184 const submitButton = $('#activate_license_button');
1185 const licenseStatus = $('#mxchat-license-status');
1186
1187 if (form.length && licenseStatus.length && submitButton.length) {
1188 function handleActivationResponse(response) {
1189 spinner.hide();
1190 if (response.success) {
1191 licenseStatus.text('Active');
1192 licenseStatus.removeClass('inactive').addClass('active');
1193 form.hide();
1194 } else {
1195 licenseStatus.text('Inactive');
1196 alert(response.data || 'Activation failed. Please check your input.');
1197 submitButton.prop('disabled', false);
1198 }
1199 }
1200
1201 form.on('submit', function(event) {
1202 event.preventDefault();
1203 spinner.show();
1204 submitButton.prop('disabled', true);
1205
1206 var formData = {
1207 action: 'mxchat_activate_license',
1208 mxchat_pro_email: $('#mxchat_pro_email').val(),
1209 mxchat_activation_key: $('#mxchat_activation_key').val(),
1210 security: mxchatAdmin.license_nonce
1211 };
1212
1213 $.post(mxchatAdmin.ajax_url, formData, function(response) {
1214 handleActivationResponse(response);
1215 }).fail(function() {
1216 alert('Server error. Please try again.');
1217 spinner.hide();
1218 submitButton.prop('disabled', false);
1219 });
1220 });
1221 }
1222
1223 // Questions handling
1224 $('.mxchat-add-question').on('click', function () {
1225 const container = $('#mxchat-additional-questions-container');
1226 const questionCount = container.find('.mxchat-question-row').length + 4;
1227 const questionIndex = container.find('.mxchat-question-row').length;
1228
1229 const newQuestion = `
1230 <div class="mxchat-question-row">
1231 <input type="text"
1232 name="additional_popular_questions[]"
1233 placeholder="Enter Additional Popular Question ${questionCount}"
1234 class="regular-text mxchat-question-input"
1235 data-question-index="${questionIndex}" />
1236 <button type="button" class="button mxchat-remove-question"
1237 aria-label="Remove question">Remove</button>
1238 </div>
1239 `;
1240 container.append(newQuestion);
1241 });
1242
1243 $(document).on('click', '.mxchat-remove-question', function () {
1244 $(this).closest('.mxchat-question-row').remove();
1245 saveQuestions();
1246 });
1247
1248 $(document).on('change', '.mxchat-question-input', function() {
1249 saveQuestions();
1250 });
1251
1252 function saveQuestions() {
1253 const questions = [];
1254 $('.mxchat-question-input').each(function() {
1255 const value = $(this).val().trim();
1256 if (value) {
1257 questions.push(value);
1258 }
1259 });
1260
1261 const feedbackContainer = $('<div class="feedback-container"></div>');
1262 const spinner = $('<div class="saving-spinner"></div>');
1263 const successIcon = $('<div class="success-icon">✔</div>');
1264
1265 // Append feedback after the add button
1266 $('.mxchat-add-question').after(feedbackContainer);
1267 feedbackContainer.append(spinner);
1268
1269 // Save via AJAX
1270 $.ajax({
1271 url: mxchatAdmin.ajax_url,
1272 type: 'POST',
1273 data: {
1274 action: 'mxchat_save_setting',
1275 name: 'additional_popular_questions',
1276 value: JSON.stringify(questions),
1277 _ajax_nonce: mxchatAdmin.setting_nonce
1278 },
1279 success: function(response) {
1280 if (response.success) {
1281 spinner.fadeOut(200, function() {
1282 feedbackContainer.append(successIcon);
1283 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
1284 feedbackContainer.remove();
1285 });
1286 });
1287 } else {
1288 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
1289 feedbackContainer.remove();
1290 }
1291 },
1292 error: function() {
1293 alert('An error occurred while saving questions.');
1294 feedbackContainer.remove();
1295 }
1296 });
1297 }
1298
1299 // Live agent status handler
1300 const statusToggle = document.getElementById('live_agent_status');
1301 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
1302 if (statusToggle && statusText) {
1303 statusToggle.addEventListener('change', function() {
1304 // Update display text
1305 statusText.textContent = this.checked ? 'Online' : 'Offline';
1306
1307 // Send the correct on/off value to the server
1308 if (window.mxchatSaveSetting) {
1309 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
1310 }
1311 });
1312 }
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');
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 }
1803 });
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 });
1838
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 });