// Simple debounce function implementation function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Helper function to open edit modal for intents/actions function mxchatOpenEditModal(intentId, phrases) { const modal = document.getElementById('mxchat-edit-modal'); if (!modal) return; // Get form fields const intentIdField = document.getElementById('edit_intent_id'); const phrasesField = document.getElementById('edit_phrases'); // Set values intentIdField.value = intentId; phrasesField.value = phrases; // Show modal with animation modal.style.display = 'flex'; requestAnimationFrame(() => { modal.classList.add('active'); }); // Set up close handlers const closeModal = () => { modal.classList.remove('active'); setTimeout(() => { modal.style.display = 'none'; }, 300); // Match the CSS transition time }; // Close button handler const closeBtn = modal.querySelector('.mxchat-modal-close'); if (closeBtn) { closeBtn.onclick = closeModal; } // Cancel button handler const cancelBtn = modal.querySelector('.mxchat-modal-cancel'); if (cancelBtn) { cancelBtn.onclick = closeModal; } // Click outside modal to close modal.onclick = (e) => { if (e.target === modal) { closeModal(); } }; // Focus the textarea phrasesField.focus(); } // Updated mxchatOpenActionModal function to integrate with the new selector function mxchatOpenActionModal(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') { const modal = document.getElementById('mxchat-action-modal'); if (!modal) return; // Get form fields const actionIdField = document.getElementById('edit_action_id'); const labelField = document.getElementById('intent_label'); const phrasesField = document.getElementById('action_phrases'); const formActionType = document.getElementById('form_action_type'); const callbackGroup = document.getElementById('callback_selection_group'); const callbackSelect = document.getElementById('callback_function'); const saveButton = document.getElementById('mxchat-save-action-btn'); const nonceContainer = document.getElementById('action-nonce-container'); const thresholdSlider = document.getElementById('similarity_threshold'); const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display'); // Set up modal for edit or create if (isEdit) { saveButton.textContent = 'Update Action'; formActionType.value = 'mxchat_edit_intent'; actionIdField.value = actionId; labelField.value = label; phrasesField.value = phrases; callbackGroup.style.display = 'none'; // Hide callback selection when editing thresholdSlider.value = threshold; // Set the current threshold value thresholdDisplay.textContent = threshold + '%'; // Update display // Remove the required attribute when editing callbackSelect.removeAttribute('required'); // Update the nonce field for editing nonceContainer.innerHTML = ''; // Clear existing nonce if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) { nonceContainer.innerHTML = ``; } } else { saveButton.textContent = 'Save Action'; formActionType.value = 'mxchat_add_intent'; actionIdField.value = ''; labelField.value = ''; phrasesField.value = ''; callbackGroup.style.display = 'block'; // Show callback selection when creating thresholdSlider.value = 85; // Default value for new actions thresholdDisplay.textContent = '85%'; // Default display // Ensure the required attribute is present when adding callbackSelect.setAttribute('required', 'required'); // Update the nonce field for adding nonceContainer.innerHTML = ''; // Clear existing nonce if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) { nonceContainer.innerHTML = ``; } } // Show modal with animation modal.style.display = 'flex'; requestAnimationFrame(() => { modal.classList.add('active'); }); // Set up close handlers const closeModal = () => { modal.classList.remove('active'); setTimeout(() => { modal.style.display = 'none'; }, 300); // Match the CSS transition time }; // Close button handler const closeBtn = modal.querySelector('.mxchat-modal-close'); if (closeBtn) { closeBtn.onclick = closeModal; } // Cancel button handler const cancelBtn = modal.querySelector('.mxchat-modal-cancel'); if (cancelBtn) { cancelBtn.onclick = closeModal; } // Click outside modal to close modal.onclick = (e) => { if (e.target === modal) { closeModal(); } }; // Escape key to close modal document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && modal.classList.contains('active')) { closeModal(); } }, { once: true }); // Focus the first field labelField.focus(); // Dispatch an event for the action type selector to catch const event = new CustomEvent('mxchatModalOpened', { detail: { isEdit: isEdit, callbackFunction: callbackFunction || (isEdit ? callbackSelect.value : '') } }); document.dispatchEvent(event); return closeModal; // Return close function for external use } // Initialize event listeners document.addEventListener('DOMContentLoaded', () => { // Set up edit button handlers for intents document.querySelectorAll('.mxchat-edit-button').forEach(button => { button.onclick = () => { const intentId = button.dataset.intentId; const phrases = button.dataset.phrases; mxchatOpenEditModal(intentId, phrases); }; }); // Set up edit button handlers for actions (new functionality) document.querySelectorAll('.mxchat-action-card .mxchat-edit-button').forEach(button => { button.onclick = () => { const actionId = button.dataset.actionId; const phrases = button.dataset.phrases; const label = button.dataset.label; const threshold = button.dataset.threshold || 85; const callbackFunction = button.dataset.callbackFunction; // Add this data attribute mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction); }; }); // Set up add new action buttons (new functionality) const addActionBtn = document.getElementById('mxchat-add-action-btn'); if (addActionBtn) { addActionBtn.onclick = () => mxchatOpenActionModal(); } const createFirstAction = document.getElementById('mxchat-create-first-action'); if (createFirstAction) { createFirstAction.onclick = () => mxchatOpenActionModal(); } // Setup category-specific new action buttons (new functionality) document.querySelectorAll('.mxchat-new-action-button').forEach(button => { button.onclick = () => { const category = button.closest('.mxchat-new-action-card').dataset.category; const closeModal = mxchatOpenActionModal(); // Pre-select the appropriate callback based on category if (category) { const callbackSelect = document.getElementById('callback_function'); if (callbackSelect) { setTimeout(() => { // Map categories to default callbacks const categoryToCallback = { 'data_collection': 'mxchat_handle_form_collection', 'integrations': 'mxchat_handle_slack_message', 'custom_actions': 'mxchat_handle_custom_action', 'recommendations': 'mxchat_handle_product_recommendations' // Add more mappings as needed }; if (categoryToCallback[category]) { callbackSelect.value = categoryToCallback[category]; } }, 100); } } }; }); // Handle action toggle switches (new functionality) document.querySelectorAll('.mxchat-action-toggle').forEach(toggle => { toggle.onchange = function() { const actionId = this.dataset.actionId; const isEnabled = this.checked; // Show loading indicator const loadingEl = document.getElementById('mxchat-action-loading'); if (loadingEl) loadingEl.style.display = 'flex'; // Send AJAX request to update status fetch(ajaxurl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'mxchat_toggle_action', intent_id: actionId, enabled: isEnabled ? 1 : 0, nonce: mxchatAdmin.toggle_action_nonce // Use the correct nonce }) }) .then(response => response.json()) .then(data => { if (!data.success) { alert('Failed to update action status: ' + (data.data?.message || 'Unknown error')); this.checked = !isEnabled; // Revert the toggle } }) .catch(error => { //console.error('Error:', error); alert('Server error. Please try again.'); this.checked = !isEnabled; // Revert the toggle }) .finally(() => { if (loadingEl) loadingEl.style.display = 'none'; }); }; }); // Handle threshold sliders in action cards (new functionality) document.querySelectorAll('.mxchat-threshold-slider').forEach(slider => { slider.oninput = function() { const actionId = this.id.replace('intent_threshold_', ''); document.getElementById('threshold_output_' + actionId).textContent = this.value + '%'; }; }); // Handle threshold save buttons in action cards (new functionality) document.querySelectorAll('.mxchat-threshold-save').forEach(button => { button.onclick = function(e) { e.preventDefault(); const form = this.closest('form'); const intentId = form.querySelector('input[name="intent_id"]').value; const threshold = form.querySelector('input[name="intent_threshold"]').value; const nonce = form.querySelector('input[name="_wpnonce"]').value; // Show loading indicator const loadingEl = document.getElementById('mxchat-action-loading'); if (loadingEl) loadingEl.style.display = 'flex'; // Send AJAX request fetch(ajaxurl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'mxchat_update_intent_threshold', intent_id: intentId, intent_threshold: threshold, _wpnonce: nonce }) }) .then(response => response.json()) .then(data => { if (data.success) { // Visual feedback of success const card = this.closest('.mxchat-action-card'); card.style.background = 'rgba(120, 115, 245, 0.1)'; setTimeout(() => { card.style.background = 'white'; }, 300); } else { alert('Failed to update threshold: ' + (data.data?.message || 'Unknown error')); } }) .catch(error => { //console.error('Error:', error); alert('Server error. Please try again.'); }) .finally(() => { if (loadingEl) loadingEl.style.display = 'none'; }); }; }); }); jQuery(document).ready(function($) { // Ensure we have a debounce function (use lodash if available, otherwise use our implementation) const useDebounce = (window._ && window._.debounce) ? window._.debounce : debounce; // --- AJAX Auto-Save --- const $autosaveSections = $('.mxchat-autosave-section'); // Track whether fields have been modified by user const userModifiedFields = new Set(); if ($autosaveSections.length) { // Track user interactions with input fields to determine if changes are user-initiated $autosaveSections.find('input, textarea, select').on('focus keydown paste', function() { const fieldName = $(this).attr('name'); if (fieldName) { userModifiedFields.add(fieldName); } }); // Handle real-time range slider value updates $autosaveSections.find('input[type="range"]').on('input', function() { const value = $(this).val(); $('#threshold_value').text(value); }); // Handle all input changes (including range slider) $autosaveSections.find('input, textarea, select').on('change', function() { const $field = $(this); const name = $field.attr('name'); // Skip saving for API key fields that haven't been interacted with and are empty const isApiKeyField = name && ( name === 'loops_api_key' || name === 'api_key' || name === 'xai_api_key' || name === 'claude_api_key' || name === 'voyage_api_key' || name === 'gemini_api_key' || name === 'deepseek_api_key' || name.indexOf('_api_key') !== -1 ); // Skip processing if: // 1. It's an API key field // 2. The user hasn't interacted with it // 3. The field is empty if (isApiKeyField && !userModifiedFields.has(name) && (!$field.val() || $field.val().trim() === '')) { //console.log('Skipping auto-save for untouched API key field:', name); return; } let value; // Handle different input types if ($field.attr('type') === 'checkbox') { value = $field.is(':checked') ? 'on' : 'off'; } else { value = $field.val(); } // Create feedback container const feedbackContainer = $('
'); const spinner = $('
'); const successIcon = $('
'); // Position feedback container based on input type if ($field.closest('.toggle-switch').length) { $field.closest('td').append(feedbackContainer); } else if ($field.closest('.mxchat-toggle-switch').length) { $field.closest('.mxchat-toggle-container').append(feedbackContainer); } else if ($field.closest('.slider-container').length) { $field.closest('.slider-container').after(feedbackContainer); } else { $field.after(feedbackContainer); } feedbackContainer.append(spinner); // Determine which AJAX action and nonce to use: var ajaxAction, nonce; // Use the new AJAX action for submenu fields: if (name.indexOf('mxchat_prompts_options') !== -1 || name === 'mxchat_auto_sync_posts' || name === 'mxchat_auto_sync_pages' || name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields ajaxAction = 'mxchat_save_prompts_setting'; nonce = mxchatPromptsAdmin.prompts_setting_nonce; } else { // Otherwise, use the existing AJAX action. ajaxAction = 'mxchat_save_setting'; nonce = mxchatAdmin.setting_nonce; } // AJAX save request $.ajax({ url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url, type: 'POST', data: { action: ajaxAction, name: name, value: value, _ajax_nonce: nonce }, success: function(response) { if (response.success) { spinner.fadeOut(200, function() { feedbackContainer.append(successIcon); successIcon.fadeIn(200).delay(1000).fadeOut(200, function() { feedbackContainer.remove(); }); }); // Check if the response contains a "no changes" message and log it if (response.data && response.data.message === 'No changes detected') { //console.log('No changes detected for field:', name); } } else { // Only show alert for actual errors, not for "no changes" let errorMessage = response.data?.message || 'Unknown error'; // Don't display an alert for "no changes" message if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') { alert('Error saving: ' + errorMessage); } else { // Still provide visual feedback that no changes were needed spinner.fadeOut(200, function() { feedbackContainer.append(successIcon); successIcon.fadeIn(200).delay(1000).fadeOut(200, function() { feedbackContainer.remove(); }); }); //console.log('No changes detected for field:', name); return; } // Only revert checkbox state if it was an actual error if (errorMessage !== 'No changes detected' && errorMessage !== 'Update failed or no changes') { if ($field.attr('type') === 'checkbox') { $field.prop('checked', !$field.is(':checked')); } } // Always clean up the feedback container feedbackContainer.remove(); } }, error: function(xhr, textStatus, error) { //console.error('AJAX Error:', textStatus, error); alert('An error occurred while saving. Please try again.'); // Revert checkbox state on error if ($field.attr('type') === 'checkbox') { $field.prop('checked', !$field.is(':checked')); } feedbackContainer.remove(); } }); }); // Initialize color pickers with debouncing $autosaveSections.find('.my-color-field').each(function() { const $colorField = $(this); $(this).wpColorPicker({ change: useDebounce(function(event, ui) { // Safety check - ensure we have a valid field and value if (!$colorField || !$colorField.val()) { //console.warn('Color picker not ready'); return; } const name = $colorField.attr('name'); const value = $colorField.val(); if (!name || !value) { //console.warn('Missing required color picker values'); return; } // Create feedback container const feedbackContainer = $('
'); const spinner = $('
'); const successIcon = $('
'); // Position feedback container $colorField.closest('.wp-picker-container').after(feedbackContainer); feedbackContainer.append(spinner); // Determine which AJAX action and nonce to use: var ajaxAction, nonce; // Use the new AJAX action for submenu fields: if (name.indexOf('mxchat_prompts_options') !== -1 || name === 'mxchat_auto_sync_posts' || name === 'mxchat_auto_sync_pages' || name.indexOf('mxchat_auto_sync_') === 0) { // Modified to catch all auto-sync fields ajaxAction = 'mxchat_save_prompts_setting'; nonce = mxchatPromptsAdmin.prompts_setting_nonce; } else { // Otherwise, use the existing AJAX action. ajaxAction = 'mxchat_save_setting'; nonce = mxchatAdmin.setting_nonce; } // AJAX save request $.ajax({ url: (ajaxAction === 'mxchat_save_prompts_setting') ? mxchatPromptsAdmin.ajax_url : mxchatAdmin.ajax_url, type: 'POST', data: { action: ajaxAction, name: name, value: value, _ajax_nonce: nonce }, success: function(response) { if (response.success) { spinner.fadeOut(200, function() { feedbackContainer.append(successIcon); successIcon.fadeIn(200).delay(1000).fadeOut(200, function() { feedbackContainer.remove(); }); }); } else { alert('Error saving: ' + (response.data?.message || 'Unknown error')); feedbackContainer.remove(); } }, error: function() { alert('An error occurred while saving.'); feedbackContainer.remove(); } }); }, 500) }); }); // Reinitialize color pickers when switching tabs $('.mxchat-tab-button').on('click.mxchat', function() { setTimeout(function() { $('.my-color-field:visible').wpColorPicker('close'); }, 100); }); } // Initialize tabs system function initTabs() { // Remove any existing handlers first $('.mxchat-tab-button').off('click.mxchat'); // Add new click handlers $('.mxchat-tab-button').on('click.mxchat', function(e) { e.preventDefault(); e.stopPropagation(); var $this = $(this); // Get tab ID from data-tab attribute var tabId = $this.data('tab') || 'chatbot'; // Safety check for empty tabId if (!tabId) { //console.warn('No tab identifier found'); return; } // Update tab buttons $('.mxchat-tab-button').removeClass('active'); $this.addClass('active'); // Update content areas - with safety check $('.mxchat-tab-content').removeClass('active'); var $targetTab = $('#' + tabId); if ($targetTab.length) { $targetTab.addClass('active'); // Removed localStorage saving functionality } else { //console.warn('Tab content #' + tabId + ' not found'); } }); } // Initialize tabs and handle events initTabs(); $(document).on('widget-added widget-updated postbox-toggled', initTabs); // Always activate the first tab (Chatbot) $('.mxchat-tab-button').first().trigger('click.mxchat'); // Attach edit modal event handler $(document).on('click', '.mxchat-edit-button', function() { const intentId = $(this).data('intent-id'); const phrases = $(this).data('phrases'); mxchatOpenEditModal(intentId, phrases); }); // Toggle visibility handlers function toggleVisibility(selector) { $(selector).on('click', function() { var inputField = $(this).prev('input'); if (inputField.attr('type') === 'password') { inputField.attr('type', 'text'); $(this).text('Hide'); } else { inputField.attr('type', 'password'); $(this).text('Show'); } }); } // Initialize all toggle visibility buttons [ '#toggleApiKeyVisibility', '#toggleWooCommerceSecretVisibility', '#toggleVoyageAPIKeyVisibility', '#toggleLoopsApiKeyVisibility', '#toggleXaiApiKeyVisibility', '#toggleClaudeApiKeyVisibility', '#toggleBraveApiKeyVisibility', '#toggleWebhookUrlVisibility', '#toggleSecretKeyVisibility', '#toggleBotTokenVisibility', '#toggleDeepSeekApiKeyVisibility', '#toggleGeminiApiKeyVisibility' // Added Gemini toggle ].forEach(toggleVisibility); // Handle API key visibility based on model selection function setupAPIKeyVisibility() { // Cache the selectors const $chatModelSelect = $('#model'); const $embeddingModelSelect = $('#embedding_model'); // First, locate and mark the API key rows setupAPIKeyRows(); // Initial setup based on current selections updateApiKeyVisibility(); // Listen for changes to the model selectors $chatModelSelect.on('change', updateApiKeyVisibility); $embeddingModelSelect.on('change', updateApiKeyVisibility); /** * Locate and mark rows that contain API key fields */ function setupAPIKeyRows() { // Find key rows by their field IDs const providerMap = { 'api_key': 'openai', 'xai_api_key': 'xai', 'claude_api_key': 'claude', 'deepseek_api_key': 'deepseek', 'voyage_api_key': 'voyage', 'gemini_api_key': 'gemini' // Added Gemini API key mapping }; $.each(providerMap, function(fieldId, provider) { const $field = $('#' + fieldId); if ($field.length) { const $row = $field.closest('tr'); $row.addClass('mxchat-setting-row'); $row.attr('data-provider', provider); } }); } /** * Updates the visibility of API key fields based on current model selections */ function updateApiKeyVisibility() { const chatModel = $chatModelSelect.val(); const embeddingModel = $embeddingModelSelect.val(); // Determine which providers are needed const isOpenAIChat = chatModel && chatModel.startsWith('gpt-'); const isXAI = chatModel && chatModel.startsWith('grok-'); const isClaude = chatModel && chatModel.startsWith('claude-'); const isDeepSeek = chatModel && chatModel.startsWith('deepseek-'); const isGemini = chatModel && chatModel.startsWith('gemini-'); // Added Gemini detection const isOpenAIEmbedding = embeddingModel && embeddingModel.startsWith('text-embedding-'); const isVoyage = embeddingModel && embeddingModel.startsWith('voyage-'); // Update API key visibility for each provider updateWrapperVisibility('openai', isOpenAIChat || isOpenAIEmbedding); updateWrapperVisibility('xai', isXAI); updateWrapperVisibility('claude', isClaude); updateWrapperVisibility('deepseek', isDeepSeek); updateWrapperVisibility('voyage', isVoyage); updateWrapperVisibility('gemini', isGemini); // Added Gemini visibility update // Update provider-specific notices for OpenAI if (isOpenAIChat && isOpenAIEmbedding) { $('div[data-provider="openai"] .api-key-notice').text( 'Required for your selected chat model and embedding model. Important: You must add credits before use.' ); } else if (isOpenAIChat) { $('div[data-provider="openai"] .api-key-notice').text( 'Required for your selected chat model. Important: You must add credits before use.' ); } else if (isOpenAIEmbedding) { $('div[data-provider="openai"] .api-key-notice').text( 'Required for your selected embedding model. Important: You must add credits before use.' ); } } /** * Updates visibility of a specific provider's API key wrapper */ function updateWrapperVisibility(provider, isVisible) { const $row = $('tr.mxchat-setting-row[data-provider="' + provider + '"]'); if (!$row.length) { //console.warn('API key row not found for provider: ' + provider); return; } if (isVisible) { $row.show(); if (!$row.hasClass('highlighted')) { $row.addClass('highlighted'); setTimeout(() => { $row.removeClass('highlighted'); }, 1500); } } else { $row.hide(); } } } // Add this to your JavaScript file function setupMxChatModelSelector() { const $modelSelect = $('#model'); const $modelSelectorButton = $('
`; $('body').append(modelSelectorModal); // Populate models grid function populateModelsGrid(filter = '', category = 'all') { const $grid = $('#mxchat_models_grid'); $grid.empty(); const models = { gemini: [ { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Next-Gen features, speed & multimodal generation' }, { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Cost-efficient with low latency' }, { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Complex reasoning tasks requiring more intelligence' }, { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Fast and versatile performance' }, ], openai: [ { value: 'gpt-4.1-2025-04-14', label: 'GPT-4.1', description: 'Flagship model for complex tasks' }, { value: 'gpt-4o', label: 'GPT-4o', description: 'Recommended for most use cases' }, { value: 'gpt-4o-mini', label: 'GPT-4o Mini', description: 'Fast and lightweight' }, { value: 'gpt-4-turbo', label: 'GPT-4 Turbo', description: 'High-performance model' }, { value: 'gpt-4', label: 'GPT-4', description: 'High intelligence model' }, { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', description: 'Affordable and fast' }, ], claude: [ { value: 'claude-3-7-sonnet-20250219', label: 'Claude 3.7 Sonnet', description: 'Most intelligent Claude model' }, { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet', description: 'Intelligent and balanced' }, { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus', description: 'Highly complex tasks' }, { value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', description: 'Balanced performance' }, { value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', description: 'Fastest Claude model' }, ], xai: [ { value: 'grok-3-beta', label: 'Grok-3', description: 'Powerful model with 131K context' }, { value: 'grok-3-fast-beta', label: 'Grok-3 Fast', description: 'High performance with faster responses' }, { value: 'grok-3-mini-beta', label: 'Grok-3 Mini', description: 'Affordable model with good performance' }, { value: 'grok-3-mini-fast-beta', label: 'Grok-3 Mini Fast', description: 'Quick and cost-effective' }, { value: 'grok-2', label: 'Grok 2', description: 'Latest X.AI model' }, ], deepseek: [ { value: 'deepseek-chat', label: 'DeepSeek-V3', description: 'Advanced AI assistant' }, ], }; let allModels = []; Object.keys(models).forEach(key => { if (category === 'all' || category === key) { allModels = allModels.concat(models[key]); } }); // Filter by search term if present if (filter) { const lowerFilter = filter.toLowerCase(); allModels = allModels.filter(model => model.label.toLowerCase().includes(lowerFilter) || model.description.toLowerCase().includes(lowerFilter) ); } // Create model cards allModels.forEach(model => { const isSelected = $modelSelect.val() === model.value; const $modelCard = $(`
${getModelIcon(model.value)}

${model.label}

${model.description}

${isSelected ? '
' : ''}
`); $grid.append($modelCard); }); } // Helper function to get icon for each model function getModelIcon(modelValue) { if (modelValue.startsWith('gemini-')) return ''; if (modelValue.startsWith('gpt-')) return ''; if (modelValue.startsWith('claude-')) return ''; if (modelValue.startsWith('grok-')) return ''; if (modelValue.startsWith('deepseek-')) return ''; return ''; } // Event handlers $modelSelectorButton.on('click', function() { $('#mxchat_model_selector_modal').show(); populateModelsGrid('', 'all'); }); $('.mxchat-model-selector-modal-close, #mxchat_cancel_model_selection').on('click', function() { $('#mxchat_model_selector_modal').hide(); }); $('.mxchat-model-category-btn').on('click', function() { $('.mxchat-model-category-btn').removeClass('active'); $(this).addClass('active'); const category = $(this).data('category'); const searchTerm = $('#mxchat_model_search_input').val(); populateModelsGrid(searchTerm, category); }); $('#mxchat_model_search_input').on('input', function() { const searchTerm = $(this).val(); const activeCategory = $('.mxchat-model-category-btn.active').data('category'); populateModelsGrid(searchTerm, activeCategory); }); $(document).on('click', '.mxchat-model-selector-card', function() { const modelValue = $(this).data('value'); $modelSelect.val(modelValue).trigger('change'); updateButtonText(); $('#mxchat_model_selector_modal').hide(); }); // Close modal when clicking outside $(window).on('click', function(event) { if ($(event.target).is('#mxchat_model_selector_modal')) { $('#mxchat_model_selector_modal').hide(); } }); } // Embedding model selector - completely separate from chat model selector function setupMxChatEmbeddingModelSelector() { const $embeddingModelSelect = $('#embedding_model'); // Skip if the element doesn't exist on the page if ($embeddingModelSelect.length === 0) { return; } const $embeddingModelSelectorButton = $('
`; // Use jQuery's append to ensure it doesn't clash with existing modals $('body').append(embeddingModelSelectorModal); // Populate models grid function populateEmbeddingModelsGrid(filter = '', category = 'all') { const $grid = $('#mxchat_embedding_models_grid'); $grid.empty(); // Define embedding models with descriptions and context lengths const models = { openai: [ { value: 'text-embedding-3-small', label: 'TE3 Small', description: 'Fast and cost-effective embeddings (1536 dimensions, 8K context)' }, { value: 'text-embedding-ada-002', label: 'Ada 2', description: 'Balanced performance embeddings (1536 dimensions, 8K context)' }, { value: 'text-embedding-3-large', label: 'TE3 Large', description: 'High-performance embeddings (3072 dimensions, 8K context)' } ], voyage: [ { value: 'voyage-3-large', label: 'Voyage-3 Large', description: 'Advanced semantic search embeddings (2048 dimensions, 32K context)' } ] }; let allModels = []; Object.keys(models).forEach(key => { if (category === 'all' || category === key) { allModels = allModels.concat(models[key]); } }); // Filter by search term if present if (filter) { const lowerFilter = filter.toLowerCase(); allModels = allModels.filter(model => model.label.toLowerCase().includes(lowerFilter) || model.description.toLowerCase().includes(lowerFilter) ); } // Create model cards allModels.forEach(model => { const isSelected = $embeddingModelSelect.val() === model.value; const providerClass = model.value.startsWith('voyage-') ? 'mxchat-embedding-model-provider-voyage' : 'mxchat-embedding-model-provider-openai'; const $modelCard = $(`
${model.value.startsWith('voyage-') ? '' : '' }

${model.label}

${model.description}

${isSelected ? '
' : ''}
`); $grid.append($modelCard); }); } // Event handlers - use namespaced events to avoid conflicts $embeddingModelSelectorButton.on('click.embeddingModelSelector', function(e) { e.stopPropagation(); // Prevent event bubbling $('#' + embeddingModalId).show(); populateEmbeddingModelsGrid('', 'all'); }); $('.mxchat-embedding-model-selector-modal-close, #mxchat_cancel_embedding_model_selection').on('click.embeddingModelSelector', function(e) { e.stopPropagation(); // Prevent event bubbling $('#' + embeddingModalId).hide(); }); $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').on('click.embeddingModelSelector', function(e) { e.stopPropagation(); // Prevent event bubbling $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn').removeClass('active'); $(this).addClass('active'); const category = $(this).data('category'); const searchTerm = $('#mxchat_embedding_model_search_input').val(); populateEmbeddingModelsGrid(searchTerm, category); }); $('#mxchat_embedding_model_search_input').on('input.embeddingModelSelector', function() { const searchTerm = $(this).val(); const activeCategory = $('.mxchat-embedding-model-selector-categories .mxchat-embedding-model-category-btn.active').data('category'); populateEmbeddingModelsGrid(searchTerm, activeCategory); }); // Use a direct selector to avoid conflicts with other card elements $(document).on('click.embeddingModelSelector', '.mxchat-embedding-model-selector-grid .mxchat-embedding-model-selector-card', function(e) { e.stopPropagation(); // Prevent event bubbling const modelValue = $(this).data('value'); // Important: Only update this specific select element $embeddingModelSelect.val(modelValue); // Manually trigger change only on this element const changeEvent = new Event('change', { bubbles: true }); $embeddingModelSelect[0].dispatchEvent(changeEvent); // Update button text updateButtonText(); // Hide modal $('#' + embeddingModalId).hide(); }); // Close modal when clicking outside - use namespaced events $(window).on('click.embeddingModelSelector', function(event) { if ($(event.target).is('#' + embeddingModalId)) { $('#' + embeddingModalId).hide(); } }); } // Call this function after the DOM is fully loaded $(document).ready(function() { setupMxChatModelSelector(); setupMxChatEmbeddingModelSelector(); }); // Initialize API key visibility setupAPIKeyVisibility(); // Add Intent Form Submission $('#mxchat-add-intent-form').on('submit', function(event) { $('#mxchat-intent-loading').show(); $('#mxchat-intent-loading-text').show(); $(this).find('button[type="submit"]').hide(); }); // Inline Edit Functionality $('.edit-button').on('click', function() { var row = $(this).closest('tr'); row.find('.content-view, .url-view').hide(); row.find('.content-edit, .url-edit').show(); row.find('.edit-button').hide(); row.find('.save-button').show(); }); // Save button handler $('.save-button').on('click', function() { var button = $(this); var row = button.closest('tr'); var id = button.data('id'); var newContent = row.find('.content-edit').val(); var newUrl = row.find('.url-edit').val(); button.prop('disabled', true); button.text('Saving...'); $.ajax({ url: mxchatAdmin.ajax_url, type: 'POST', data: { action: 'mxchat_save_inline_prompt', id: id, article_content: newContent, article_url: newUrl, _ajax_nonce: mxchatAdmin.inline_edit_nonce }, success: function(response) { button.prop('disabled', false); button.text('Save'); if (response.success) { row.find('.content-view').html(newContent.replace(/\n/g, "
")); if (newUrl) { row.find('.url-view').html('' + newUrl + ''); } else { row.find('.url-view').html('N/A'); } row.find('.content-edit, .url-edit').hide(); row.find('.content-view, .url-view').show(); row.find('.save-button').hide(); row.find('.edit-button').show(); } else { alert('Error saving content: ' + (response.data?.message || 'Unknown error')); } }, error: function() { button.prop('disabled', false); button.text('Save'); alert('An error occurred while saving.'); } }); }); // Questions handling $('.mxchat-add-question').on('click', function () { const container = $('#mxchat-additional-questions-container'); const questionCount = container.find('.mxchat-question-row').length + 4; const questionIndex = container.find('.mxchat-question-row').length; const newQuestion = `
`; container.append(newQuestion); }); $(document).on('click', '.mxchat-remove-question', function () { $(this).closest('.mxchat-question-row').remove(); saveQuestions(); }); $(document).on('change', '.mxchat-question-input', function() { saveQuestions(); }); function saveQuestions() { const questions = []; $('.mxchat-question-input').each(function() { const value = $(this).val().trim(); if (value) { questions.push(value); } }); const feedbackContainer = $('
'); const spinner = $('
'); const successIcon = $('
'); // Append feedback after the add button $('.mxchat-add-question').after(feedbackContainer); feedbackContainer.append(spinner); // Save via AJAX $.ajax({ url: mxchatAdmin.ajax_url, type: 'POST', data: { action: 'mxchat_save_setting', name: 'additional_popular_questions', value: JSON.stringify(questions), _ajax_nonce: mxchatAdmin.setting_nonce }, success: function(response) { if (response.success) { spinner.fadeOut(200, function() { feedbackContainer.append(successIcon); successIcon.fadeIn(200).delay(1000).fadeOut(200, function() { feedbackContainer.remove(); }); }); } else { alert('Error saving questions: ' + (response.data?.message || 'Unknown error')); feedbackContainer.remove(); } }, error: function() { alert('An error occurred while saving questions.'); feedbackContainer.remove(); } }); } // Live agent status handler const statusToggle = document.getElementById('live_agent_status'); const statusText = statusToggle?.parentElement.nextElementSibling?.querySelector('.status-text'); if (statusToggle && statusText) { statusToggle.addEventListener('change', function() { // Update display text statusText.textContent = this.checked ? 'Online' : 'Offline'; // Send the correct on/off value to the server if (window.mxchatSaveSetting) { window.mxchatSaveSetting('live_agent_status', this.checked ? 'on' : 'off'); } }); } // Function to adjust the textarea height to content function adjustTextareaHeight() { this.style.height = 'auto'; // Reset to auto to calculate scrollHeight this.style.height = this.scrollHeight + 'px'; // Expand to content height } // Function to reset the textarea height to initial function resetTextareaHeight() { this.style.height = ''; // Remove inline height, reverting to CSS default } // Target the specific textarea by ID var $textarea = $('#system_prompt_instructions'); // Bind events $textarea.on('focus input', adjustTextareaHeight) // Expand on focus and input .on('blur', resetTextareaHeight); // Reset on blur }); document.addEventListener('DOMContentLoaded', function() { // Check if we're on the correct page before initializing const modal = document.getElementById('mxchat-action-modal'); // Only initialize if the modal exists on this page if (modal) { //console.log('MXChat Action Modal JS Loaded'); // Initialize the action modal functionality initStepBasedActionModal(); } // Function to initialize the step-based action modal function initStepBasedActionModal() { // We already checked for modal existence above, so no need to check again const actionStep1 = document.getElementById('mxchat-action-step-1'); const actionStep2 = document.getElementById('mxchat-action-step-2'); const backToStep1Btn = document.getElementById('mxchat-back-to-step-1'); const searchInput = document.getElementById('action-type-search'); const categoryButtons = modal.querySelectorAll('.mxchat-category-button'); const actionCards = modal.querySelectorAll('.mxchat-action-type-card'); const actionForm = document.getElementById('mxchat-action-form'); const callbackInput = document.getElementById('callback_function'); const actionIdField = document.getElementById('edit_action_id'); const labelField = document.getElementById('intent_label'); const phrasesField = document.getElementById('action_phrases'); const formActionType = document.getElementById('form_action_type'); const nonceContainer = document.getElementById('action-nonce-container'); const thresholdSlider = document.getElementById('similarity_threshold'); const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display'); // Rest of your initialization code remains the same... // Log the structure of one action card for debugging if (actionCards.length > 0) { //console.log('First action card data attributes:', actionCards[0].dataset); //console.log('First action card HTML:', actionCards[0].outerHTML); } // Add click event listeners to category buttons categoryButtons.forEach(button => { button.addEventListener('click', function() { //console.log('Category button clicked:', this.dataset.category); // Remove active class from all buttons categoryButtons.forEach(btn => btn.classList.remove('active')); // Add active class to clicked button this.classList.add('active'); // Get selected category const category = this.dataset.category; // Filter action cards filterActionCards(category, searchInput.value); }); }); // Add search functionality if (searchInput) { searchInput.addEventListener('input', function() { // Get active category const activeCategory = modal.querySelector('.mxchat-category-button.active')?.dataset.category || 'all'; //console.log('Search input changed, active category:', activeCategory); // Filter action cards filterActionCards(activeCategory, this.value); }); } // Add click event listeners to action cards actionCards.forEach(card => { card.addEventListener('click', function() { // Get the action data const isPro = this.dataset.pro === 'true'; const isInstalled = this.dataset.installed === 'true'; const addonName = this.dataset.addon || ''; const actionValue = this.dataset.value; const actionLabel = this.dataset.label; const actionIcon = this.querySelector('.dashicons').getAttribute('class').replace('dashicons dashicons-', ''); const actionDescription = this.querySelector('p').textContent; // Pro check using the proper detection method const proIsActivated = typeof mxchatAdmin !== 'undefined' && (mxchatAdmin.is_activated === '1' || mxchatAdmin.is_activated === 'true' || mxchatAdmin.is_activated === true); // Handle different states if (isPro && !proIsActivated) { // Pro feature but no Pro license showProFeatureNotice(); return; } if (addonName && !isInstalled) { // Add-on required but not installed const addonDisplayName = this.querySelector('.mxchat-addon-info')?.textContent?.replace('Requires ', '') || addonName + ' Add-on'; showAddonRequiredNotice(addonDisplayName); return; } // If we get here, the action is available - proceed as normal callbackInput.value = actionValue; // Update the selected action display in step 2 document.getElementById('selected-action-title').textContent = actionLabel; document.getElementById('selected-action-description').textContent = actionDescription; document.getElementById('selected-action-icon').innerHTML = ``; // Set a default label based on the action type (user can change it) if (!labelField.value) { labelField.value = actionLabel; } // Move to step 2 actionStep1.classList.remove('active'); actionStep2.classList.add('active'); // Update modal title }); }); // Back button functionality if (backToStep1Btn) { backToStep1Btn.addEventListener('click', function() { //console.log('Back button clicked'); actionStep2.classList.remove('active'); actionStep1.classList.add('active'); }); } // Function to filter action cards by category and search term function filterActionCards(category, searchTerm) { searchTerm = searchTerm.toLowerCase().trim(); //console.log(`Filtering cards by category: "${category}", search: "${searchTerm}"`); let visibleCount = 0; // Show all cards initially with animation actionCards.forEach((card, index) => { // Reset animation card.style.animation = 'none'; // Trigger reflow void card.offsetWidth; // Determine if card should be visible based on category and search term const cardCategory = card.dataset.category || ''; const matchesCategory = category === 'all' || cardCategory === category; const cardTitle = card.querySelector('h4')?.textContent?.toLowerCase() || ''; const cardDesc = card.querySelector('p')?.textContent?.toLowerCase() || ''; const matchesSearch = searchTerm === '' || cardTitle.includes(searchTerm) || cardDesc.includes(searchTerm); // Show/hide card with animation if (matchesCategory && matchesSearch) { card.style.display = 'flex'; // Staggered animation for cards card.style.animation = `fadeIn 0.2s ease forwards ${index * 0.03}s`; visibleCount++; } else { card.style.display = 'none'; } }); //console.log(`Filter results: ${visibleCount} cards visible out of ${actionCards.length}`); } // Function to show notice for Pro features function showProFeatureNotice() { //console.log('Showing Pro feature notice'); // Check if we already have a notification container let noticeContainer = document.querySelector('.mxchat-pro-notice'); if (!noticeContainer) { // Create the notice container noticeContainer = document.createElement('div'); noticeContainer.className = 'mxchat-pro-notice'; // Create content noticeContainer.innerHTML = `

MxChat Pro Feature

This action is available in the Pro version only.

Upgrade to Pro
`; // Append to body document.body.appendChild(noticeContainer); // Add close functionality const closeButton = noticeContainer.querySelector('.mxchat-pro-notice-close'); closeButton.addEventListener('click', function() { noticeContainer.classList.remove('active'); setTimeout(() => { noticeContainer.remove(); }, 300); }); // Click outside to close noticeContainer.addEventListener('click', function(e) { if (e.target === noticeContainer) { closeButton.click(); } }); // Show with animation setTimeout(() => { noticeContainer.classList.add('active'); }, 10); } else { // If it already exists, just make it visible again noticeContainer.classList.add('active'); } } // Function to show notice for add-on requirements function showAddonRequiredNotice(addonName) { //console.log(`Showing add-on notice for: ${addonName}`); // Check if we already have a notification container let noticeContainer = document.querySelector('.mxchat-addon-notice'); if (!noticeContainer) { // Create the notice container noticeContainer = document.createElement('div'); noticeContainer.className = 'mxchat-addon-notice'; // Create content noticeContainer.innerHTML = `
🧩

Add-on Required

This action requires the ${addonName} add-on to be installed.

Get Add-ons
`; // Append to body document.body.appendChild(noticeContainer); // Add close functionality const closeButton = noticeContainer.querySelector('.mxchat-addon-notice-close'); closeButton.addEventListener('click', function() { noticeContainer.classList.remove('active'); setTimeout(() => { noticeContainer.remove(); }, 300); }); // Click outside to close noticeContainer.addEventListener('click', function(e) { if (e.target === noticeContainer) { closeButton.click(); } }); // Show with animation setTimeout(() => { noticeContainer.classList.add('active'); }, 10); } else { // If it already exists, update the content const addonNameElement = noticeContainer.querySelector('p strong'); if (addonNameElement) { addonNameElement.textContent = addonName; } // Make it visible again noticeContainer.classList.add('active'); } } // Form submission handling if (actionForm) { actionForm.addEventListener('submit', function() { //console.log('Form submitted'); document.getElementById('mxchat-action-loading').style.display = 'flex'; this.querySelector('button[type="submit"]').disabled = true; }); } } // Setup add action buttons (only if we're on the correct page) if (modal) { // Update the modal open function to support the step-based flow window.mxchatOpenActionModal = function(isEdit = false, actionId = '', label = '', phrases = '', threshold = 85, callbackFunction = '') { //console.log('Modal opening, edit mode:', isEdit); // No need to check again, we already verified modal exists // Get form fields const actionIdField = document.getElementById('edit_action_id'); const labelField = document.getElementById('intent_label'); const phrasesField = document.getElementById('action_phrases'); const formActionType = document.getElementById('form_action_type'); const callbackInput = document.getElementById('callback_function'); const saveButton = document.getElementById('mxchat-save-action-btn'); const nonceContainer = document.getElementById('action-nonce-container'); const thresholdSlider = document.getElementById('similarity_threshold'); const thresholdDisplay = document.querySelector('.mxchat-threshold-value-display'); const actionStep1 = document.getElementById('mxchat-action-step-1'); const actionStep2 = document.getElementById('mxchat-action-step-2'); const searchInput = document.getElementById('action-type-search'); // Set up modal for edit or create if (isEdit) { saveButton.textContent = 'Update Action'; formActionType.value = 'mxchat_edit_intent'; actionIdField.value = actionId; labelField.value = label; phrasesField.value = phrases; callbackInput.value = callbackFunction; thresholdSlider.value = threshold; // Set the current threshold value thresholdDisplay.textContent = threshold + '%'; // Update display // Update the nonce field for editing nonceContainer.innerHTML = ''; // Clear existing nonce if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.edit_intent_nonce) { nonceContainer.innerHTML = ``; } // For editing, go directly to step 2 and update the selected action display actionStep1.classList.remove('active'); actionStep2.classList.add('active'); // Find the matching action card to get its details const actionCards = document.querySelectorAll('.mxchat-action-type-card'); let foundCard = null; actionCards.forEach(card => { if (card.dataset.value === callbackFunction) { foundCard = card; } }); if (foundCard) { //console.log('Found matching action card for:', callbackFunction); const actionLabel = foundCard.dataset.label || foundCard.querySelector('h4')?.textContent || ''; const actionIconElement = foundCard.querySelector('.dashicons'); const actionIcon = actionIconElement ? actionIconElement.getAttribute('class').replace('dashicons dashicons-', '') : 'admin-generic'; const actionDescription = foundCard.querySelector('p')?.textContent || ''; document.getElementById('selected-action-title').textContent = actionLabel; document.getElementById('selected-action-description').textContent = actionDescription; document.getElementById('selected-action-icon').innerHTML = ``; } else { //console.log('No matching action card found for:', callbackFunction); // Fallback if we can't find the card document.getElementById('selected-action-title').textContent = label; document.getElementById('selected-action-description').textContent = 'Configure this action for your chatbot'; document.getElementById('selected-action-icon').innerHTML = ``; } } else { //console.log('Setting up create mode'); saveButton.textContent = 'Save Action'; formActionType.value = 'mxchat_add_intent'; actionIdField.value = ''; labelField.value = ''; phrasesField.value = ''; callbackInput.value = ''; thresholdSlider.value = 85; // Default value for new actions thresholdDisplay.textContent = '85%'; // Default display // Update the nonce field for adding nonceContainer.innerHTML = ''; // Clear existing nonce if (typeof mxchatAdmin !== 'undefined' && mxchatAdmin.add_intent_nonce) { nonceContainer.innerHTML = ``; } // For creating new, start at step 1 actionStep1.classList.add('active'); actionStep2.classList.remove('active'); } // Show modal with animation modal.style.display = 'flex'; requestAnimationFrame(() => { modal.classList.add('active'); }); // Set up close handlers const closeModal = () => { //console.log('Closing modal'); modal.classList.remove('active'); setTimeout(() => { modal.style.display = 'none'; }, 300); // Match the CSS transition time }; // Close button handler const closeBtn = modal.querySelector('.mxchat-modal-close'); if (closeBtn) { closeBtn.onclick = closeModal; } // Cancel button handler const cancelBtns = modal.querySelectorAll('.mxchat-modal-cancel'); if (cancelBtns) { cancelBtns.forEach(btn => { btn.onclick = closeModal; }); } // Click outside modal to close modal.onclick = (e) => { if (e.target === modal) { closeModal(); } }; // Escape key to close modal document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && modal.classList.contains('active')) { closeModal(); } }, { once: true }); // Focus appropriate field based on current step if (isEdit || actionStep2.classList.contains('active')) { if (labelField) labelField.focus(); } else { if (searchInput) searchInput.focus(); } return closeModal; // Return close function for external use }; // Setup add action buttons const addActionBtn = document.getElementById('mxchat-add-action-btn'); if (addActionBtn) { //console.log('Add action button found'); addActionBtn.onclick = () => window.mxchatOpenActionModal(); } const createFirstAction = document.getElementById('mxchat-create-first-action'); if (createFirstAction) { //console.log('Create first action button found'); createFirstAction.onclick = () => window.mxchatOpenActionModal(); } // Setup edit buttons const editButtons = document.querySelectorAll('.mxchat-action-card .mxchat-edit-button'); //console.log('Edit buttons found:', editButtons.length); editButtons.forEach(button => { button.onclick = () => { const actionId = button.dataset.actionId; const phrases = button.dataset.phrases; const label = button.dataset.label; const threshold = button.dataset.threshold || 85; const callbackFunction = button.dataset.callbackFunction; window.mxchatOpenActionModal(true, actionId, label, phrases, threshold, callbackFunction); }; }); } }); jQuery(document).ready(function($) { // Toggle custom post types container $('#mxchat-custom-post-types-toggle').on('click', function(e) { e.preventDefault(); $('#mxchat-custom-post-types-container').slideToggle(300); // Rotate the toggle icon const $icon = $(this).find('.mxchat-accordion-icon'); if ($('#mxchat-custom-post-types-container').is(':visible')) { $icon.css('transform', 'rotate(180deg)'); $(this).closest('.mxchat-settings-accordion').addClass('active'); } else { $icon.css('transform', 'rotate(0deg)'); $(this).closest('.mxchat-settings-accordion').removeClass('active'); } }); // If there are any selections made, auto-expand the container function autoExpandIfNeeded() { // Check if any checkbox in the container is checked const hasCheckedItems = $('#mxchat-custom-post-types-container input[type="checkbox"]:checked').length > 0; if (hasCheckedItems) { $('#mxchat-custom-post-types-container').show(); $('#mxchat-custom-post-types-toggle .mxchat-accordion-icon').css('transform', 'rotate(180deg)'); $('.mxchat-settings-accordion').addClass('active'); } } // Run on page load autoExpandIfNeeded(); }); jQuery(document).ready(function($) { // Track if a form has been submitted to trigger updates let formSubmitted = false; // Global interval ID to manage the polling let updateIntervalId = null; // Check if we're on the right admin page with status cards or import forms if ($('.mxchat-status-card').length > 0 || $('.mxchat-import-options').length > 0) { //console.log('MxChat: Status update script initialized'); // Initialize AJAX status updates initStatusUpdates(); } // Initialize status updates function initStatusUpdates() { // Get the refresh interval (default to 3 seconds if not set) const refreshInterval = parseInt(mxchatAdmin.status_refresh_interval || 3000); // Check if there are active status cards const hasActiveStatus = $('.mxchat-status-card').length > 0; // Set up form submission listeners $('#mxchat-url-form, #mxchat-content-form').on('submit', function() { //console.log('MxChat: Form submitted, will start checking for updates'); formSubmitted = true; // Store submission info in sessionStorage to persist through redirects sessionStorage.setItem('mxchat_form_submitted', 'true'); sessionStorage.setItem('mxchat_form_submitted_time', Date.now()); // Start checking for status updates right away startPolling(refreshInterval); // Create a temporary message if ($('.mxchat-processing-message').length === 0) { const message = $('
Processing request... Status will update automatically.
'); $('.mxchat-import-section').after(message); // Fade out after 5 seconds setTimeout(function() { message.fadeOut(500, function() { $(this).remove(); }); }, 5000); } }); // Listen for import option clicks $('.mxchat-import-box').on('click', function() { const option = $(this).data('option'); //console.log('MxChat: Import option clicked - ' + option); }); // Check if we recently submitted a form (within last 30 seconds) if (sessionStorage.getItem('mxchat_form_submitted') === 'true') { const submittedTime = parseInt(sessionStorage.getItem('mxchat_form_submitted_time') || '0'); if (Date.now() - submittedTime < 30000) { // 30 seconds //console.log('MxChat: Detected recent form submission via sessionStorage'); formSubmitted = true; } else { // Clear old submission data sessionStorage.removeItem('mxchat_form_submitted'); sessionStorage.removeItem('mxchat_form_submitted_time'); } } // Attach event listener to stop button to clear the interval $('.mxchat-stop-form').on('submit', function() { //console.log('MxChat: Stop processing requested, clearing update interval'); stopPolling(); sessionStorage.removeItem('mxchat_form_submitted'); sessionStorage.removeItem('mxchat_form_submitted_time'); }); // Start the interval for automatic updates if we have status cards or a form was submitted if (hasActiveStatus || formSubmitted) { //console.log('MxChat: Starting automatic status checks'); startPolling(refreshInterval); } } // Function to start polling function startPolling(interval) { // Clear any existing interval first stopPolling(); // Do an initial fetch immediately fetchStatusUpdates(); // Set up new interval updateIntervalId = setInterval(function() { fetchStatusUpdates(); }, interval); //console.log('MxChat: Polling started with interval', interval); } // Function to stop polling function stopPolling() { if (updateIntervalId !== null) { clearInterval(updateIntervalId); updateIntervalId = null; //console.log('MxChat: Polling stopped'); } } // Fetch status updates from the server function fetchStatusUpdates() { // If user is actively viewing the failed URLs, don't refresh as frequently const $details = $('.mxchat-failed-urls-container details'); const isUserViewing = $details.length > 0 && $details.prop('open'); // If details are open, we'll refresh at a slower rate if (isUserViewing) { // Alternative: Update less frequently when details are open setTimeout(function() { performStatusUpdate(); }, 5000); // Slow down updates to every 5 seconds when details are open } else { performStatusUpdate(); } } // Perform the actual AJAX request function performStatusUpdate() { //console.log('MxChat: Checking for status updates...'); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mxchat_get_status_updates', nonce: mxchatAdmin.status_nonce }, success: function(response) { //console.log('MxChat: Status update received'); // If we have active processing or the form was submitted if ((response && response.is_processing) || formSubmitted) { updateStatusUI(response); // If we get a complete status, reload the page if (response.sitemap_status && response.sitemap_status.status === 'complete') { //console.log('MxChat: Sitemap processing complete, reloading page'); // Clear session storage before reload sessionStorage.removeItem('mxchat_form_submitted'); sessionStorage.removeItem('mxchat_form_submitted_time'); setTimeout(function() { location.reload(); }, 1000); return; } if (response.pdf_status && response.pdf_status.status === 'complete') { //console.log('MxChat: PDF processing complete, reloading page'); // Clear session storage before reload sessionStorage.removeItem('mxchat_form_submitted'); sessionStorage.removeItem('mxchat_form_submitted_time'); setTimeout(function() { location.reload(); }, 1000); return; } // Reset form submitted flag if no active processing if (!response.is_processing) { formSubmitted = false; // Clear session storage when no longer processing sessionStorage.removeItem('mxchat_form_submitted'); sessionStorage.removeItem('mxchat_form_submitted_time'); // Also stop polling when processing is complete stopPolling(); } } // Show single URL status if available and no active processing if (response.single_url_status && !response.is_processing) { updateSingleUrlStatus(response.single_url_status); } }, error: function(xhr, status, error) { console.error('MxChat: Status update failed:', error); } }); } // Update the UI with status information function updateStatusUI(data) { // Update PDF status if available if (data.pdf_status) { updatePdfStatus(data.pdf_status); } // Update sitemap status if available if (data.sitemap_status) { updateSitemapStatus(data.sitemap_status); } // Handle single URL status if available and no active processing if (data.single_url_status && !data.is_processing) { updateSingleUrlStatus(data.single_url_status); } else if (data.is_processing) { // Hide single URL status while processing $('#mxchat-single-url-status-container').hide(); } } // Update PDF status card function updatePdfStatus(status) { // Check if PDF card exists let $pdfCard = $('.mxchat-status-card:contains("PDF Processing")'); // If no card exists but we have status, create it if ($pdfCard.length === 0 && status) { //console.log('MxChat: Creating new PDF status card'); createPdfStatusCard(status); $pdfCard = $('.mxchat-status-card:contains("PDF Processing")'); } // If card exists, update it if ($pdfCard.length > 0) { // Update progress bar $pdfCard.find('.mxchat-progress-fill').css('width', status.percentage + '%'); // Update progress text $pdfCard.find('.mxchat-status-details p:first').text( 'Progress: ' + status.processed_pages + ' of ' + status.total_pages + ' pages (' + status.percentage + '%)' ); // Update status text (if it exists) const $statusText = $pdfCard.find('.mxchat-status-details p:nth-child(2)'); if ($statusText.length > 0) { $statusText.text('Status: ' + status.status.charAt(0).toUpperCase() + status.status.slice(1)); } // Update last update text (if it exists) const $lastUpdateText = $pdfCard.find('.mxchat-status-details p:nth-child(3)'); if ($lastUpdateText.length > 0) { $lastUpdateText.text('Last update: ' + status.last_update); } // If we have an error, show it if (status.status === 'error' && status.error) { let $errorNotice = $pdfCard.find('.mxchat-error-notice'); if ($errorNotice.length === 0) { $errorNotice = $('

'); $pdfCard.find('.mxchat-status-details').append($errorNotice); } $errorNotice.find('p.error').text(status.error); // Make sure error badge is shown if ($pdfCard.find('.mxchat-status-badge.mxchat-status-failed').length === 0) { $pdfCard.find('.mxchat-status-header').append('Error'); } } } } // Create a new PDF status card function createPdfStatusCard(status) { let html = '
'; html += '
'; html += '

PDF Processing Status

'; // Add stop processing form if processing if (status.status === 'processing') { html += '
'; html += ''; html += '
'; } // Add error badge if error if (status.status === 'error') { html += 'Error'; } html += '
'; // End header // Progress bar html += '
'; html += '
'; html += '
'; // Status details html += '
'; html += '

Progress: ' + status.processed_pages + ' of ' + status.total_pages + ' pages (' + status.percentage + '%)

'; html += '

Status: ' + status.status.charAt(0).toUpperCase() + status.status.slice(1) + '

'; html += '

Last update: ' + status.last_update + '

'; // Add error message if any if (status.status === 'error' && status.error) { html += '
'; html += '

' + status.error + '

'; html += '
'; } html += '
'; // End details html += '
'; // End card // Try to find the import tab content to insert the status card into let $importTabContent = $('#mxchat-kb-tab-import'); if ($importTabContent.length > 0) { // For the tabbed interface, add to the import tab let $sitemapCard = $importTabContent.find('.mxchat-status-card:contains("Sitemap Processing")'); if ($sitemapCard.length > 0) { $sitemapCard.before($(html)); } else { $importTabContent.find('.mxchat-import-section').after($(html)); } } else { // Fallback to the old method let $sitemapCard = $('.mxchat-status-card:contains("Sitemap Processing")'); if ($sitemapCard.length > 0) { $sitemapCard.before($(html)); } else { $('.mxchat-import-section').after($(html)); } } } // Update sitemap status card function updateSitemapStatus(status) { // Check if sitemap card exists let $sitemapCard = $('.mxchat-status-card:contains("Sitemap Processing")'); // If no card exists but we have status, create it if ($sitemapCard.length === 0 && status) { //console.log('MxChat: Creating new sitemap status card'); createSitemapStatusCard(status); $sitemapCard = $('.mxchat-status-card:contains("Sitemap Processing")'); } // If card exists, update it if ($sitemapCard.length > 0) { // Update progress bar $sitemapCard.find('.mxchat-progress-fill').css('width', status.percentage + '%'); // Update progress text $sitemapCard.find('.mxchat-status-details p:first').text( 'Progress: ' + status.processed_urls + ' of ' + status.total_urls + ' URLs (' + status.percentage + '%)' ); // Check if details is already open before updating const isDetailsOpen = $sitemapCard.find('.mxchat-failed-urls-container details').prop('open'); // Update errors display let $errorContainer = $sitemapCard.find('.mxchat-error-notice'); if ($errorContainer.length === 0 && (status.error || status.last_error || (status.failed_urls_list && status.failed_urls_list.length > 0))) { // Create error container if it doesn't exist $errorContainer = $('
'); $sitemapCard.find('.mxchat-status-details').append($errorContainer); } // Update or create error notices if ($errorContainer.length > 0) { let errorHTML = ''; if (status.error) { errorHTML += '

' + status.error + '

'; } if (status.last_error) { errorHTML += '

Last error: ' + status.last_error + '

'; } // Add failed URLs list if (status.failed_urls_list && status.failed_urls_list.length > 0) { errorHTML += '
'; errorHTML += '
Failed URLs (' + status.failed_urls_list.length + ')
'; // Set the 'open' attribute based on previous state errorHTML += ''; errorHTML += 'Show Failed URLs'; errorHTML += '
'; // Create table for failed URLs errorHTML += ''; errorHTML += ''; errorHTML += ''; // Sort failed URLs by most recent const sortedFailedUrls = [...status.failed_urls_list].sort((a, b) => b.time - a.time); // Show up to 50 failed URLs const displayUrls = sortedFailedUrls.slice(0, 50); displayUrls.forEach(item => { const timeAgo = formatTimeAgo(item.time); errorHTML += ''; errorHTML += ''; errorHTML += ''; errorHTML += ''; errorHTML += ''; }); errorHTML += '
URLErrorTime
'; errorHTML += ''; errorHTML += truncateUrl(item.url) + '' + item.error + '' + timeAgo + '
'; if (status.failed_urls_list.length > 50) { errorHTML += '
+ ' + (status.failed_urls_list.length - 50) + ' more failed URLs not shown
'; } errorHTML += '
'; // End of failed-urls-list errorHTML += ''; errorHTML += '
'; // End of failed-urls-container } $errorContainer.html(errorHTML); // Additionally, add a click handler to pause refreshes when viewing details $sitemapCard.find('.mxchat-failed-urls-container details').on('toggle', function() { if (this.open) { // User opened the details - set a flag $(this).data('user-opened', true); } else { // User closed the details - remove the flag $(this).data('user-opened', false); } }); } } } // Create a new sitemap status card function createSitemapStatusCard(status) { let html = '
'; html += '
'; html += '

Sitemap Processing Status

'; // Add stop processing form if processing if (status.status === 'processing') { html += '
'; html += ''; html += '
'; } // Add error badge if error if (status.status === 'error') { html += 'Error'; } html += '
'; // End header // Progress bar html += '
'; html += '
'; html += '
'; // Status details html += '
'; html += '

Progress: ' + status.processed_urls + ' of ' + status.total_urls + ' URLs (' + status.percentage + '%)

'; // Add error message if any if ((status.error || status.last_error) && status.status === 'error') { html += '
'; if (status.error) { html += '

' + status.error + '

'; } if (status.last_error) { html += '

Last error: ' + status.last_error + '

'; } html += '
'; } html += '
'; // End details html += '
'; // End card // Try to find the import tab content to insert the status card into let $importTabContent = $('#mxchat-kb-tab-import'); if ($importTabContent.length > 0) { // For the tabbed interface, add to the import tab let $pdfCard = $importTabContent.find('.mxchat-status-card:contains("PDF Processing")'); if ($pdfCard.length > 0) { $pdfCard.after($(html)); } else { $importTabContent.find('.mxchat-import-section').after($(html)); } } else { // Fallback to the old method let $pdfCard = $('.mxchat-status-card:contains("PDF Processing")'); if ($pdfCard.length > 0) { $pdfCard.after($(html)); } else { $('.mxchat-import-section').after($(html)); } } } // Update single URL status function updateSingleUrlStatus(status) { // Check if container exists let $container = $('#mxchat-single-url-status-container'); if ($container.length === 0) { // Create container $container = $('
'); // Try to find the import tab content to insert the status card into let $importTabContent = $('#mxchat-kb-tab-import'); if ($importTabContent.length > 0) { // For the tabbed interface, add to the import tab let $lastStatusCard = $importTabContent.find('.mxchat-status-card').last(); if ($lastStatusCard.length > 0) { $lastStatusCard.after($container); } else { $importTabContent.find('.mxchat-import-section').after($container); } } else { // Fallback to the old method let $lastStatusCard = $('.mxchat-status-card').last(); if ($lastStatusCard.length > 0) { $lastStatusCard.after($container); } else { $('.mxchat-import-section').after($container); } } } // Update container content let html = '
'; html += '
'; html += '

Last URL Submission

'; if (status.status === 'failed') { html += 'Failed'; } else { html += 'Success'; } html += '
'; // End header html += '
'; html += '

URL: '; html += ''; // Truncate URL if needed const displayUrl = status.url.length > 60 ? status.url.substring(0, 57) + '...' : status.url; html += displayUrl; html += '

'; html += '

Submitted: ' + status.human_time + '

'; if (status.status === 'failed' && status.error) { html += '
'; html += '

' + status.error + '

'; html += '
'; } if (status.status === 'complete') { html += '

Content Length: ' + status.content_length + ' characters

'; html += '

Embedding Dimensions: ' + status.embedding_dimensions + '

'; } html += '
'; // End details html += '
'; // End card $container.html(html).show(); } // Helper function to format time ago function formatTimeAgo(timestamp) { const now = Math.floor(Date.now() / 1000); const seconds = now - timestamp; if (seconds < 60) { return seconds + ' seconds ago'; } else if (seconds < 3600) { return Math.floor(seconds / 60) + ' minutes ago'; } else if (seconds < 86400) { return Math.floor(seconds / 3600) + ' hours ago'; } else { return Math.floor(seconds / 86400) + ' days ago'; } } // Helper function to truncate long URLs function truncateUrl(url) { const maxLength = 50; if (url.length <= maxLength) return url; // Remove protocol let displayUrl = url.replace(/^https?:\/\//, ''); if (displayUrl.length <= maxLength) return displayUrl; // Keep the domain and truncate the path const domainMatch = displayUrl.match(/^([^\/]+)\//); if (domainMatch) { const domain = domainMatch[1]; const path = displayUrl.substring(domain.length); if (path.length > 10) { return domain + path.substring(0, maxLength - domain.length - 3) + '...'; } } // Final fallback for very long strings return displayUrl.substring(0, maxLength - 3) + '...'; } });