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

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