// 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');
if ($autosaveSections.length) {
// 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');
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();
});
});
} else {
alert('Error saving: ' + (response.data?.message || 'Unknown error'));
if ($field.attr('type') === 'checkbox') {
$field.prop('checked', !$field.is(':checked'));
}
feedbackContainer.remove();
}
},
error: function() {
alert('An error occurred while saving.');
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');
// Store active tab
try {
localStorage.setItem('mxchat_active_tab', tabId);
} catch (e) {
console.warn('LocalStorage not available:', e);
}
} else {
console.warn('Tab content #' + tabId + ' not found');
}
});
}
// Initialize tabs and handle events
initTabs();
$(document).on('widget-added widget-updated postbox-toggled', initTabs);
// Activate initial tab
try {
var savedTab = localStorage.getItem('mxchat_active_tab');
if (savedTab && $('#' + savedTab).length > 0) {
$('.mxchat-tab-button[data-tab="' + savedTab + '"]').trigger('click.mxchat');
} else {
$('.mxchat-tab-button').first().trigger('click.mxchat');
}
} catch (e) {
$('.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 = $('