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

2,442 lines 112.5 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 // Track whether fields have been modified by user
345 const userModifiedFields = new Set();
346
347 if ($autosaveSections.length) {
348 // Track user interactions with input fields to determine if changes are user-initiated
349 $autosaveSections.find('input, textarea, select').on('focus keydown paste', function() {
350 const fieldName = $(this).attr('name');
351 if (fieldName) {
352 userModifiedFields.add(fieldName);
353 }
354 });
355
356 // Handle real-time range slider value updates
357 $autosaveSections.find('input[type="range"]').on('input', function() {
358 const value = $(this).val();
359 $('#threshold_value').text(value);
360 });
361
362 // Handle all input changes (including range slider)
363 $autosaveSections.find('input, textarea, select').on('change', function() {
364 const $field = $(this);
365 const name = $field.attr('name');
366
367 // Skip saving for API key fields that haven't been interacted with and are empty
368 const isApiKeyField = name && (
369 name === 'loops_api_key' ||
370 name === 'api_key' ||
371 name === 'xai_api_key' ||
372 name === 'claude_api_key' ||
373 name === 'voyage_api_key' ||
374 name === 'gemini_api_key' ||
375 name === 'deepseek_api_key' ||
376 name.indexOf('_api_key') !== -1
377 );
378
379 // Skip processing if:
380 // 1. It's an API key field
381 // 2. The user hasn't interacted with it
382 // 3. The field is empty
383 if (isApiKeyField && !userModifiedFields.has(name) && (!$field.val() || $field.val().trim() === '')) {
384 //console.log('Skipping auto-save for untouched API key field:', name);
385 return;
386 }
387
388 let value;
389
390 // Handle different input types
391 if ($field.attr('type') === 'checkbox') {
392 value = $field.is(':checked') ? 'on' : 'off';
393 } else {
394 value = $field.val();
395 }
396
397 // Create feedback container
398 const feedbackContainer = $('<div class="feedback-container"></div>');
399 const spinner = $('<div class="saving-spinner"></div>');
400 const successIcon = $('<div class="success-icon">✔</div>');
401
402 // Position feedback container based on input type
403 if ($field.closest('.toggle-switch').length) {
404 $field.closest('td').append(feedbackContainer);
405 } else if ($field.closest('.mxchat-toggle-switch').length) {
406 $field.closest('.mxchat-toggle-container').append(feedbackContainer);
407 } else if ($field.closest('.slider-container').length) {
408 $field.closest('.slider-container').after(feedbackContainer);
409 } else {
410 $field.after(feedbackContainer);
411 }
412 feedbackContainer.append(spinner);
413
414 // Determine which AJAX action and nonce to use:
415 var ajaxAction, nonce;
416 // Use the new AJAX action for submenu fields:
417 if (name.indexOf('mxchat_prompts_options') !== -1 ||
418 name === 'mxchat_auto_sync_posts' ||
419 name === 'mxchat_auto_sync_pages' ||
420 name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields
421 ajaxAction = 'mxchat_save_prompts_setting';
422 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
423 } else {
424 // Otherwise, use the existing AJAX action.
425 ajaxAction = 'mxchat_save_setting';
426 nonce = mxchatAdmin.setting_nonce;
427 }
428
429 // AJAX save request
430 $.ajax({
431 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
432 type: 'POST',
433 data: {
434 action: ajaxAction,
435 name: name,
436 value: value,
437 _ajax_nonce: nonce
438 },
439 success: function(response) {
440 if (response.success) {
441 spinner.fadeOut(200, function() {
442 feedbackContainer.append(successIcon);
443 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
444 feedbackContainer.remove();
445 });
446 });
447
448 // Check if the response contains a "no changes" message and log it
449 if (response.data && response.data.message === 'No changes detected') {
450 //console.log('No changes detected for field:', name);
451 }
452 } else {
453 // Only show alert for actual errors, not for "no changes"
454 let errorMessage = response.data?.message || 'Unknown error';
455
456 // Don't display an alert for "no changes" message
457 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
458 alert('Error saving: ' + errorMessage);
459 } else {
460 // Still provide visual feedback that no changes were needed
461 spinner.fadeOut(200, function() {
462 feedbackContainer.append(successIcon);
463 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
464 feedbackContainer.remove();
465 });
466 });
467 //console.log('No changes detected for field:', name);
468 return;
469 }
470
471 // Only revert checkbox state if it was an actual error
472 if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') {
473 if ($field.attr('type') === 'checkbox') {
474 $field.prop('checked', !$field.is(':checked'));
475 }
476 }
477
478 // Always clean up the feedback container
479 feedbackContainer.remove();
480 }
481 },
482 error: function(xhr, textStatus, error) {
483 //console.error('AJAX Error:', textStatus, error);
484 alert('An error occurred while saving. Please try again.');
485
486 // Revert checkbox state on error
487 if ($field.attr('type') === 'checkbox') {
488 $field.prop('checked', !$field.is(':checked'));
489 }
490
491 feedbackContainer.remove();
492 }
493 });
494 });
495
496 // Initialize color pickers with debouncing
497 $autosaveSections.find('.my-color-field').each(function() {
498 const $colorField = $(this);
499
500 $(this).wpColorPicker({
501 change: useDebounce(function(event, ui) {
502 // Safety check - ensure we have a valid field and value
503 if (!$colorField || !$colorField.val()) {
504 //console.warn('Color picker not ready');
505 return;
506 }
507
508 const name = $colorField.attr('name');
509 const value = $colorField.val();
510
511 if (!name || !value) {
512 //console.warn('Missing required color picker values');
513 return;
514 }
515
516 // Create feedback container
517 const feedbackContainer = $('<div class="feedback-container"></div>');
518 const spinner = $('<div class="saving-spinner"></div>');
519 const successIcon = $('<div class="success-icon">✔</div>');
520
521 // Position feedback container
522 $colorField.closest('.wp-picker-container').after(feedbackContainer);
523 feedbackContainer.append(spinner);
524
525 // Determine which AJAX action and nonce to use:
526 var ajaxAction, nonce;
527 // Use the new AJAX action for submenu fields:
528 if (name.indexOf('mxchat_prompts_options') !== -1 ||
529 name === 'mxchat_auto_sync_posts' ||
530 name === 'mxchat_auto_sync_pages' ||
531 name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields
532 ajaxAction = 'mxchat_save_prompts_setting';
533 nonce = mxchatPromptsAdmin.prompts_setting_nonce;
534 } else {
535 // Otherwise, use the existing AJAX action.
536 ajaxAction = 'mxchat_save_setting';
537 nonce = mxchatAdmin.setting_nonce;
538 }
539 // AJAX save request
540 $.ajax({
541 url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url,
542 type: 'POST',
543 data: {
544 action: ajaxAction,
545 name: name,
546 value: value,
547 _ajax_nonce: nonce
548 },
549 success: function(response) {
550 if (response.success) {
551 spinner.fadeOut(200, function() {
552 feedbackContainer.append(successIcon);
553 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
554 feedbackContainer.remove();
555 });
556 });
557 } else {
558 alert('Error saving: ' + (response.data?.message || 'Unknown error'));
559 feedbackContainer.remove();
560 }
561 },
562 error: function() {
563 alert('An error occurred while saving.');
564 feedbackContainer.remove();
565 }
566 });
567 }, 500)
568 });
569 });
570
571 // Reinitialize color pickers when switching tabs
572 $('.mxchat-tab-button').on('click.mxchat', function() {
573 setTimeout(function() {
574 $('.my-color-field:visible').wpColorPicker('close');
575 }, 100);
576 });
577 }
578
579 // Initialize tabs system
580 function initTabs() {
581 // Remove any existing handlers first
582 $('.mxchat-tab-button').off('click.mxchat');
583
584 // Add new click handlers
585 $('.mxchat-tab-button').on('click.mxchat', function(e) {
586 e.preventDefault();
587 e.stopPropagation();
588
589 var $this = $(this);
590
591 // Get tab ID from data-tab attribute
592 var tabId = $this.data('tab') || 'chatbot';
593
594 // Safety check for empty tabId
595 if (!tabId) {
596 //console.warn('No tab identifier found');
597 return;
598 }
599
600 // Update tab buttons
601 $('.mxchat-tab-button').removeClass('active');
602 $this.addClass('active');
603
604 // Update content areas - with safety check
605 $('.mxchat-tab-content').removeClass('active');
606 var $targetTab = $('#' + tabId);
607 if ($targetTab.length) {
608 $targetTab.addClass('active');
609 // Removed localStorage saving functionality
610 } else {
611 //console.warn('Tab content #' + tabId + ' not found');
612 }
613 });
614 }
615
616 // Initialize tabs and handle events
617 initTabs();
618 $(document).on('widget-added widget-updated postbox-toggled', initTabs);
619
620 // Always activate the first tab (Chatbot)
621 $('.mxchat-tab-button').first().trigger('click.mxchat');
622
623 // Attach edit modal event handler
624 $(document).on('click', '.mxchat-edit-button', function() {
625 const intentId = $(this).data('intent-id');
626 const phrases = $(this).data('phrases');
627 mxchatOpenEditModal(intentId, phrases);
628 });
629
630 // Toggle visibility handlers
631 function toggleVisibility(selector) {
632 $(selector).on('click', function() {
633 var inputField = $(this).prev('input');
634 if (inputField.attr('type') === 'password') {
635 inputField.attr('type', 'text');
636 $(this).text('Hide');
637 } else {
638 inputField.attr('type', 'password');
639 $(this).text('Show');
640 }
641 });
642 }
643
644 // Initialize all toggle visibility buttons
645 [
646 '#toggleApiKeyVisibility',
647 '#toggleWooCommerceSecretVisibility',
648 '#toggleVoyageAPIKeyVisibility',
649 '#toggleLoopsApiKeyVisibility',
650 '#toggleXaiApiKeyVisibility',
651 '#toggleClaudeApiKeyVisibility',
652 '#toggleBraveApiKeyVisibility',
653 '#toggleWebhookUrlVisibility',
654 '#toggleSecretKeyVisibility',
655 '#toggleBotTokenVisibility',
656 '#toggleDeepSeekApiKeyVisibility',
657 '#toggleGeminiApiKeyVisibility' // Added Gemini toggle
658 ].forEach(toggleVisibility);
659
660 // Handle API key visibility based on model selection
661 function setupAPIKeyVisibility() {
662 // Cache the selectors
663 const $chatModelSelect = $('#model');
664 const $embeddingModelSelect = $('#embedding_model');
665
666 // First, locate and mark the API key rows
667 setupAPIKeyRows();
668
669 // Initial setup based on current selections
670 updateApiKeyVisibility();
671
672 // Listen for changes to the model selectors
673 $chatModelSelect.on('change', updateApiKeyVisibility);
674 $embeddingModelSelect.on('change', updateApiKeyVisibility);
675
676 /**
677 * Locate and mark rows that contain API key fields
678 */
679 function setupAPIKeyRows() {
680 // Find key rows by their field IDs
681 const providerMap = {
682 'api_key': 'openai',
683 'xai_api_key': 'xai',
684 'claude_api_key': 'claude',
685 'deepseek_api_key': 'deepseek',
686 'voyage_api_key': 'voyage',
687 'gemini_api_key': 'gemini' // Added Gemini API key mapping
688 };
689
690 $.each(providerMap, function(fieldId, provider) {
691 const $field = $('#' + fieldId);
692 if ($field.length) {
693 const $row = $field.closest('tr');
694 $row.addClass('mxchat-setting-row');
695 $row.attr('data-provider', provider);
696 }
697 });
698 }
699
700 /**
701 * Updates the visibility of API key fields based on current model selections
702 */
703 function updateApiKeyVisibility() {
704 const chatModel = $chatModelSelect.val();
705 const embeddingModel = $embeddingModelSelect.val();
706
707 // Determine which providers are needed
708 const isOpenAIChat = chatModel && chatModel.startsWith('gpt-');
709 const isXAI = chatModel && chatModel.startsWith('grok-');
710 const isClaude = chatModel && chatModel.startsWith('claude-');
711 const isDeepSeek = chatModel && chatModel.startsWith('deepseek-');
712 const isGemini = chatModel && chatModel.startsWith('gemini-'); // Added Gemini detection
713
714 const isOpenAIEmbedding = embeddingModel && embeddingModel.startsWith('text-embedding-');
715 const isVoyage = embeddingModel && embeddingModel.startsWith('voyage-');
716
717 // Update API key visibility for each provider
718 updateWrapperVisibility('openai', isOpenAIChat || isOpenAIEmbedding);
719 updateWrapperVisibility('xai', isXAI);
720 updateWrapperVisibility('claude', isClaude);
721 updateWrapperVisibility('deepseek', isDeepSeek);
722 updateWrapperVisibility('voyage', isVoyage);
723 updateWrapperVisibility('gemini', isGemini); // Added Gemini visibility update
724
725 // Update provider-specific notices for OpenAI
726 if (isOpenAIChat && isOpenAIEmbedding) {
727 $('div[data-provider="openai"] .api-key-notice').text(
728 'Required for your selected chat model and embedding model. Important: You must add credits before use.'
729 );
730 } else if (isOpenAIChat) {
731 $('div[data-provider="openai"] .api-key-notice').text(
732 'Required for your selected chat model. Important: You must add credits before use.'
733 );
734 } else if (isOpenAIEmbedding) {
735 $('div[data-provider="openai"] .api-key-notice').text(
736 'Required for your selected embedding model. Important: You must add credits before use.'
737 );
738 }
739 }
740
741 /**
742 * Updates visibility of a specific provider's API key wrapper
743 */
744 function updateWrapperVisibility(provider, isVisible) {
745 const $row = $('tr.mxchat-setting-row[data-provider="' + provider + '"]');
746
747 if (!$row.length) {
748 //console.warn('API key row not found for provider: ' + provider);
749 return;
750 }
751
752 if (isVisible) {
753 $row.show();
754 if (!$row.hasClass('highlighted')) {
755 $row.addClass('highlighted');
756 setTimeout(() => {
757 $row.removeClass('highlighted');
758 }, 1500);
759 }
760 } else {
761 $row.hide();
762 }
763 }
764 }
765
766 // Add this to your JavaScript file
767 function setupMxChatModelSelector() {
768 const $modelSelect = $('#model');
769 const $modelSelectorButton = $('<button>', {
770 type: 'button',
771 id: 'mxchat_model_selector_btn',
772 class: 'button-primary mxchat-model-selector-btn',
773 text: 'Select AI Model'
774 });
775
776 // Replace the select dropdown with a button
777 $modelSelect.hide().after($modelSelectorButton);
778
779 // Update button text to show currently selected model
780 function updateButtonText() {
781 const selectedModel = $modelSelect.val();
782 const selectedModelText = $modelSelect.find('option:selected').text();
783 $modelSelectorButton.text(selectedModelText);
784 }
785
786 // Initialize button text
787 updateButtonText();
788
789 // Create and append modal HTML
790 const modelSelectorModal = `
791 <div id="mxchat_model_selector_modal" class="mxchat-model-selector-modal">
792 <div class="mxchat-model-selector-modal-content">
793 <div class="mxchat-model-selector-modal-header">
794 <h3>Select AI Model</h3>
795 <span class="mxchat-model-selector-modal-close">&times;</span>
796 </div>
797 <div class="mxchat-model-selector-modal-body">
798 <div class="mxchat-model-selector-search-container">
799 <input type="text" id="mxchat_model_search_input" class="mxchat-model-search-input" placeholder="Search models...">
800 </div>
801 <div class="mxchat-model-selector-categories">
802 <button class="mxchat-model-category-btn active" data-category="all">All</button>
803 <button class="mxchat-model-category-btn" data-category="gemini">Google Gemini</button>
804 <button class="mxchat-model-category-btn" data-category="openai">OpenAI</button>
805 <button class="mxchat-model-category-btn" data-category="claude">Claude</button>
806 <button class="mxchat-model-category-btn" data-category="xai">X.AI</button>
807 <button class="mxchat-model-category-btn" data-category="deepseek">DeepSeek</button>
808 </div>
809 <div class="mxchat-model-selector-grid" id="mxchat_models_grid"></div>
810 </div>
811 <div class="mxchat-model-selector-modal-footer">
812 <button id="mxchat_cancel_model_selection" class="button mxchat-model-cancel-btn">Cancel</button>
813 </div>
814 </div>
815 </div>
816 `;
817
818 $('body').append(modelSelectorModal);
819
820 // Populate models grid
821 function populateModelsGrid(filter = '', category = 'all') {
822 const $grid = $('#mxchat_models_grid');
823 $grid.empty();
824
825 const models = {
826 gemini: [
827 { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Next-Gen features, speed & multimodal generation' },
828 { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Cost-efficient with low latency' },
829 { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Complex reasoning tasks requiring more intelligence' },
830 { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Fast and versatile performance' },
831 ],
832 openai: [
833 { value: 'gpt-4.1-2025-04-14', label: 'GPT-4.1', description: 'Flagship model for complex tasks' },
834 { value: 'gpt-4o', label: 'GPT-4o', description: 'Recommended for most use cases' },
835 { value: 'gpt-4o-mini', label: 'GPT-4o Mini', description: 'Fast and lightweight' },
836 { value: 'gpt-4-turbo', label: 'GPT-4 Turbo', description: 'High-performance model' },
837 { value: 'gpt-4', label: 'GPT-4', description: 'High intelligence model' },
838 { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', description: 'Affordable and fast' },
839 ],
840 claude: [
841 { value: 'claude-3-7-sonnet-20250219', label: 'Claude 3.7 Sonnet', description: 'Most intelligent Claude model' },
842 { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet', description: 'Intelligent and balanced' },
843 { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus', description: 'Highly complex tasks' },
844 { value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', description: 'Balanced performance' },
845 { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', description: 'Fastest Claude model' },
846 ],
847 xai: [
848 { value: 'grok-3-beta', label: 'Grok-3', description: 'Powerful model with 131K context' },
849 { value: 'grok-3-fast-beta', label: 'Grok-3 Fast', description: 'High performance with faster responses' },
850 { value: 'grok-3-mini-beta', label: 'Grok-3 Mini', description: 'Affordable model with good performance' },
851 { value: 'grok-3-mini-fast-beta', label: 'Grok-3 Mini Fast', description: 'Quick and cost-effective' },
852 { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' },
853 ],
854 deepseek: [
855 { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' },
856 ],
857 };
858
859 let allModels = [];
860 Object.keys(models).forEach(key => {
861 if (category === 'all' || category === key) {
862 allModels = allModels.concat(models[key]);
863 }
864 });
865
866 // Filter by search term if present
867 if (filter) {
868 const lowerFilter = filter.toLowerCase();
869 allModels = allModels.filter(model =>
870 model.label.toLowerCase().includes(lowerFilter) ||
871 model.description.toLowerCase().includes(lowerFilter)
872 );
873 }
874
875 // Create model cards
876 allModels.forEach(model => {
877 const isSelected = $modelSelect.val() === model.value;
878 const $modelCard = $(`
879 <div class="mxchat-model-selector-card ${isSelected ? 'mxchat-model-selected' : ''}" data-value="${model.value}">
880 <div class="mxchat-model-selector-icon">${getModelIcon(model.value)}</div>
881 <div class="mxchat-model-selector-info">
882 <h4 class="mxchat-model-selector-title">${model.label}</h4>
883 <p class="mxchat-model-selector-description">${model.description}</p>
884 </div>
885 ${isSelected ? '<div class="mxchat-model-selector-checkmark">✓</div>' : ''}
886 </div>
887 `);
888 $grid.append($modelCard);
889 });
890 }
891
892 // Helper function to get icon for each model
893 function getModelIcon(modelValue) {
894 if (modelValue.startsWith('gemini-')) return '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" class="mxchat-model-icon-gemini"><defs><path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"></path></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"></use></clipPath><path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"></path><path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"></path><path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"></path><path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"></path></svg>';
895 if (modelValue.startsWith('gpt-')) return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" class="mxchat-model-icon-openai"><path fill="currentColor" d="M297 131a80.6 80.6 0 0 0-93.7-104.2 80.6 80.6 0 0 0-137 29A80.6 80.6 0 0 0 23 189a80.6 80.6 0 0 0 93.7 104.2 80.6 80.6 0 0 0 137-29A80.7 80.7 0 0 0 297.1 131zM176.9 299c-14 .1-27.6-4.8-38.4-13.8l1.9-1 63.7-36.9c3.3-1.8 5.3-5.3 5.2-9v-89.9l27 15.6c.3.1.4.4.5.7v74.4a60 60 0 0 1-60 60zM47.9 244a59.7 59.7 0 0 1-7.1-40.1l1.9 1.1 63.7 36.8c3.2 1.9 7.2 1.9 10.5 0l77.8-45V228c0 .3-.2.6-.4.8L129.9 266a60 60 0 0 1-82-22zM31.2 105c7-12.2 18-21.5 31.2-26.3v75.8c0 3.7 2 7.2 5.2 9l77.8 45-27 15.5a1 1 0 0 1-.9 0L53.1 187a60 60 0 0 1-22-82zm221.2 51.5-77.8-45 27-15.5a1 1 0 0 1 .9 0l64.4 37.1a60 60 0 0 1-9.3 108.2v-75.8c0-3.7-2-7.2-5.2-9zm26.8-40.4-1.9-1.1-63.7-36.8a10.4 10.4 0 0 0-10.5 0L125.4 123V92c0-.3 0-.6.3-.8L190.1 54a60 60 0 0 1 89.1 62.1zm-168.5 55.4-27-15.5a1 1 0 0 1-.4-.7V80.9a60 60 0 0 1 98.3-46.1l-1.9 1L116 72.8a10.3 10.3 0 0 0-5.2 9v89.8zm14.6-31.5 34.7-20 34.6 20v40L160 200l-34.7-20z"></path></svg>';
896 if (modelValue.startsWith('claude-')) return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 176" fill="none" class="mxchat-model-icon-claude"><path fill="currentColor" d="m147.487 0l70.081 175.78H256L185.919 0zM66.183 106.221l23.98-61.774l23.98 61.774zM70.07 0L0 175.78h39.18l14.33-36.914h73.308l14.328 36.914h39.179L110.255 0z"></path></svg>';
897 if (modelValue.startsWith('grok-')) return '<svg fill="currentColor" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="mxchat-model-icon-xai"><path d="M6.469 8.776L16.512 23h-4.464L2.005 8.776H6.47zm-.004 7.9l2.233 3.164L6.467 23H2l4.465-6.324zM22 2.582V23h-3.659V7.764L22 2.582zM22 1l-9.952 14.095-2.233-3.163L17.533 1H22z"></path></svg>';
898 if (modelValue.startsWith('deepseek-')) return '<svg height="1em" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg" class="mxchat-model-icon-deepseek"><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="currentColor"></path></svg>';
899 return '<span class="dashicons dashicons-admin-generic mxchat-model-icon-generic"></span>';
900 }
901
902 // Event handlers
903 $modelSelectorButton.on('click', function() {
904 $('#mxchat_model_selector_modal').show();
905 populateModelsGrid('', 'all');
906 });
907
908 $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() {
909 $('#mxchat_model_selector_modal').hide();
910 });
911
912 $('.mxchat-model-category-btn').on('click', function() {
913 $('.mxchat-model-category-btn').removeClass('active');
914 $(this).addClass('active');
915 const category = $(this).data('category');
916 const searchTerm = $('#mxchat_model_search_input').val();
917 populateModelsGrid(searchTerm, category);
918 });
919
920 $('#mxchat_model_search_input').on('input', function() {
921 const searchTerm = $(this).val();
922 const activeCategory = $('.mxchat-model-category-btn.active').data('category');
923 populateModelsGrid(searchTerm, activeCategory);
924 });
925
926 $(document).on('click', '.mxchat-model-selector-card', function() {
927 const modelValue = $(this).data('value');
928 $modelSelect.val(modelValue).trigger('change');
929 updateButtonText();
930 $('#mxchat_model_selector_modal').hide();
931 });
932
933 // Close modal when clicking outside
934 $(window).on('click', function(event) {
935 if ($(event.target).is('#mxchat_model_selector_modal')) {
936 $('#mxchat_model_selector_modal').hide();
937 }
938 });
939 }
940
941 // Embedding model selector - completely separate from chat model selector
942 function setupMxChatEmbeddingModelSelector() {
943 const $embeddingModelSelect = $('#embedding_model');
944
945 // Skip if the element doesn't exist on the page
946 if ($embeddingModelSelect.length === 0) {
947 return;
948 }
949
950 const $embeddingModelSelectorButton = $('<button>', {
951 type: 'button',
952 id: 'mxchat_embedding_model_selector_btn',
953 class: 'button-primary mxchat-embedding-model-selector-btn', // Changed class name to be more specific
954 text: 'Select Embedding Model'
955 });
956
957 // Replace the select dropdown with a button
958 $embeddingModelSelect.hide().after($embeddingModelSelectorButton);
959
960 // Update button text to show currently selected model
961 function updateButtonText() {
962 const selectedModel = $embeddingModelSelect.val();
963 const selectedModelText = $embeddingModelSelect.find('option:selected').text();
964 $embeddingModelSelectorButton.text(selectedModelText);
965 }
966
967 // Initialize button text
968 updateButtonText();
969
970 // Create a unique ID for the modal to avoid conflicts
971 const embeddingModalId = 'mxchat_embedding_model_selector_modal';
972
973 // Create and append modal HTML with unique IDs
974 const embeddingModelSelectorModal = `
975 <div id="${embeddingModalId}" class="mxchat-embedding-model-selector-modal">
976 <div class="mxchat-embedding-model-selector-modal-content">
977 <div class="mxchat-embedding-model-selector-modal-header">
978 <h3>Select Embedding Model</h3>
979 <span class="mxchat-embedding-model-selector-modal-close">&times;</span>
980 </div>
981 <div class="mxchat-embedding-model-selector-modal-body">
982 <div class="mxchat-embedding-model-selector-search-container">
983 <input type="text" id="mxchat_embedding_model_search_input" class="mxchat-embedding-model-search-input" placeholder="Search models...">
984 </div>
985 <div class="mxchat-embedding-model-selector-categories">
986 <button class="mxchat-embedding-model-category-btn active" data-category="all">All</button>
987 <button class="mxchat-embedding-model-category-btn" data-category="openai">OpenAI</button>
988 <button class="mxchat-embedding-model-category-btn" data-category="voyage">Voyage AI</button>
989 </div>
990 <div class="mxchat-embedding-model-selector-grid" id="mxchat_embedding_models_grid"></div>
991 </div>
992 <div class="mxchat-embedding-model-selector-modal-footer">
993 <button id="mxchat_cancel_embedding_model_selection" class="button mxchat-embedding-model-cancel-btn">Cancel</button>
994 </div>
995 </div>
996 </div>
997 `;
998
999 // Use jQuery's append to ensure it doesn't clash with existing modals
1000 $('body').append(embeddingModelSelectorModal);
1001
1002 // Populate models grid
1003 function populateEmbeddingModelsGrid(filter = '', category = 'all') {
1004 const $grid = $('#mxchat_embedding_models_grid');
1005 $grid.empty();
1006
1007 // Define embedding models with descriptions and context lengths
1008 const models = {
1009 openai: [
1010 {
1011 value: 'text-embedding-3-small',
1012 label: 'TE3 Small',
1013 description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)'
1014 },
1015 {
1016 value: 'text-embedding-ada-002',
1017 label: 'Ada 2',
1018 description: 'Balanced performance embeddings (1536 dimensions, 8K context)'
1019 },
1020 {
1021 value: 'text-embedding-3-large',
1022 label: 'TE3 Large',
1023 description: 'High-performance embeddings (3072 dimensions, 8K context)'
1024 }
1025 ],
1026 voyage: [
1027 {
1028 value: 'voyage-3-large',
1029 label: 'Voyage-3 Large',
1030 description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)'
1031 }
1032 ]
1033 };
1034
1035 let allModels = [];
1036 Object.keys(models).forEach(key => {
1037 if (category === 'all' || category === key) {
1038 allModels = allModels.concat(models[key]);
1039 }
1040 });
1041
1042 // Filter by search term if present
1043 if (filter) {
1044 const lowerFilter = filter.toLowerCase();
1045 allModels = allModels.filter(model =>
1046 model.label.toLowerCase().includes(lowerFilter) ||
1047 model.description.toLowerCase().includes(lowerFilter)
1048 );
1049 }
1050
1051 // Create model cards
1052 allModels.forEach(model => {
1053 const isSelected = $embeddingModelSelect.val() === model.value;
1054 const providerClass = model.value.startsWith('voyage-') ? 'mxchat-embedding-model-provider-voyage' : 'mxchat-embedding-model-provider-openai';
1055
1056 const $modelCard = $(`
1057 <div class="mxchat-embedding-model-selector-card ${isSelected ? 'mxchat-embedding-model-selected' : ''} ${providerClass}" data-value="${model.value}">
1058 <div class="mxchat-embedding-model-selector-icon">
1059 ${model.value.startsWith('voyage-') ?
1060 '<span class="dashicons dashicons-chart-line mxchat-embedding-model-icon-voyage"></span>' :
1061 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" class="mxchat-embedding-model-icon-openai"><path fill="currentColor" d="M297 131a80.6 80.6 0 0 0-93.7-104.2 80.6 80.6 0 0 0-137 29A80.6 80.6 0 0 0 23 189a80.6 80.6 0 0 0 93.7 104.2 80.6 80.6 0 0 0 137-29A80.7 80.7 0 0 0 297.1 131zM176.9 299c-14 .1-27.6-4.8-38.4-13.8l1.9-1 63.7-36.9c3.3-1.8 5.3-5.3 5.2-9v-89.9l27 15.6c.3.1.4.4.5.7v74.4a60 60 0 0 1-60 60zM47.9 244a59.7 59.7 0 0 1-7.1-40.1l1.9 1.1 63.7 36.8c3.2 1.9 7.2 1.9 10.5 0l77.8-45V228c0 .3-.2.6-.4.8L129.9 266a60 60 0 0 1-82-22zM31.2 105c7-12.2 18-21.5 31.2-26.3v75.8c0 3.7 2 7.2 5.2 9l77.8 45-27 15.5a1 1 0 0 1-.9 0L53.1 187a60 60 0 0 1-22-82zm221.2 51.5-77.8-45 27-15.5a1 1 0 0 1 .9 0l64.4 37.1a60 60 0 0 1-9.3 108.2v-75.8c0-3.7-2-7.2-5.2-9zm26.8-40.4-1.9-1.1-63.7-36.8a10.4 10.4 0 0 0-10.5 0L125.4 123V92c0-.3 0-.6.3-.8L190.1 54a60 60 0 0 1 89.1 62.1zm-168.5 55.4-27-15.5a1 1 0 0 1-.4-.7V80.9a60 60 0 0 1 98.3-46.1l-1.9 1L116 72.8a10.3 10.3 0 0 0-5.2 9v89.8zm14.6-31.5 34.7-20 34.6 20v40L160 200l-34.7-20z"></path></svg>'
1062 }
1063 </div>
1064 <div class="mxchat-embedding-model-selector-info">
1065 <h4 class="mxchat-embedding-model-selector-title">${model.label}</h4>
1066 <p class="mxchat-embedding-model-selector-description">${model.description}</p>
1067 </div>
1068 ${isSelected ? '<div class="mxchat-embedding-model-selector-checkmark">✓</div>' : ''}
1069 </div>
1070 `);
1071
1072
1073 $grid.append($modelCard);
1074 });
1075 }
1076
1077 // Event handlers - use namespaced events to avoid conflicts
1078 $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) {
1079 e.stopPropagation(); // Prevent event bubbling
1080 $('#' + embeddingModalId).show();
1081 populateEmbeddingModelsGrid('', 'all');
1082 });
1083
1084 $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) {
1085 e.stopPropagation(); // Prevent event bubbling
1086 $('#' + embeddingModalId).hide();
1087 });
1088
1089 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) {
1090 e.stopPropagation(); // Prevent event bubbling
1091 $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active');
1092 $(this).addClass('active');
1093 const category = $(this).data('category');
1094 const searchTerm = $('#mxchat_embedding_model_search_input').val();
1095 populateEmbeddingModelsGrid(searchTerm, category);
1096 });
1097
1098 $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() {
1099 const searchTerm = $(this).val();
1100 const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category');
1101 populateEmbeddingModelsGrid(searchTerm, activeCategory);
1102 });
1103
1104 // Use a direct selector to avoid conflicts with other card elements
1105 $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) {
1106 e.stopPropagation(); // Prevent event bubbling
1107 const modelValue = $(this).data('value');
1108
1109 // Important: Only update this specific select element
1110 $embeddingModelSelect.val(modelValue);
1111
1112 // Manually trigger change only on this element
1113 const changeEvent = new Event('change', { bubbles: true });
1114 $embeddingModelSelect[0].dispatchEvent(changeEvent);
1115
1116 // Update button text
1117 updateButtonText();
1118
1119 // Hide modal
1120 $('#' + embeddingModalId).hide();
1121 });
1122
1123 // Close modal when clicking outside - use namespaced events
1124 $(window).on('click.embeddingModelSelector', function(event) {
1125 if ($(event.target).is('#' + embeddingModalId)) {
1126 $('#' + embeddingModalId).hide();
1127 }
1128 });
1129 }
1130
1131 // Call this function after the DOM is fully loaded
1132 $(document).ready(function() {
1133 setupMxChatModelSelector();
1134 setupMxChatEmbeddingModelSelector();
1135 });
1136
1137 // Initialize API key visibility
1138 setupAPIKeyVisibility();
1139
1140 // Add Intent Form Submission
1141 $('#mxchat-add-intent-form').on('submit', function(event) {
1142 $('#mxchat-intent-loading').show();
1143 $('#mxchat-intent-loading-text').show();
1144 $(this).find('button[type="submit"]').hide();
1145 });
1146
1147 // Inline Edit Functionality
1148 $('.edit-button').on('click', function() {
1149 var row = $(this).closest('tr');
1150 row.find('.content-view, .url-view').hide();
1151 row.find('.content-edit, .url-edit').show();
1152 row.find('.edit-button').hide();
1153 row.find('.save-button').show();
1154 });
1155
1156 // Save button handler
1157 $('.save-button').on('click', function() {
1158 var button = $(this);
1159 var row = button.closest('tr');
1160 var id = button.data('id');
1161 var newContent = row.find('.content-edit').val();
1162 var newUrl = row.find('.url-edit').val();
1163
1164 button.prop('disabled', true);
1165 button.text('Saving...');
1166
1167 $.ajax({
1168 url: mxchatAdmin.ajax_url,
1169 type: 'POST',
1170 data: {
1171 action: 'mxchat_save_inline_prompt',
1172 id: id,
1173 article_content: newContent,
1174 article_url: newUrl,
1175 _ajax_nonce: mxchatAdmin.inline_edit_nonce
1176 },
1177 success: function(response) {
1178 button.prop('disabled', false);
1179 button.text('Save');
1180
1181 if (response.success) {
1182 row.find('.content-view').html(newContent.replace(/\n/g, "<br>"));
1183 if (newUrl) {
1184 row.find('.url-view').html('<a href="' + newUrl + '" target="_blank">' + newUrl + '</a>');
1185 } else {
1186 row.find('.url-view').html('N/A');
1187 }
1188
1189 row.find('.content-edit, .url-edit').hide();
1190 row.find('.content-view, .url-view').show();
1191 row.find('.save-button').hide();
1192 row.find('.edit-button').show();
1193 } else {
1194 alert('Error saving content: ' + (response.data?.message || 'Unknown error'));
1195 }
1196 },
1197 error: function() {
1198 button.prop('disabled', false);
1199 button.text('Save');
1200 alert('An error occurred while saving.');
1201 }
1202 });
1203 });
1204
1205
1206 // Questions handling
1207 $('.mxchat-add-question').on('click', function () {
1208 const container = $('#mxchat-additional-questions-container');
1209 const questionCount = container.find('.mxchat-question-row').length + 4;
1210 const questionIndex = container.find('.mxchat-question-row').length;
1211
1212 const newQuestion = `
1213 <div class="mxchat-question-row">
1214 <input type="text"
1215 name="additional_popular_questions[]"
1216 placeholder="Enter Additional Popular Question ${questionCount}"
1217 class="regular-text mxchat-question-input"
1218 data-question-index="${questionIndex}" />
1219 <button type="button" class="button mxchat-remove-question"
1220 aria-label="Remove question">Remove</button>
1221 </div>
1222 `;
1223 container.append(newQuestion);
1224 });
1225
1226 $(document).on('click', '.mxchat-remove-question', function () {
1227 $(this).closest('.mxchat-question-row').remove();
1228 saveQuestions();
1229 });
1230
1231 $(document).on('change', '.mxchat-question-input', function() {
1232 saveQuestions();
1233 });
1234
1235 function saveQuestions() {
1236 const questions = [];
1237 $('.mxchat-question-input').each(function() {
1238 const value = $(this).val().trim();
1239 if (value) {
1240 questions.push(value);
1241 }
1242 });
1243
1244 const feedbackContainer = $('<div class="feedback-container"></div>');
1245 const spinner = $('<div class="saving-spinner"></div>');
1246 const successIcon = $('<div class="success-icon">✔</div>');
1247
1248 // Append feedback after the add button
1249 $('.mxchat-add-question').after(feedbackContainer);
1250 feedbackContainer.append(spinner);
1251
1252 // Save via AJAX
1253 $.ajax({
1254 url: mxchatAdmin.ajax_url,
1255 type: 'POST',
1256 data: {
1257 action: 'mxchat_save_setting',
1258 name: 'additional_popular_questions',
1259 value: JSON.stringify(questions),
1260 _ajax_nonce: mxchatAdmin.setting_nonce
1261 },
1262 success: function(response) {
1263 if (response.success) {
1264 spinner.fadeOut(200, function() {
1265 feedbackContainer.append(successIcon);
1266 successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
1267 feedbackContainer.remove();
1268 });
1269 });
1270 } else {
1271 alert('Error saving questions: ' + (response.data?.message || 'Unknown error'));
1272 feedbackContainer.remove();
1273 }
1274 },
1275 error: function() {
1276 alert('An error occurred while saving questions.');
1277 feedbackContainer.remove();
1278 }
1279 });
1280 }
1281
1282 // Live agent status handler
1283 const statusToggle = document.getElementById('live_agent_status');
1284 const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text');
1285 if (statusToggle && statusText) {
1286 statusToggle.addEventListener('change', function() {
1287 // Update display text
1288 statusText.textContent = this.checked ? 'Online' : 'Offline';
1289
1290 // Send the correct on/off value to the server
1291 if (window.mxchatSaveSetting) {
1292 window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off');
1293 }
1294 });
1295 }
1296
1297 // Function to adjust the textarea height to content
1298 function adjustTextareaHeight() {
1299 this.style.height = 'auto'; // Reset to auto to calculate scrollHeight
1300 this.style.height = this.scrollHeight + 'px'; // Expand to content height
1301 }
1302
1303 // Function to reset the textarea height to initial
1304 function resetTextareaHeight() {
1305 this.style.height = ''; // Remove inline height, reverting to CSS default
1306 }
1307
1308 // Target the specific textarea by ID
1309 var $textarea = $('#system_prompt_instructions');
1310
1311 // Bind events
1312 $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input
1313 .on('blur', resetTextareaHeight); // Reset on blur
1314 });
1315
1316 document.addEventListener('DOMContentLoaded', function() {
1317 // Check if we're on the correct page before initializing
1318 const modal = document.getElementById('mxchat-action-modal');
1319
1320 // Only initialize if the modal exists on this page
1321 if (modal) {
1322 //console.log('MXChat Action Modal JS Loaded');
1323
1324 // Initialize the action modal functionality
1325 initStepBasedActionModal();
1326 }
1327
1328 // Function to initialize the step-based action modal
1329 function initStepBasedActionModal() {
1330 // We already checked for modal existence above, so no need to check again
1331
1332 const actionStep1 = document.getElementById('mxchat-action-step-1');
1333 const actionStep2 = document.getElementById('mxchat-action-step-2');
1334 const backToStep1Btn = document.getElementById('mxchat-back-to-step-1');
1335 const searchInput = document.getElementById('action-type-search');
1336 const categoryButtons = modal.querySelectorAll('.mxchat-category-button');
1337 const actionCards = modal.querySelectorAll('.mxchat-action-type-card');
1338 const actionForm = document.getElementById('mxchat-action-form');
1339 const callbackInput = document.getElementById('callback_function');
1340 const actionIdField = document.getElementById('edit_action_id');
1341 const labelField = document.getElementById('intent_label');
1342 const phrasesField = document.getElementById('action_phrases');
1343 const formActionType = document.getElementById('form_action_type');
1344 const nonceContainer = document.getElementById('action-nonce-container');
1345 const thresholdSlider = document.getElementById('similarity_threshold');
1346 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1347
1348 // Rest of your initialization code remains the same...
1349
1350 // Log the structure of one action card for debugging
1351 if (actionCards.length > 0) {
1352 //console.log('First action card data attributes:', actionCards[0].dataset);
1353 //console.log('First action card HTML:', actionCards[0].outerHTML);
1354 }
1355
1356 // Add click event listeners to category buttons
1357 categoryButtons.forEach(button => {
1358 button.addEventListener('click', function() {
1359 //console.log('Category button clicked:', this.dataset.category);
1360
1361 // Remove active class from all buttons
1362 categoryButtons.forEach(btn => btn.classList.remove('active'));
1363
1364 // Add active class to clicked button
1365 this.classList.add('active');
1366
1367 // Get selected category
1368 const category = this.dataset.category;
1369
1370 // Filter action cards
1371 filterActionCards(category, searchInput.value);
1372 });
1373 });
1374
1375 // Add search functionality
1376 if (searchInput) {
1377 searchInput.addEventListener('input', function() {
1378 // Get active category
1379 const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all';
1380 //console.log('Search input changed, active category:', activeCategory);
1381
1382 // Filter action cards
1383 filterActionCards(activeCategory, this.value);
1384 });
1385 }
1386
1387 // Add click event listeners to action cards
1388 actionCards.forEach(card => {
1389 card.addEventListener('click', function() {
1390 // Get the action data
1391 const isPro = this.dataset.pro === 'true';
1392 const isInstalled = this.dataset.installed === 'true';
1393 const addonName = this.dataset.addon || '';
1394 const actionValue = this.dataset.value;
1395 const actionLabel = this.dataset.label;
1396 const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', '');
1397 const actionDescription = this.querySelector('p').textContent;
1398
1399 // Pro check using the proper detection method
1400 const proIsActivated = typeof mxchatAdmin !== 'undefined' &&
1401 (mxchatAdmin.is_activated === '1' ||
1402 mxchatAdmin.is_activated === 'true' ||
1403 mxchatAdmin.is_activated === true);
1404
1405 // Handle different states
1406 if (isPro && !proIsActivated) {
1407 // Pro feature but no Pro license
1408 showProFeatureNotice();
1409 return;
1410 }
1411
1412 if (addonName && !isInstalled) {
1413 // Add-on required but not installed
1414 const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '') || addonName + ' Add-on';
1415 showAddonRequiredNotice(addonDisplayName);
1416 return;
1417 }
1418
1419 // If we get here, the action is available - proceed as normal
1420 callbackInput.value = actionValue;
1421
1422 // Update the selected action display in step 2
1423 document.getElementById('selected-action-title').textContent = actionLabel;
1424 document.getElementById('selected-action-description').textContent = actionDescription;
1425 document.getElementById('selected-action-icon').innerHTML =
1426 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1427
1428 // Set a default label based on the action type (user can change it)
1429 if (!labelField.value) {
1430 labelField.value = actionLabel;
1431 }
1432
1433 // Move to step 2
1434 actionStep1.classList.remove('active');
1435 actionStep2.classList.add('active');
1436
1437 // Update modal title
1438 });
1439 });
1440
1441 // Back button functionality
1442 if (backToStep1Btn) {
1443 backToStep1Btn.addEventListener('click', function() {
1444 //console.log('Back button clicked');
1445 actionStep2.classList.remove('active');
1446 actionStep1.classList.add('active');
1447 });
1448 }
1449
1450 // Function to filter action cards by category and search term
1451 function filterActionCards(category, searchTerm) {
1452 searchTerm = searchTerm.toLowerCase().trim();
1453 //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`);
1454
1455 let visibleCount = 0;
1456
1457 // Show all cards initially with animation
1458 actionCards.forEach((card, index) => {
1459 // Reset animation
1460 card.style.animation = 'none';
1461 // Trigger reflow
1462 void card.offsetWidth;
1463
1464 // Determine if card should be visible based on category and search term
1465 const cardCategory = card.dataset.category || '';
1466 const matchesCategory = category === 'all' || cardCategory === category;
1467
1468 const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || '';
1469 const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || '';
1470 const matchesSearch = searchTerm === '' ||
1471 cardTitle.includes(searchTerm) ||
1472 cardDesc.includes(searchTerm);
1473
1474 // Show/hide card with animation
1475 if (matchesCategory && matchesSearch) {
1476 card.style.display = 'flex';
1477 // Staggered animation for cards
1478 card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`;
1479 visibleCount++;
1480 } else {
1481 card.style.display = 'none';
1482 }
1483 });
1484
1485 //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`);
1486 }
1487
1488 // Function to show notice for Pro features
1489 function showProFeatureNotice() {
1490 //console.log('Showing Pro feature notice');
1491 // Check if we already have a notification container
1492 let noticeContainer = document.querySelector('.mxchat-pro-notice');
1493
1494 if (!noticeContainer) {
1495 // Create the notice container
1496 noticeContainer = document.createElement('div');
1497 noticeContainer.className = 'mxchat-pro-notice';
1498
1499 // Create content
1500 noticeContainer.innerHTML = `
1501 <div class="mxchat-pro-notice-content">
1502 <h3>MxChat Pro Feature</h3>
1503 <p>This action is available in the Pro version only.</p>
1504 <div class="mxchat-pro-notice-buttons">
1505 <button class="mxchat-button-secondary mxchat-pro-notice-close">Close</button>
1506 <a href="https://mxchat.ai/" class="mxchat-button-primary">Upgrade to Pro</a>
1507 </div>
1508 </div>
1509 `;
1510
1511 // Append to body
1512 document.body.appendChild(noticeContainer);
1513
1514 // Add close functionality
1515 const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close');
1516 closeButton.addEventListener('click', function() {
1517 noticeContainer.classList.remove('active');
1518 setTimeout(() => {
1519 noticeContainer.remove();
1520 }, 300);
1521 });
1522
1523 // Click outside to close
1524 noticeContainer.addEventListener('click', function(e) {
1525 if (e.target === noticeContainer) {
1526 closeButton.click();
1527 }
1528 });
1529
1530 // Show with animation
1531 setTimeout(() => {
1532 noticeContainer.classList.add('active');
1533 }, 10);
1534 } else {
1535 // If it already exists, just make it visible again
1536 noticeContainer.classList.add('active');
1537 }
1538 }
1539
1540 // Function to show notice for add-on requirements
1541 function showAddonRequiredNotice(addonName) {
1542 //console.log(`Showing add-on notice for: ${addonName}`);
1543 // Check if we already have a notification container
1544 let noticeContainer = document.querySelector('.mxchat-addon-notice');
1545
1546 if (!noticeContainer) {
1547 // Create the notice container
1548 noticeContainer = document.createElement('div');
1549 noticeContainer.className = 'mxchat-addon-notice';
1550
1551 // Create content
1552 noticeContainer.innerHTML = `
1553 <div class="mxchat-addon-notice-content">
1554 <span class="mxchat-addon-notice-icon">🧩</span>
1555 <h3>Add-on Required</h3>
1556 <p>This action requires the <strong>${addonName}</strong> add-on to be installed.</p>
1557 <div class="mxchat-addon-notice-buttons">
1558 <button class="mxchat-button-secondary mxchat-addon-notice-close">Close</button>
1559 <a href="admin.php?page=mxchat-addons" class="mxchat-button-primary">Get Add-ons</a>
1560 </div>
1561 </div>
1562 `;
1563
1564 // Append to body
1565 document.body.appendChild(noticeContainer);
1566
1567 // Add close functionality
1568 const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close');
1569 closeButton.addEventListener('click', function() {
1570 noticeContainer.classList.remove('active');
1571 setTimeout(() => {
1572 noticeContainer.remove();
1573 }, 300);
1574 });
1575
1576 // Click outside to close
1577 noticeContainer.addEventListener('click', function(e) {
1578 if (e.target === noticeContainer) {
1579 closeButton.click();
1580 }
1581 });
1582
1583 // Show with animation
1584 setTimeout(() => {
1585 noticeContainer.classList.add('active');
1586 }, 10);
1587 } else {
1588 // If it already exists, update the content
1589 const addonNameElement = noticeContainer.querySelector('p strong');
1590 if (addonNameElement) {
1591 addonNameElement.textContent = addonName;
1592 }
1593
1594 // Make it visible again
1595 noticeContainer.classList.add('active');
1596 }
1597 }
1598
1599 // Form submission handling
1600 if (actionForm) {
1601 actionForm.addEventListener('submit', function() {
1602 //console.log('Form submitted');
1603 document.getElementById('mxchat-action-loading').style.display = 'flex';
1604 this.querySelector('button[type="submit"]').disabled = true;
1605 });
1606 }
1607 }
1608
1609 // Setup add action buttons (only if we're on the correct page)
1610 if (modal) {
1611 // Update the modal open function to support the step-based flow
1612 window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') {
1613 //console.log('Modal opening, edit mode:', isEdit);
1614
1615 // No need to check again, we already verified modal exists
1616
1617 // Get form fields
1618 const actionIdField = document.getElementById('edit_action_id');
1619 const labelField = document.getElementById('intent_label');
1620 const phrasesField = document.getElementById('action_phrases');
1621 const formActionType = document.getElementById('form_action_type');
1622 const callbackInput = document.getElementById('callback_function');
1623 const saveButton = document.getElementById('mxchat-save-action-btn');
1624 const nonceContainer = document.getElementById('action-nonce-container');
1625 const thresholdSlider = document.getElementById('similarity_threshold');
1626 const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display');
1627 const actionStep1 = document.getElementById('mxchat-action-step-1');
1628 const actionStep2 = document.getElementById('mxchat-action-step-2');
1629 const searchInput = document.getElementById('action-type-search');
1630
1631 // Set up modal for edit or create
1632 if (isEdit) {
1633 saveButton.textContent = 'Update Action';
1634 formActionType.value = 'mxchat_edit_intent';
1635 actionIdField.value = actionId;
1636 labelField.value = label;
1637 phrasesField.value = phrases;
1638 callbackInput.value = callbackFunction;
1639 thresholdSlider.value = threshold; // Set the current threshold value
1640 thresholdDisplay.textContent = threshold + '%'; // Update display
1641
1642 // Update the nonce field for editing
1643 nonceContainer.innerHTML = ''; // Clear existing nonce
1644 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) {
1645 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.edit_intent_nonce}">`;
1646 }
1647
1648 // For editing, go directly to step 2 and update the selected action display
1649 actionStep1.classList.remove('active');
1650 actionStep2.classList.add('active');
1651
1652 // Find the matching action card to get its details
1653 const actionCards = document.querySelectorAll('.mxchat-action-type-card');
1654 let foundCard = null;
1655
1656 actionCards.forEach(card => {
1657 if (card.dataset.value === callbackFunction) {
1658 foundCard = card;
1659 }
1660 });
1661
1662 if (foundCard) {
1663 //console.log('Found matching action card for:', callbackFunction);
1664 const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || '';
1665 const actionIconElement = foundCard.querySelector('.dashicons');
1666 const actionIcon = actionIconElement
1667 ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '')
1668 : 'admin-generic';
1669 const actionDescription = foundCard.querySelector('p')?.textContent || '';
1670
1671 document.getElementById('selected-action-title').textContent = actionLabel;
1672 document.getElementById('selected-action-description').textContent = actionDescription;
1673 document.getElementById('selected-action-icon').innerHTML =
1674 `<span class="dashicons dashicons-${actionIcon}"></span>`;
1675 } else {
1676 //console.log('No matching action card found for:', callbackFunction);
1677 // Fallback if we can't find the card
1678 document.getElementById('selected-action-title').textContent = label;
1679 document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot';
1680 document.getElementById('selected-action-icon').innerHTML =
1681 `<span class="dashicons dashicons-admin-generic"></span>`;
1682 }
1683 } else {
1684 //console.log('Setting up create mode');
1685 saveButton.textContent = 'Save Action';
1686 formActionType.value = 'mxchat_add_intent';
1687 actionIdField.value = '';
1688 labelField.value = '';
1689 phrasesField.value = '';
1690 callbackInput.value = '';
1691 thresholdSlider.value = 85; // Default value for new actions
1692 thresholdDisplay.textContent = '85%'; // Default display
1693
1694 // Update the nonce field for adding
1695 nonceContainer.innerHTML = ''; // Clear existing nonce
1696 if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) {
1697 nonceContainer.innerHTML = `<input type="hidden" name="_wpnonce" value="${mxchatAdmin.add_intent_nonce}">`;
1698 }
1699
1700 // For creating new, start at step 1
1701 actionStep1.classList.add('active');
1702 actionStep2.classList.remove('active');
1703 }
1704
1705 // Show modal with animation
1706 modal.style.display = 'flex';
1707 requestAnimationFrame(() => {
1708 modal.classList.add('active');
1709 });
1710
1711 // Set up close handlers
1712 const closeModal = () => {
1713 //console.log('Closing modal');
1714 modal.classList.remove('active');
1715 setTimeout(() => {
1716 modal.style.display = 'none';
1717 }, 300); // Match the CSS transition time
1718 };
1719
1720 // Close button handler
1721 const closeBtn = modal.querySelector('.mxchat-modal-close');
1722 if (closeBtn) {
1723 closeBtn.onclick = closeModal;
1724 }
1725
1726 // Cancel button handler
1727 const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel');
1728 if (cancelBtns) {
1729 cancelBtns.forEach(btn => {
1730 btn.onclick = closeModal;
1731 });
1732 }
1733
1734 // Click outside modal to close
1735 modal.onclick = (e) => {
1736 if (e.target === modal) {
1737 closeModal();
1738 }
1739 };
1740
1741 // Escape key to close modal
1742 document.addEventListener('keydown', function(e) {
1743 if (e.key === 'Escape' && modal.classList.contains('active')) {
1744 closeModal();
1745 }
1746 }, { once: true });
1747
1748 // Focus appropriate field based on current step
1749 if (isEdit || actionStep2.classList.contains('active')) {
1750 if (labelField) labelField.focus();
1751 } else {
1752 if (searchInput) searchInput.focus();
1753 }
1754
1755 return closeModal; // Return close function for external use
1756 };
1757
1758 // Setup add action buttons
1759 const addActionBtn = document.getElementById('mxchat-add-action-btn');
1760 if (addActionBtn) {
1761 //console.log('Add action button found');
1762 addActionBtn.onclick = () => window.mxchatOpenActionModal();
1763 }
1764
1765 const createFirstAction = document.getElementById('mxchat-create-first-action');
1766 if (createFirstAction) {
1767 //console.log('Create first action button found');
1768 createFirstAction.onclick = () => window.mxchatOpenActionModal();
1769 }
1770
1771 // Setup edit buttons
1772 const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button');
1773 //console.log('Edit buttons found:', editButtons.length);
1774 editButtons.forEach(button => {
1775 button.onclick = () => {
1776 const actionId = button.dataset.actionId;
1777 const phrases = button.dataset.phrases;
1778 const label = button.dataset.label;
1779 const threshold = button.dataset.threshold || 85;
1780 const callbackFunction = button.dataset.callbackFunction;
1781
1782 window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction);
1783 };
1784 });
1785 }
1786 });
1787
1788 jQuery(document).ready(function($) {
1789 // Toggle custom post types container
1790 $('#mxchat-custom-post-types-toggle').on('click', function(e) {
1791 e.preventDefault();
1792
1793 $('#mxchat-custom-post-types-container').slideToggle(300);
1794
1795 // Rotate the toggle icon
1796 const $icon = $(this).find('.mxchat-accordion-icon');
1797 if ($('#mxchat-custom-post-types-container').is(':visible')) {
1798 $icon.css('transform', 'rotate(180deg)');
1799 $(this).closest('.mxchat-settings-accordion').addClass('active');
1800 } else {
1801 $icon.css('transform', 'rotate(0deg)');
1802 $(this).closest('.mxchat-settings-accordion').removeClass('active');
1803 }
1804 });
1805
1806 // If there are any selections made, auto-expand the container
1807 function autoExpandIfNeeded() {
1808 // Check if any checkbox in the container is checked
1809 const hasCheckedItems = $('#mxchat-custom-post-types-container input[type="checkbox"]:checked').length > 0;
1810
1811 if (hasCheckedItems) {
1812 $('#mxchat-custom-post-types-container').show();
1813 $('#mxchat-custom-post-types-toggle .mxchat-accordion-icon').css('transform', 'rotate(180deg)');
1814 $('.mxchat-settings-accordion').addClass('active');
1815 }
1816 }
1817
1818 // Run on page load
1819 autoExpandIfNeeded();
1820 });
1821
1822 jQuery(document).ready(function($) {
1823 // Track if a form has been submitted to trigger updates
1824 let formSubmitted = false;
1825
1826 // Global interval ID to manage the polling
1827 let updateIntervalId = null;
1828
1829 // Check if we're on the right admin page with status cards or import forms
1830 if ($('.mxchat-status-card').length > 0 || $('.mxchat-import-options').length > 0) {
1831 //console.log('MxChat: Status update script initialized');
1832 // Initialize AJAX status updates
1833 initStatusUpdates();
1834 }
1835
1836 // Initialize status updates
1837 function initStatusUpdates() {
1838 // Get the refresh interval (default to 3 seconds if not set)
1839 const refreshInterval = parseInt(mxchatAdmin.status_refresh_interval || 3000);
1840
1841 // Check if there are active status cards
1842 const hasActiveStatus = $('.mxchat-status-card').length > 0;
1843
1844 // Set up form submission listeners
1845 $('#mxchat-url-form, #mxchat-content-form').on('submit', function() {
1846 //console.log('MxChat: Form submitted, will start checking for updates');
1847 formSubmitted = true;
1848
1849 // Store submission info in sessionStorage to persist through redirects
1850 sessionStorage.setItem('mxchat_form_submitted', 'true');
1851 sessionStorage.setItem('mxchat_form_submitted_time', Date.now());
1852
1853 // Start checking for status updates right away
1854 startPolling(refreshInterval);
1855
1856 // Create a temporary message
1857 if ($('.mxchat-processing-message').length === 0) {
1858 const message = $('<div class="mxchat-processing-message" style="text-align: center; padding: 15px; background: #f0f7ff; border-radius: 8px; margin-top: 15px;">Processing request... Status will update automatically.</div>');
1859 $('.mxchat-import-section').after(message);
1860
1861 // Fade out after 5 seconds
1862 setTimeout(function() {
1863 message.fadeOut(500, function() {
1864 $(this).remove();
1865 });
1866 }, 5000);
1867 }
1868 });
1869
1870 // Listen for import option clicks
1871 $('.mxchat-import-box').on('click', function() {
1872 const option = $(this).data('option');
1873 //console.log('MxChat: Import option clicked - ' + option);
1874 });
1875
1876 // Check if we recently submitted a form (within last 30 seconds)
1877 if (sessionStorage.getItem('mxchat_form_submitted') === 'true') {
1878 const submittedTime = parseInt(sessionStorage.getItem('mxchat_form_submitted_time') || '0');
1879 if (Date.now() - submittedTime < 30000) { // 30 seconds
1880 //console.log('MxChat: Detected recent form submission via sessionStorage');
1881 formSubmitted = true;
1882 } else {
1883 // Clear old submission data
1884 sessionStorage.removeItem('mxchat_form_submitted');
1885 sessionStorage.removeItem('mxchat_form_submitted_time');
1886 }
1887 }
1888
1889 // Attach event listener to stop button to clear the interval
1890 $('.mxchat-stop-form').on('submit', function() {
1891 //console.log('MxChat: Stop processing requested, clearing update interval');
1892 stopPolling();
1893 sessionStorage.removeItem('mxchat_form_submitted');
1894 sessionStorage.removeItem('mxchat_form_submitted_time');
1895 });
1896
1897 // Start the interval for automatic updates if we have status cards or a form was submitted
1898 if (hasActiveStatus || formSubmitted) {
1899 //console.log('MxChat: Starting automatic status checks');
1900 startPolling(refreshInterval);
1901 }
1902 }
1903
1904 // Function to start polling
1905 function startPolling(interval) {
1906 // Clear any existing interval first
1907 stopPolling();
1908
1909 // Do an initial fetch immediately
1910 fetchStatusUpdates();
1911
1912 // Set up new interval
1913 updateIntervalId = setInterval(function() {
1914 fetchStatusUpdates();
1915 }, interval);
1916
1917 //console.log('MxChat: Polling started with interval', interval);
1918 }
1919
1920 // Function to stop polling
1921 function stopPolling() {
1922 if (updateIntervalId !== null) {
1923 clearInterval(updateIntervalId);
1924 updateIntervalId = null;
1925 //console.log('MxChat: Polling stopped');
1926 }
1927 }
1928
1929 // Fetch status updates from the server
1930 function fetchStatusUpdates() {
1931 // If user is actively viewing the failed URLs, don't refresh as frequently
1932 const $details = $('.mxchat-failed-urls-container details');
1933 const isUserViewing = $details.length > 0 && $details.prop('open');
1934
1935 // If details are open, we'll refresh at a slower rate
1936 if (isUserViewing) {
1937 // Alternative: Update less frequently when details are open
1938 setTimeout(function() {
1939 performStatusUpdate();
1940 }, 5000); // Slow down updates to every 5 seconds when details are open
1941 } else {
1942 performStatusUpdate();
1943 }
1944 }
1945
1946 // Perform the actual AJAX request
1947 function performStatusUpdate() {
1948 //console.log('MxChat: Checking for status updates...');
1949
1950 $.ajax({
1951 url: ajaxurl,
1952 type: 'POST',
1953 data: {
1954 action: 'mxchat_get_status_updates',
1955 nonce: mxchatAdmin.status_nonce
1956 },
1957 success: function(response) {
1958 //console.log('MxChat: Status update received');
1959
1960 // If we have active processing or the form was submitted
1961 if ((response && response.is_processing) || formSubmitted) {
1962 updateStatusUI(response);
1963
1964 // If we get a complete status, reload the page
1965 if (response.sitemap_status && response.sitemap_status.status === 'complete') {
1966 //console.log('MxChat: Sitemap processing complete, reloading page');
1967 // Clear session storage before reload
1968 sessionStorage.removeItem('mxchat_form_submitted');
1969 sessionStorage.removeItem('mxchat_form_submitted_time');
1970 setTimeout(function() {
1971 location.reload();
1972 }, 1000);
1973 return;
1974 }
1975
1976 if (response.pdf_status && response.pdf_status.status === 'complete') {
1977 //console.log('MxChat: PDF processing complete, reloading page');
1978 // Clear session storage before reload
1979 sessionStorage.removeItem('mxchat_form_submitted');
1980 sessionStorage.removeItem('mxchat_form_submitted_time');
1981 setTimeout(function() {
1982 location.reload();
1983 }, 1000);
1984 return;
1985 }
1986
1987 // Reset form submitted flag if no active processing
1988 if (!response.is_processing) {
1989 formSubmitted = false;
1990 // Clear session storage when no longer processing
1991 sessionStorage.removeItem('mxchat_form_submitted');
1992 sessionStorage.removeItem('mxchat_form_submitted_time');
1993 // Also stop polling when processing is complete
1994 stopPolling();
1995 }
1996 }
1997
1998 // Show single URL status if available and no active processing
1999 if (response.single_url_status && !response.is_processing) {
2000 updateSingleUrlStatus(response.single_url_status);
2001 }
2002 },
2003 error: function(xhr, status, error) {
2004 console.error('MxChat: Status update failed:', error);
2005 }
2006 });
2007 }
2008
2009 // Update the UI with status information
2010 function updateStatusUI(data) {
2011 // Update PDF status if available
2012 if (data.pdf_status) {
2013 updatePdfStatus(data.pdf_status);
2014 }
2015
2016 // Update sitemap status if available
2017 if (data.sitemap_status) {
2018 updateSitemapStatus(data.sitemap_status);
2019 }
2020
2021 // Handle single URL status if available and no active processing
2022 if (data.single_url_status && !data.is_processing) {
2023 updateSingleUrlStatus(data.single_url_status);
2024 } else if (data.is_processing) {
2025 // Hide single URL status while processing
2026 $('#mxchat-single-url-status-container').hide();
2027 }
2028 }
2029
2030 // Update PDF status card
2031 function updatePdfStatus(status) {
2032 // Check if PDF card exists
2033 let $pdfCard = $('.mxchat-status-card:contains("PDF Processing")');
2034
2035 // If no card exists but we have status, create it
2036 if ($pdfCard.length === 0 && status) {
2037 //console.log('MxChat: Creating new PDF status card');
2038 createPdfStatusCard(status);
2039 $pdfCard = $('.mxchat-status-card:contains("PDF Processing")');
2040 }
2041
2042 // If card exists, update it
2043 if ($pdfCard.length > 0) {
2044 // Update progress bar
2045 $pdfCard.find('.mxchat-progress-fill').css('width', status.percentage + '%');
2046
2047 // Update progress text
2048 $pdfCard.find('.mxchat-status-details p:first').text(
2049 'Progress: ' + status.processed_pages + ' of ' +
2050 status.total_pages + ' pages (' + status.percentage + '%)'
2051 );
2052
2053 // Update status text (if it exists)
2054 const $statusText = $pdfCard.find('.mxchat-status-details p:nth-child(2)');
2055 if ($statusText.length > 0) {
2056 $statusText.text('Status: ' + status.status.charAt(0).toUpperCase() + status.status.slice(1));
2057 }
2058
2059 // Update last update text (if it exists)
2060 const $lastUpdateText = $pdfCard.find('.mxchat-status-details p:nth-child(3)');
2061 if ($lastUpdateText.length > 0) {
2062 $lastUpdateText.text('Last update: ' + status.last_update);
2063 }
2064
2065 // If we have an error, show it
2066 if (status.status === 'error' && status.error) {
2067 let $errorNotice = $pdfCard.find('.mxchat-error-notice');
2068
2069 if ($errorNotice.length === 0) {
2070 $errorNotice = $('<div class="mxchat-error-notice"><p class="error"></p></div>');
2071 $pdfCard.find('.mxchat-status-details').append($errorNotice);
2072 }
2073
2074 $errorNotice.find('p.error').text(status.error);
2075
2076 // Make sure error badge is shown
2077 if ($pdfCard.find('.mxchat-status-badge.mxchat-status-failed').length === 0) {
2078 $pdfCard.find('.mxchat-status-header').append('<span class="mxchat-status-badge mxchat-status-failed">Error</span>');
2079 }
2080 }
2081 }
2082 }
2083
2084 // Create a new PDF status card
2085 function createPdfStatusCard(status) {
2086 let html = '<div class="mxchat-status-card">';
2087 html += '<div class="mxchat-status-header">';
2088 html += '<h4>PDF Processing Status</h4>';
2089
2090 // Add stop processing form if processing
2091 if (status.status === 'processing') {
2092 html += '<form method="post" class="mxchat-stop-form" action="' +
2093 mxchatAdmin.admin_url + 'admin-post.php?action=mxchat_stop_processing">';
2094 html += '<input type="hidden" name="mxchat_stop_processing_nonce" value="' +
2095 mxchatAdmin.stop_nonce + '">';
2096 html += '<button type="submit" name="stop_processing" class="mxchat-button-secondary">';
2097 html += 'Stop Processing</button></form>';
2098 }
2099
2100 // Add error badge if error
2101 if (status.status === 'error') {
2102 html += '<span class="mxchat-status-badge mxchat-status-failed">Error</span>';
2103 }
2104
2105 html += '</div>'; // End header
2106
2107 // Progress bar
2108 html += '<div class="mxchat-progress-bar">';
2109 html += '<div class="mxchat-progress-fill" style="width: ' + status.percentage + '%"></div>';
2110 html += '</div>';
2111
2112 // Status details
2113 html += '<div class="mxchat-status-details">';
2114 html += '<p>Progress: ' + status.processed_pages + ' of ' +
2115 status.total_pages + ' pages (' + status.percentage + '%)</p>';
2116 html += '<p>Status: ' + status.status.charAt(0).toUpperCase() + status.status.slice(1) + '</p>';
2117 html += '<p>Last update: ' + status.last_update + '</p>';
2118
2119 // Add error message if any
2120 if (status.status === 'error' && status.error) {
2121 html += '<div class="mxchat-error-notice">';
2122 html += '<p class="error">' + status.error + '</p>';
2123 html += '</div>';
2124 }
2125
2126 html += '</div>'; // End details
2127 html += '</div>'; // End card
2128
2129 // Try to find the import tab content to insert the status card into
2130 let $importTabContent = $('#mxchat-kb-tab-import');
2131 if ($importTabContent.length > 0) {
2132 // For the tabbed interface, add to the import tab
2133 let $sitemapCard = $importTabContent.find('.mxchat-status-card:contains("Sitemap Processing")');
2134 if ($sitemapCard.length > 0) {
2135 $sitemapCard.before($(html));
2136 } else {
2137 $importTabContent.find('.mxchat-import-section').after($(html));
2138 }
2139 } else {
2140 // Fallback to the old method
2141 let $sitemapCard = $('.mxchat-status-card:contains("Sitemap Processing")');
2142 if ($sitemapCard.length > 0) {
2143 $sitemapCard.before($(html));
2144 } else {
2145 $('.mxchat-import-section').after($(html));
2146 }
2147 }
2148 }
2149
2150 // Update sitemap status card
2151 function updateSitemapStatus(status) {
2152 // Check if sitemap card exists
2153 let $sitemapCard = $('.mxchat-status-card:contains("Sitemap Processing")');
2154
2155 // If no card exists but we have status, create it
2156 if ($sitemapCard.length === 0 && status) {
2157 //console.log('MxChat: Creating new sitemap status card');
2158 createSitemapStatusCard(status);
2159 $sitemapCard = $('.mxchat-status-card:contains("Sitemap Processing")');
2160 }
2161
2162 // If card exists, update it
2163 if ($sitemapCard.length > 0) {
2164 // Update progress bar
2165 $sitemapCard.find('.mxchat-progress-fill').css('width', status.percentage + '%');
2166
2167 // Update progress text
2168 $sitemapCard.find('.mxchat-status-details p:first').text(
2169 'Progress: ' + status.processed_urls + ' of ' +
2170 status.total_urls + ' URLs (' + status.percentage + '%)'
2171 );
2172
2173 // Check if details is already open before updating
2174 const isDetailsOpen = $sitemapCard.find('.mxchat-failed-urls-container details').prop('open');
2175
2176 // Update errors display
2177 let $errorContainer = $sitemapCard.find('.mxchat-error-notice');
2178
2179 if ($errorContainer.length === 0 &&
2180 (status.error || status.last_error || (status.failed_urls_list && status.failed_urls_list.length > 0))) {
2181 // Create error container if it doesn't exist
2182 $errorContainer = $('<div class="mxchat-error-notice"></div>');
2183 $sitemapCard.find('.mxchat-status-details').append($errorContainer);
2184 }
2185
2186 // Update or create error notices
2187 if ($errorContainer.length > 0) {
2188 let errorHTML = '';
2189
2190 if (status.error) {
2191 errorHTML += '<p class="error">' + status.error + '</p>';
2192 }
2193
2194 if (status.last_error) {
2195 errorHTML += '<p class="last-error">Last error: ' + status.last_error + '</p>';
2196 }
2197
2198 // Add failed URLs list
2199 if (status.failed_urls_list && status.failed_urls_list.length > 0) {
2200 errorHTML += '<div class="mxchat-failed-urls-container">';
2201 errorHTML += '<h5>Failed URLs (' + status.failed_urls_list.length + ')</h5>';
2202
2203 // Set the 'open' attribute based on previous state
2204 errorHTML += '<details' + (isDetailsOpen ? ' open' : '') + '>';
2205 errorHTML += '<summary>Show Failed URLs</summary>';
2206 errorHTML += '<div class="mxchat-failed-urls-list">';
2207
2208 // Create table for failed URLs
2209 errorHTML += '<table class="widefat striped">';
2210 errorHTML += '<thead><tr><th>URL</th><th>Error</th><th>Time</th></tr></thead>';
2211 errorHTML += '<tbody>';
2212
2213 // Sort failed URLs by most recent
2214 const sortedFailedUrls = [...status.failed_urls_list].sort((a, b) => b.time - a.time);
2215
2216 // Show up to 50 failed URLs
2217 const displayUrls = sortedFailedUrls.slice(0, 50);
2218
2219 displayUrls.forEach(item => {
2220 const timeAgo = formatTimeAgo(item.time);
2221 errorHTML += '<tr>';
2222 errorHTML += '<td style="word-break: break-all;">';
2223 errorHTML += '<a href="' + item.url + '" target="_blank" rel="noopener noreferrer">';
2224 errorHTML += truncateUrl(item.url) + '</a></td>';
2225 errorHTML += '<td>' + item.error + '</td>';
2226 errorHTML += '<td>' + timeAgo + '</td>';
2227 errorHTML += '</tr>';
2228 });
2229
2230 errorHTML += '</tbody></table>';
2231
2232 if (status.failed_urls_list.length > 50) {
2233 errorHTML += '<div class="mxchat-failed-urls-more">+ ' +
2234 (status.failed_urls_list.length - 50) +
2235 ' more failed URLs not shown</div>';
2236 }
2237
2238 errorHTML += '</div>'; // End of failed-urls-list
2239 errorHTML += '</details>';
2240 errorHTML += '</div>'; // End of failed-urls-container
2241 }
2242
2243 $errorContainer.html(errorHTML);
2244
2245 // Additionally, add a click handler to pause refreshes when viewing details
2246 $sitemapCard.find('.mxchat-failed-urls-container details').on('toggle', function() {
2247 if (this.open) {
2248 // User opened the details - set a flag
2249 $(this).data('user-opened', true);
2250 } else {
2251 // User closed the details - remove the flag
2252 $(this).data('user-opened', false);
2253 }
2254 });
2255 }
2256 }
2257 }
2258
2259 // Create a new sitemap status card
2260 function createSitemapStatusCard(status) {
2261 let html = '<div class="mxchat-status-card">';
2262 html += '<div class="mxchat-status-header">';
2263 html += '<h4>Sitemap Processing Status</h4>';
2264
2265 // Add stop processing form if processing
2266 if (status.status === 'processing') {
2267 html += '<form method="post" class="mxchat-stop-form" action="' +
2268 mxchatAdmin.admin_url + 'admin-post.php?action=mxchat_stop_processing">';
2269 html += '<input type="hidden" name="mxchat_stop_processing_nonce" value="' +
2270 mxchatAdmin.stop_nonce + '">';
2271 html += '<button type="submit" name="stop_processing" class="mxchat-button-secondary">';
2272 html += 'Stop Processing</button></form>';
2273 }
2274
2275 // Add error badge if error
2276 if (status.status === 'error') {
2277 html += '<span class="mxchat-status-badge mxchat-status-failed">Error</span>';
2278 }
2279
2280 html += '</div>'; // End header
2281
2282 // Progress bar
2283 html += '<div class="mxchat-progress-bar">';
2284 html += '<div class="mxchat-progress-fill" style="width: ' + status.percentage + '%"></div>';
2285 html += '</div>';
2286
2287 // Status details
2288 html += '<div class="mxchat-status-details">';
2289 html += '<p>Progress: ' + status.processed_urls + ' of ' +
2290 status.total_urls + ' URLs (' + status.percentage + '%)</p>';
2291
2292 // Add error message if any
2293 if ((status.error || status.last_error) && status.status === 'error') {
2294 html += '<div class="mxchat-error-notice">';
2295
2296 if (status.error) {
2297 html += '<p class="error">' + status.error + '</p>';
2298 }
2299
2300 if (status.last_error) {
2301 html += '<p class="last-error">Last error: ' + status.last_error + '</p>';
2302 }
2303
2304 html += '</div>';
2305 }
2306
2307 html += '</div>'; // End details
2308 html += '</div>'; // End card
2309
2310 // Try to find the import tab content to insert the status card into
2311 let $importTabContent = $('#mxchat-kb-tab-import');
2312 if ($importTabContent.length > 0) {
2313 // For the tabbed interface, add to the import tab
2314 let $pdfCard = $importTabContent.find('.mxchat-status-card:contains("PDF Processing")');
2315 if ($pdfCard.length > 0) {
2316 $pdfCard.after($(html));
2317 } else {
2318 $importTabContent.find('.mxchat-import-section').after($(html));
2319 }
2320 } else {
2321 // Fallback to the old method
2322 let $pdfCard = $('.mxchat-status-card:contains("PDF Processing")');
2323 if ($pdfCard.length > 0) {
2324 $pdfCard.after($(html));
2325 } else {
2326 $('.mxchat-import-section').after($(html));
2327 }
2328 }
2329 }
2330
2331 // Update single URL status
2332 function updateSingleUrlStatus(status) {
2333 // Check if container exists
2334 let $container = $('#mxchat-single-url-status-container');
2335
2336 if ($container.length === 0) {
2337 // Create container
2338 $container = $('<div id="mxchat-single-url-status-container"></div>');
2339
2340 // Try to find the import tab content to insert the status card into
2341 let $importTabContent = $('#mxchat-kb-tab-import');
2342 if ($importTabContent.length > 0) {
2343 // For the tabbed interface, add to the import tab
2344 let $lastStatusCard = $importTabContent.find('.mxchat-status-card').last();
2345 if ($lastStatusCard.length > 0) {
2346 $lastStatusCard.after($container);
2347 } else {
2348 $importTabContent.find('.mxchat-import-section').after($container);
2349 }
2350 } else {
2351 // Fallback to the old method
2352 let $lastStatusCard = $('.mxchat-status-card').last();
2353 if ($lastStatusCard.length > 0) {
2354 $lastStatusCard.after($container);
2355 } else {
2356 $('.mxchat-import-section').after($container);
2357 }
2358 }
2359 }
2360
2361 // Update container content
2362 let html = '<div class="mxchat-status-card">';
2363 html += '<div class="mxchat-status-header">';
2364 html += '<h4>Last URL Submission</h4>';
2365
2366 if (status.status === 'failed') {
2367 html += '<span class="mxchat-status-badge mxchat-status-failed">Failed</span>';
2368 } else {
2369 html += '<span class="mxchat-status-badge mxchat-status-success">Success</span>';
2370 }
2371
2372 html += '</div>'; // End header
2373
2374 html += '<div class="mxchat-status-details">';
2375 html += '<p><strong>URL:</strong> ';
2376 html += '<a href="' + status.url + '" target="_blank">';
2377
2378 // Truncate URL if needed
2379 const displayUrl = status.url.length > 60 ? status.url.substring(0, 57) + '...' : status.url;
2380 html += displayUrl;
2381
2382 html += '</a></p>';
2383 html += '<p><strong>Submitted:</strong> ' + status.human_time + '</p>';
2384
2385 if (status.status === 'failed' && status.error) {
2386 html += '<div class="mxchat-error-notice">';
2387 html += '<p class="error">' + status.error + '</p>';
2388 html += '</div>';
2389 }
2390
2391 if (status.status === 'complete') {
2392 html += '<p><strong>Content Length:</strong> ' + status.content_length + ' characters</p>';
2393 html += '<p><strong>Embedding Dimensions:</strong> ' + status.embedding_dimensions + '</p>';
2394 }
2395
2396 html += '</div>'; // End details
2397 html += '</div>'; // End card
2398
2399 $container.html(html).show();
2400 }
2401
2402 // Helper function to format time ago
2403 function formatTimeAgo(timestamp) {
2404 const now = Math.floor(Date.now() / 1000);
2405 const seconds = now - timestamp;
2406
2407 if (seconds < 60) {
2408 return seconds + ' seconds ago';
2409 } else if (seconds < 3600) {
2410 return Math.floor(seconds / 60) + ' minutes ago';
2411 } else if (seconds < 86400) {
2412 return Math.floor(seconds / 3600) + ' hours ago';
2413 } else {
2414 return Math.floor(seconds / 86400) + ' days ago';
2415 }
2416 }
2417
2418 // Helper function to truncate long URLs
2419 function truncateUrl(url) {
2420 const maxLength = 50;
2421 if (url.length <= maxLength) return url;
2422
2423 // Remove protocol
2424 let displayUrl = url.replace(/^https?:\/\//, '');
2425
2426 if (displayUrl.length <= maxLength) return displayUrl;
2427
2428 // Keep the domain and truncate the path
2429 const domainMatch = displayUrl.match(/^([^\/]+)\//);
2430 if (domainMatch) {
2431 const domain = domainMatch[1];
2432 const path = displayUrl.substring(domain.length);
2433
2434 if (path.length > 10) {
2435 return domain + path.substring(0, maxLength - domain.length - 3) + '...';
2436 }
2437 }
2438
2439 // Final fallback for very long strings
2440 return displayUrl.substring(0, maxLength - 3) + '...';
2441 }
2442 });