File: /var/www/html/www.winghung.com/wp-content/plugins/mxchat-basic/js/chat-script.js
jQuery(document).ready(function($) {
// ====================================
// GLOBAL VARIABLES & CONFIGURATION
// ====================================
const toolbarIconColor = mxchatChat.toolbar_icon_color || '#212121';
// Initialize color settings
var userMessageBgColor = mxchatChat.user_message_bg_color;
var userMessageFontColor = mxchatChat.user_message_font_color;
var botMessageBgColor = mxchatChat.bot_message_bg_color;
var botMessageFontColor = mxchatChat.bot_message_font_color;
var liveAgentMessageBgColor = mxchatChat.live_agent_message_bg_color;
var liveAgentMessageFontColor = mxchatChat.live_agent_message_font_color;
var linkTarget = mxchatChat.link_target_toggle === 'on' ? '_blank' : '_self';
let lastSeenMessageId = '';
let notificationCheckInterval;
let notificationBadge;
var sessionId = getChatSession();
let pollingInterval;
let processedMessageIds = new Set();
let activePdfFile = null;
let activeWordFile = null;
// ====================================
// SESSION MANAGEMENT
// ====================================
function getChatSession() {
var sessionId = getCookie('mxchat_session_id');
//console.log("Session ID retrieved from cookie: ", sessionId);
if (!sessionId) {
sessionId = generateSessionId();
//console.log("Generated new session ID: ", sessionId);
setChatSession(sessionId);
}
//console.log("Final session ID: ", sessionId);
return sessionId;
}
function setChatSession(sessionId) {
// Set the cookie with a 24-hour expiration (86400 seconds)
document.cookie = "mxchat_session_id=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
}
function getCookie(name) {
let value = "; " + document.cookie;
let parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
function generateSessionId() {
return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
}
// ====================================
// CONTEXTUAL AWARENESS FUNCTIONALITY
// ====================================
function getPageContext() {
// Check if contextual awareness is enabled
if (mxchatChat.contextual_awareness_toggle !== 'on') {
return null;
}
// Get page URL
const pageUrl = window.location.href;
// Get page title
const pageTitle = document.title || '';
// Get main content from the page
let pageContent = '';
// Try to get content from common content areas
const contentSelectors = [
'main',
'[role="main"]',
'.content',
'.main-content',
'.post-content',
'.entry-content',
'.page-content',
'article',
'#content',
'#main'
];
let contentElement = null;
for (const selector of contentSelectors) {
contentElement = document.querySelector(selector);
if (contentElement) {
break;
}
}
// If no specific content area found, use body but exclude header, footer, nav, sidebar
if (!contentElement) {
contentElement = document.body;
}
if (contentElement) {
// Clone the element to avoid modifying the original
const clone = contentElement.cloneNode(true);
// Remove unwanted elements
const unwantedSelectors = [
'header',
'footer',
'nav',
'.navigation',
'.sidebar',
'.widget',
'.menu',
'script',
'style',
'.comments',
'#comments',
'.breadcrumb',
'.breadcrumbs',
'#floating-chatbot',
'#floating-chatbot-button',
'.mxchat',
'[class*="chat"]',
'[id*="chat"]'
];
unwantedSelectors.forEach(selector => {
const elements = clone.querySelectorAll(selector);
elements.forEach(el => el.remove());
});
// Extract MxChat context data attributes before getting text content
const contextData = [];
clone.querySelectorAll('[data-mxchat-context]').forEach(el => {
const contextValue = el.dataset.mxchatContext;
if (contextValue && contextValue.trim()) {
contextData.push(contextValue);
}
});
// Get text content and clean it up
pageContent = clone.textContent || clone.innerText || '';
// Add context data to page content if any were found
if (contextData.length > 0) {
pageContent += '\n\nAdditional Context:\n' + contextData.join('\n');
}
// Clean up whitespace and limit length
pageContent = pageContent
.replace(/\s+/g, ' ')
.trim()
.substring(0, 3000); // Limit to 3000 characters to avoid token limits
}
// Only return context if we have meaningful content
if (!pageContent || pageContent.length < 50) {
return null;
}
return {
url: pageUrl,
title: pageTitle,
content: pageContent
};
}
// Track originating page when chat starts
function trackOriginatingPage() {
const sessionId = getChatSession();
const pageUrl = window.location.href;
const pageTitle = document.title || 'Untitled Page';
// Only track once per session
const trackingKey = 'mxchat_originating_tracked_' + sessionId;
if (sessionStorage.getItem(trackingKey)) {
return;
}
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
data: {
action: 'mxchat_track_originating_page',
session_id: sessionId,
page_url: pageUrl,
page_title: pageTitle,
nonce: mxchatChat.nonce
},
success: function(response) {
if (response.success) {
sessionStorage.setItem(trackingKey, 'true');
}
}
});
}
// ====================================
// CORE CHAT FUNCTIONALITY
// ====================================
// Update your existing sendMessage function
function sendMessage() {
var message = $('#chat-input').val();
// ADD PROMPT HOOK HERE
if (typeof customMxChatFilter === 'function') {
message = customMxChatFilter(message, "prompt");
}
if (message) {
appendMessage("user", message);
$('#chat-input').val('');
$('#chat-input').css('height', 'auto');
if (hasQuickQuestions()) {
collapseQuickQuestions();
}
appendThinkingMessage();
scrollToBottom();
const currentModel = mxchatChat.model || 'gpt-4o';
// Check if streaming is enabled AND supported for this model
if (shouldUseStreaming(currentModel)) {
callMxChatStream(message, function(response) {
$('.bot-message.temporary-message').removeClass('temporary-message');
});
} else {
callMxChat(message, function(response) {
replaceLastMessage("bot", response);
});
}
}
}
// Update your existing sendMessageToChatbot function
function sendMessageToChatbot(message) {
// ADD PROMPT HOOK HERE
if (typeof customMxChatFilter === 'function') {
message = customMxChatFilter(message, "prompt");
}
var sessionId = getChatSession();
if (hasQuickQuestions()) {
collapseQuickQuestions();
}
appendThinkingMessage();
scrollToBottom();
const currentModel = mxchatChat.model || 'gpt-4o';
// Check if streaming is enabled AND supported for this model
if (shouldUseStreaming(currentModel)) {
callMxChatStream(message, function(response) {
$('.bot-message.temporary-message').removeClass('temporary-message');
});
} else {
callMxChat(message, function(response) {
$('.temporary-message').remove();
replaceLastMessage("bot", response);
});
}
}
// Updated shouldUseStreaming function with debugging
function shouldUseStreaming(model) {
// Check if streaming is enabled in settings (using your toggle naming pattern)
const streamingEnabled = mxchatChat.enable_streaming_toggle === 'on';
// Check if model supports streaming
const streamingSupported = isStreamingSupported(model);
// Only use streaming if both enabled and supported
return streamingEnabled && streamingSupported;
}
// Helper function to handle chat mode updates
function handleChatModeUpdates(response, responseText) {
// Check for explicit chat mode in response (THIS IS THE KEY FIX)
if (response.chat_mode) {
updateChatModeIndicator(response.chat_mode);
return; // Return early since we found explicit mode
}
// Check for fallback response chat mode
else if (response.fallbackResponse && response.fallbackResponse.chat_mode) {
updateChatModeIndicator(response.fallbackResponse.chat_mode);
return; // Return early since we found explicit mode
}
// Only do text-based detection if no explicit mode was provided
// Check for specific AI chatbot response text
if (responseText === 'You are now chatting with the AI chatbot.' ||
responseText.includes('now chatting with the AI') ||
responseText.includes('switched to AI mode') ||
responseText.includes('AI chatbot is now')) {
updateChatModeIndicator('ai');
}
// Check for agent transfer messages
else if (responseText.includes('agent') &&
(responseText.includes('transfer') || responseText.includes('connected'))) {
updateChatModeIndicator('agent');
}
}
//Function to get bot ID from the chatbot wrapper
function getMxChatBotId() {
const chatbotWrapper = document.getElementById('mxchat-chatbot-wrapper');
return chatbotWrapper ? chatbotWrapper.getAttribute('data-bot-id') || 'default' : 'default';
}
function callMxChat(message, callback) {
// Get page context if contextual awareness is enabled
const pageContext = getPageContext();
// Get bot ID
const botId = getMxChatBotId();
// Prepare AJAX data
const ajaxData = {
action: 'mxchat_handle_chat_request',
message: message,
session_id: getChatSession(),
nonce: mxchatChat.nonce,
current_page_url: window.location.href,
current_page_title: document.title,
bot_id: botId // Include bot ID
};
// Add page context if available
if (pageContext) {
ajaxData.page_context = JSON.stringify(pageContext);
}
// CHECK FOR VISION FLAGS AND ADD THEM
if (window.mxchatVisionProcessed) {
ajaxData.vision_processed = true;
ajaxData.original_user_message = window.mxchatOriginalMessage || message;
ajaxData.vision_images_count = window.mxchatVisionImagesCount || 0;
// Clear the flags after use
window.mxchatVisionProcessed = false;
window.mxchatOriginalMessage = null;
window.mxchatVisionImagesCount = 0;
}
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
dataType: 'json',
data: ajaxData,
success: function(response) {
// IMMEDIATE CHAT MODE UPDATE - This should be FIRST
if (response.chat_mode) {
//console.log("Updating chat mode to:", response.chat_mode);
updateChatModeIndicator(response.chat_mode);
}
// Also check in data property if response is wrapped
if (response.data && response.data.chat_mode) {
//console.log("Updating chat mode from data to:", response.data.chat_mode);
updateChatModeIndicator(response.data.chat_mode);
}
// Log the full response for debugging
//console.log("API Response:", response);
// IMMEDIATE CHAT MODE UPDATE - Handle this first, before any other processing
if (response.chat_mode) {
//console.log("Updating chat mode immediately to:", response.chat_mode);
updateChatModeIndicator(response.chat_mode);
}
// First check if this is a successful response by looking for text, html, or message fields
// This preserves compatibility with your server response format
if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
(response.success === true && response.data && response.data.status === 'waiting_for_agent')) {
// Handle successful response - this is your original success handling code
// Handle other responses
let responseText = response.text || '';
let responseHtml = response.html || '';
let responseMessage = response.message || '';
// Add PDF filename handling
if (response.data && response.data.filename) {
showActivePdf(response.data.filename);
activePdfFile = response.data.filename;
}
// Add redirect check here
if (response.redirect_url) {
if (responseText) {
replaceLastMessage("bot", responseText);
}
setTimeout(() => {
window.location.href = response.redirect_url;
}, 1500);
return;
}
// Check for live agent response
if (response.success && response.data && response.data.status === 'waiting_for_agent') {
updateChatModeIndicator('agent');
return;
}
// Handle the message and show notification if chat is hidden
if (responseText || responseHtml || responseMessage) {
// ADD RESPONSE HOOKS HERE - BEFORE DISPLAYING
if (responseText && typeof customMxChatFilter === 'function') {
responseText = customMxChatFilter(responseText, "response");
}
if (responseMessage && typeof customMxChatFilter === 'function') {
responseMessage = customMxChatFilter(responseMessage, "response");
}
// Update the messages as before
if (responseText && responseHtml) {
replaceLastMessage("bot", responseText, responseHtml);
} else if (responseText) {
replaceLastMessage("bot", responseText);
} else if (responseHtml) {
replaceLastMessage("bot", "", responseHtml);
} else if (responseMessage) {
replaceLastMessage("bot", responseMessage);
}
// Check if chat is hidden and show notification
if ($('#floating-chatbot').hasClass('hidden')) {
const badge = $('#chat-notification-badge');
if (badge.length) {
badge.show();
}
}
} else {
////console.error("Unexpected response format:", response);
replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.");
}
if (response.message_id) {
lastSeenMessageId = response.message_id;
}
return;
}
// If we got here, it's likely an error response
// Now we can check for error conditions with our robust error handling
let errorMessage = "";
let errorCode = "";
// Check various possible error locations in the response
if (response.data && response.data.error_message) {
errorMessage = response.data.error_message;
errorCode = response.data.error_code || "";
} else if (response.error_message) {
errorMessage = response.error_message;
errorCode = response.error_code || "";
} else if (response.message) {
errorMessage = response.message;
} else if (typeof response.data === 'string') {
errorMessage = response.data;
} else if (!response.success) {
// Explicit check for success: false without other error info
errorMessage = "An error occurred. Please try again or contact support.";
} else {
// Fallback for any other unexpected response format
errorMessage = "Unexpected response received. Please try again or contact support.";
}
// Log the error with code for debugging
//console.log("Response data:", response.data);
//console.error("API Error:", errorMessage, "Code:", errorCode);
// Format user-friendly error message
let displayMessage = errorMessage;
// Customize message for admin users
if (mxchatChat.is_admin) {
// For admin users, show more technical details including error code
displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
}
replaceLastMessage("bot", displayMessage);
},
error: function(xhr, status, error) {
//console.error("AJAX Error:", status, error);
//console.log("Response Text:", xhr.responseText);
let errorMessage = "An unexpected error occurred.";
// Try to parse the response if it's JSON
try {
const responseJson = JSON.parse(xhr.responseText);
//console.log("Parsed error response:", responseJson);
if (responseJson.data && responseJson.data.error_message) {
errorMessage = responseJson.data.error_message;
} else if (responseJson.message) {
errorMessage = responseJson.message;
}
} catch (e) {
// Not JSON or parsing failed, use HTTP status based messages
if (xhr.status === 0) {
errorMessage = "Network error: Please check your internet connection.";
} else if (xhr.status === 403) {
errorMessage = "Access denied: Your session may have expired. Please refresh the page.";
} else if (xhr.status === 404) {
errorMessage = "API endpoint not found. Please contact support.";
} else if (xhr.status === 429) {
errorMessage = "Too many requests. Please try again in a moment.";
} else if (xhr.status >= 500) {
errorMessage = "Server error: The server encountered an issue. Please try again later.";
}
}
replaceLastMessage("bot", errorMessage);
}
});
}
function callMxChatStream(message, callback) {
//console.log("Using streaming for message:", message);
const currentModel = mxchatChat.model || 'gpt-4o';
if (!isStreamingSupported(currentModel)) {
//console.log("Streaming not supported, falling back to regular call");
callMxChat(message, callback);
return;
}
// Get page context if contextual awareness is enabled
const pageContext = getPageContext();
// Get bot ID
const botId = getMxChatBotId();
const formData = new FormData();
formData.append('action', 'mxchat_stream_chat');
formData.append('message', message);
formData.append('session_id', getChatSession());
formData.append('nonce', mxchatChat.nonce);
formData.append('current_page_url', window.location.href);
formData.append('current_page_title', document.title);
formData.append('bot_id', botId); // Include bot ID
// Add page context if available
if (pageContext) {
formData.append('page_context', JSON.stringify(pageContext));
}
// CHECK FOR VISION FLAGS AND ADD THEM
if (window.mxchatVisionProcessed) {
formData.append('vision_processed', 'true');
formData.append('original_user_message', window.mxchatOriginalMessage || message);
formData.append('vision_images_count', window.mxchatVisionImagesCount || '0');
// Clear the flags after use
window.mxchatVisionProcessed = false;
window.mxchatOriginalMessage = null;
window.mxchatVisionImagesCount = 0;
}
let accumulatedContent = '';
let testingDataReceived = false;
let streamingStarted = false;
fetch(mxchatChat.ajax_url, {
method: 'POST',
body: formData,
credentials: 'same-origin'
})
.then(response => {
//console.log("Streaming response received:", response);
// Store the response for potential fallback handling
const responseClone = response.clone();
if (!response.ok) {
// Try to get error details from response
return responseClone.json().then(errorData => {
//console.log("Server returned error response:", errorData);
throw { isServerError: true, data: errorData };
}).catch(() => {
throw new Error('Network response was not ok');
});
}
// Check if response is JSON instead of streaming
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
//console.log("Received JSON response instead of stream");
return responseClone.json().then(data => {
//console.log("Processing JSON response:", data);
// IMMEDIATE CHAT MODE UPDATE for JSON response
if (data.chat_mode) {
//console.log("JSON Response: Updating chat mode to:", data.chat_mode);
updateChatModeIndicator(data.chat_mode);
}
// Check for testing panel
if (window.mxchatTestPanelInstance && data.testing_data) {
//console.log('Testing data found in streaming JSON response:', data.testing_data);
window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
}
// Handle the JSON response directly
handleNonStreamResponse(data, callback);
return Promise.resolve(); // Prevent further processing
});
}
// Continue with streaming processing
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
function processStream() {
reader.read().then(({ done, value }) => {
if (done) {
//console.log("Streaming completed, final content:", accumulatedContent);
// If streaming completed but no content was received, try to get response as fallback
if (!streamingStarted || !accumulatedContent) {
//console.log("Stream completed with no content, checking for fallback data");
// Try to read the response as JSON
responseClone.text().then(text => {
try {
const data = JSON.parse(text);
if (data.text || data.message) {
handleNonStreamResponse(data, callback);
} else {
// No valid data, fall back to regular call
$('.bot-message.temporary-message').remove();
callMxChat(message, callback);
}
} catch (e) {
// Could not parse, fall back to regular call
$('.bot-message.temporary-message').remove();
callMxChat(message, callback);
}
}).catch(() => {
$('.bot-message.temporary-message').remove();
callMxChat(message, callback);
});
return;
}
if (callback) {
callback(accumulatedContent);
}
return;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6);
if (data === '[DONE]') {
//console.log("Received [DONE] signal");
if (!accumulatedContent) {
//console.log("No content received before [DONE]");
$('.bot-message.temporary-message').remove();
callMxChat(message, callback);
return;
}
if (callback) {
callback(accumulatedContent);
}
return;
}
try {
const json = JSON.parse(data);
// IMMEDIATE CHAT MODE UPDATE FOR STREAMING
if (json.chat_mode) {
//console.log("Stream: Updating chat mode immediately to:", json.chat_mode);
updateChatModeIndicator(json.chat_mode);
}
// Handle testing data
if (json.testing_data && !testingDataReceived) {
//console.log('Testing data received in stream:', json.testing_data);
if (window.mxchatTestPanelInstance) {
window.mxchatTestPanelInstance.handleTestingData(json.testing_data);
testingDataReceived = true;
}
}
// Handle content streaming
else if (json.content) {
streamingStarted = true;
accumulatedContent += json.content;
updateStreamingMessage(accumulatedContent);
}
// Handle complete response in stream (fallback response)
else if (json.text || json.message) {
//console.log("Received complete response in stream:", json);
handleNonStreamResponse(json, callback);
return;
}
// Handle errors
else if (json.error) {
console.error("Streaming error:", json.error);
// Check if we have a fallback response
if (json.fallback || json.text || json.message) {
handleNonStreamResponse(json, callback);
} else {
$('.bot-message.temporary-message').remove();
callMxChat(message, callback);
}
return;
}
} catch (e) {
console.error('Error parsing SSE data:', e, 'Data:', data);
}
}
}
processStream();
}).catch(streamError => {
console.error('Error reading stream:', streamError);
$('.bot-message.temporary-message').remove();
callMxChat(message, callback);
});
}
processStream();
})
.catch(error => {
//console.log('Streaming failed:', error);
// Check if we have server error data with chat mode
if (error && error.isServerError && error.data) {
//console.log('Using server error response data');
// Check for chat mode in error data
if (error.data.chat_mode) {
//console.log("Error Response: Updating chat mode to:", error.data.chat_mode);
updateChatModeIndicator(error.data.chat_mode);
}
handleNonStreamResponse(error.data, callback);
} else {
// Only fall back to regular call if we don't have any response data
//console.log('No response data available, falling back to regular call');
$('.bot-message.temporary-message').remove();
callMxChat(message, callback);
}
});
}
// Helper function to handle non-streaming responses (updated to include immediate chat mode handling)
function handleNonStreamResponse(data, callback) {
//console.log("Handling non-stream response:", data);
// IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES
if (data.chat_mode) {
//console.log("Non-Stream: Updating chat mode immediately to:", data.chat_mode);
updateChatModeIndicator(data.chat_mode);
}
// Also check in data property if response is wrapped
if (data.data && data.data.chat_mode) {
//console.log("Non-Stream: Updating chat mode from data to:", data.data.chat_mode);
updateChatModeIndicator(data.data.chat_mode);
}
// Remove temporary message
$('.bot-message.temporary-message').remove();
// Handle different response formats
if (data.text || data.html || data.message) {
// Apply response hooks
if (data.text && typeof customMxChatFilter === 'function') {
data.text = customMxChatFilter(data.text, "response");
}
if (data.message && typeof customMxChatFilter === 'function') {
data.message = customMxChatFilter(data.message, "response");
}
// Display the response
if (data.text && data.html) {
replaceLastMessage("bot", data.text, data.html);
} else if (data.text) {
replaceLastMessage("bot", data.text);
} else if (data.html) {
replaceLastMessage("bot", "", data.html);
} else if (data.message) {
replaceLastMessage("bot", data.message);
}
}
// Handle other response properties
if (data.data && data.data.filename) {
showActivePdf(data.data.filename);
activePdfFile = data.data.filename;
}
if (data.redirect_url) {
setTimeout(() => {
window.location.href = data.redirect_url;
}, 1500);
}
if (callback) {
callback(data.text || data.message || '');
}
}
// Enhanced updateChatModeIndicator function for immediate DOM updates
function updateChatModeIndicator(mode) {
//console.log("updateChatModeIndicator called with mode:", mode);
const indicator = document.getElementById('chat-mode-indicator');
if (indicator) {
const oldText = indicator.textContent;
if (mode === 'agent') {
indicator.textContent = 'Live Agent';
startPolling();
//console.log("Set indicator to Live Agent");
} else {
// Everything else is AI mode
const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
indicator.textContent = customAiText;
stopPolling();
//console.log("Set indicator to AI Agent:", customAiText);
}
// Force immediate DOM update and reflow
if (oldText !== indicator.textContent) {
// Force a reflow to ensure the change is visible immediately
indicator.style.display = 'none';
indicator.offsetHeight; // Trigger reflow
indicator.style.display = '';
// Double-check after a brief moment to ensure the change stuck
setTimeout(() => {
//console.log("Final indicator text:", indicator.textContent);
if (mode === 'agent' && indicator.textContent !== 'Live Agent') {
console.warn("Mode indicator update failed, forcing update");
indicator.textContent = 'Live Agent';
} else if (mode !== 'agent' && indicator.textContent === 'Live Agent') {
console.warn("Mode indicator stuck on Live Agent, forcing AI update");
const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
indicator.textContent = customAiText;
}
}, 50);
}
} else {
console.error("Chat mode indicator element not found!");
}
}
// Helper function to handle non-streaming responses (updated to include chat mode handling)
function handleNonStreamResponse(data, callback) {
//console.log("Handling non-stream response:", data);
// Remove temporary message
$('.bot-message.temporary-message').remove();
// Handle different response formats
if (data.text || data.html || data.message) {
// Apply response hooks
if (data.text && typeof customMxChatFilter === 'function') {
data.text = customMxChatFilter(data.text, "response");
}
if (data.message && typeof customMxChatFilter === 'function') {
data.message = customMxChatFilter(data.message, "response");
}
// Handle chat mode updates for streaming responses too
handleChatModeUpdates(data, data.text || data.message);
// Display the response
if (data.text && data.html) {
replaceLastMessage("bot", data.text, data.html);
} else if (data.text) {
replaceLastMessage("bot", data.text);
} else if (data.html) {
replaceLastMessage("bot", "", data.html);
} else if (data.message) {
replaceLastMessage("bot", data.message);
}
}
// Handle other response properties
if (data.data && data.data.filename) {
showActivePdf(data.data.filename);
activePdfFile = data.data.filename;
}
if (data.redirect_url) {
setTimeout(() => {
window.location.href = data.redirect_url;
}, 1500);
}
if (callback) {
callback(data.text || data.message || '');
}
}
// Function to update message during streaming
function updateStreamingMessage(content) {
// ADD RESPONSE HOOK FOR REAL-TIME STREAMING
if (typeof customMxChatFilter === 'function') {
content = customMxChatFilter(content, "response");
}
const formattedContent = linkify(content);
// Find the temporary message
const tempMessage = $('.bot-message.temporary-message').last();
if (tempMessage.length) {
// Update existing message
tempMessage.html(formattedContent);
} else {
// Create new temporary message if it doesn't exist
appendMessage("bot", content, '', [], true);
}
}
function isStreamingSupported(model) {
if (!model) return false;
const modelPrefix = model.split('-')[0].toLowerCase();
// Support streaming for OpenAI, Claude, Grok, DeepSeek, and OpenRouter models
const isSupported = modelPrefix === 'gpt' ||
modelPrefix === 'o1' ||
modelPrefix === 'claude' ||
modelPrefix === 'grok' ||
modelPrefix === 'deepseek' ||
model === 'openrouter'; // Add this line - check full model name for OpenRouter
return isSupported;
}
// Update the event handlers to use the correct function names
$('#send-button').off('click').on('click', function() {
sendMessage(); // Use the updated sendMessage function
});
// Override enter key handler
$('#chat-input').off('keypress').on('keypress', function(e) {
if (e.which == 13 && !e.shiftKey) {
e.preventDefault();
sendMessage(); // Use the updated sendMessage function
}
});
function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
try {
// Determine styles based on sender type
let messageClass, bgColor, fontColor;
if (sender === "user") {
messageClass = "user-message";
bgColor = userMessageBgColor;
fontColor = userMessageFontColor;
// Only sanitize user input
messageText = sanitizeUserInput(messageText);
} else if (sender === "agent") {
messageClass = "agent-message";
bgColor = liveAgentMessageBgColor;
fontColor = liveAgentMessageFontColor;
} else {
messageClass = "bot-message";
bgColor = botMessageBgColor;
fontColor = botMessageFontColor;
}
const messageDiv = $('<div>')
.addClass(messageClass)
.attr('dir', 'auto')
.css({
'background': bgColor,
'color': fontColor,
'margin-bottom': '1em'
});
// Process the message content based on sender
let fullMessage;
if (sender === "user") {
// For user messages, apply linkify after sanitization
fullMessage = linkify(messageText);
} else {
// For bot/agent messages, preserve HTML
fullMessage = messageText;
}
// Add images if provided
if (images && images.length > 0) {
fullMessage += '<div class="image-gallery" dir="auto">';
images.forEach(img => {
const safeTitle = sanitizeUserInput(img.title);
const safeUrl = encodeURI(img.image_url);
const safeThumbnail = encodeURI(img.thumbnail_url);
fullMessage += `
<div style="margin-bottom: 10px;">
<strong>${safeTitle}</strong><br>
<a href="${safeUrl}" target="_blank">
<img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
</a>
</div>`;
});
fullMessage += '</div>';
}
// Append HTML content if provided
if (messageHtml && sender !== "user") {
fullMessage += '<br><br>' + messageHtml;
}
messageDiv.html(fullMessage);
if (isTemporary) {
messageDiv.addClass('temporary-message');
}
messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
// FIXED: Use event delegation for link tracking
if (sender === "bot" || sender === "agent") {
attachLinkTracking(messageDiv, messageText);
}
if (sender === "bot") {
const lastUserMessage = $('#chat-box').find('.user-message').last();
if (lastUserMessage.length) {
scrollElementToTop(lastUserMessage);
}
}
});
if (messageText.id) {
lastSeenMessageId = messageText.id;
hideNotification();
}
} catch (error) {
console.error("Error rendering message:", error);
}
}
// Helper function to attach link tracking with proper event handling
function attachLinkTracking(messageDiv, messageText) {
// Use a slight delay to ensure DOM is ready
setTimeout(function() {
const links = messageDiv.find('a[href]').not('[data-tracked]');
links.each(function() {
const $link = $(this);
const originalHref = $link.attr('href');
// Mark as tracked to avoid duplicate handlers
$link.attr('data-tracked', 'true');
// Only track external URLs
if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
// Remove any existing click handlers first
$link.off('click.tracking');
// Add new click handler with namespace
$link.on('click.tracking', function(e) {
e.preventDefault();
e.stopPropagation();
const messageContext = typeof messageText === 'string'
? messageText.substring(0, 200)
: '';
// Track the click
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
data: {
action: 'mxchat_track_url_click',
session_id: getChatSession(),
url: originalHref,
message_context: messageContext,
nonce: mxchatChat.nonce
},
complete: function() {
// Always redirect, even if tracking fails
if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
window.open(originalHref, '_blank');
} else {
window.location.href = originalHref;
}
}
});
return false; // Extra insurance to prevent default
});
}
});
}, 100); // Small delay to ensure DOM is ready
}
function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
// Determine styles
let bgColor, fontColor;
if (sender === "user") {
bgColor = userMessageBgColor;
fontColor = userMessageFontColor;
} else if (sender === "agent") {
bgColor = liveAgentMessageBgColor;
fontColor = liveAgentMessageFontColor;
} else {
bgColor = botMessageBgColor;
fontColor = botMessageFontColor;
}
var fullMessage = linkify(responseText);
if (responseHtml) {
fullMessage += '<br><br>' + responseHtml;
}
if (images.length > 0) {
fullMessage += '<div class="image-gallery" dir="auto">';
images.forEach(img => {
fullMessage += `
<div style="margin-bottom: 10px;">
<strong>${img.title}</strong><br>
<a href="${img.image_url}" target="_blank">
<img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
</a>
</div>`;
});
fullMessage += '</div>';
}
if (lastMessageDiv.length) {
lastMessageDiv.fadeOut(200, function() {
$(this)
.html(fullMessage)
.removeClass('bot-message user-message')
.addClass(messageClass)
.attr('dir', 'auto')
.css({
'background-color': bgColor,
'color': fontColor,
})
.removeClass('temporary-message')
.fadeIn(200, function() {
// FIXED: Use the helper function for link tracking
if (sender === "bot" || sender === "agent") {
attachLinkTracking($(this), responseText);
}
if (sender === "bot" || sender === "agent") {
const lastUserMessage = $('#chat-box').find('.user-message').last();
if (lastUserMessage.length) {
scrollElementToTop(lastUserMessage);
}
// Show notification if chat is hidden
if ($('#floating-chatbot').hasClass('hidden')) {
showNotification();
}
}
});
});
} else {
appendMessage(sender, responseText, responseHtml, images);
}
}
function appendThinkingMessage() {
// Remove any existing thinking dots first
$('.thinking-dots').remove();
// Retrieve the bot message font color and background color
var botMessageFontColor = mxchatChat.bot_message_font_color;
var botMessageBgColor = mxchatChat.bot_message_bg_color;
var thinkingHtml = '<div class="thinking-dots-container">' +
'<div class="thinking-dots">' +
'<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
'<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
'<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
'</div>' +
'</div>';
// Append the thinking dots to the chat container (or within the temporary message div)
$("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
scrollToBottom();
}
function removeThinkingDots() {
$('.thinking-dots').closest('.temporary-message').remove();
}
// ====================================
// TEXT FORMATTING & PROCESSING
// ====================================
function linkify(inputText) {
//console.log('=== LINKIFY DEBUG START ===');
//console.log('Input to linkify:', inputText);
//console.log('Input type:', typeof inputText);
//console.log('Input length:', inputText ? inputText.length : 'null/undefined');
if (!inputText) {
//console.log('Empty input, returning empty string');
return '';
}
// Process markdown headers FIRST (before paragraph wrapping)
//console.log('--- Processing Headers ---');
let processedText = formatMarkdownHeaders(inputText);
//console.log('After headers:', processedText);
//console.log('Headers changed:', processedText !== inputText);
// Process text styling (bold, italic, strikethrough)
//console.log('--- Processing Text Styling ---');
//const beforeStyling = processedText;
processedText = formatTextStyling(processedText);
//console.log('After styling:', processedText);
//console.log('Styling changed:', processedText !== beforeStyling);
// Process code blocks BEFORE processing links to avoid conflicts
//console.log('--- Processing Code Blocks ---');
const beforeCodeBlocks = processedText;
processedText = formatCodeBlocks(processedText);
//console.log('After code blocks:', processedText);
//console.log('Code blocks changed:', processedText !== beforeCodeBlocks);
// NOW convert to paragraphs (this should be AFTER markdown processing)
//console.log('--- Converting to Paragraphs ---');
const beforeParagraphs = processedText;
processedText = convertNewlinesToBreaks(processedText);
//console.log('After paragraph conversion:', processedText);
//console.log('Paragraphs changed:', processedText !== beforeParagraphs);
// Process markdown links
//console.log('--- Processing Markdown Links ---');
const beforeMarkdownLinks = processedText;
const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
//console.log('Found markdown link:', match, 'text:', text, 'url:', url);
const safeUrl = encodeURI(url);
const safeText = sanitizeUserInput(text);
return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
});
//console.log('After markdown links:', processedText);
//console.log('Markdown links changed:', processedText !== beforeMarkdownLinks);
// Process phone numbers (tel:)
//console.log('--- Processing Phone Numbers ---');
const beforePhone = processedText;
const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
processedText = processedText.replace(phonePattern, (match, text, phone) => {
//console.log('Found phone link:', match, 'text:', text, 'phone:', phone);
const safePhone = encodeURI(phone);
const safeText = sanitizeUserInput(text);
return `<a href="${safePhone}">${safeText}</a>`;
});
//console.log('After phone numbers:', processedText);
//console.log('Phone numbers changed:', processedText !== beforePhone);
// Process mailto links
//console.log('--- Processing Mailto Links ---');
const beforeMailto = processedText;
const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
console.log('Found mailto link:', match, 'text:', text, 'mailto:', mailto);
const safeMailto = encodeURI(mailto);
const safeText = sanitizeUserInput(text);
return `<a href="${safeMailto}">${safeText}</a>`;
});
//console.log('After mailto links:', processedText);
//console.log('Mailto links changed:', processedText !== beforeMailto);
// Process standalone URLs (avoid already processed links)
//console.log('--- Processing Standalone URLs ---');
const beforeStandaloneUrls = processedText;
const urlPattern = /(^|[^">])(https?:\/\/[^\s<]+)(?![^<]*<\/a>)/gim;
processedText = processedText.replace(urlPattern, (match, prefix, url) => {
//console.log('Found standalone URL:', match, 'prefix:', prefix, 'url:', url);
const safeUrl = encodeURI(url);
return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
});
//console.log('After standalone URLs:', processedText);
//console.log('Standalone URLs changed:', processedText !== beforeStandaloneUrls);
// Process www. URLs (avoid already processed links)
//console.log('--- Processing WWW URLs ---');
const beforeWwwUrls = processedText;
const wwwPattern = /(^|[^">])(www\.[\S]+(\b|$))(?![^<]*<\/a>)/gim;
processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
//console.log('Found www URL:', match, 'prefix:', prefix, 'url:', url);
const safeUrl = encodeURI(`http://${url}`);
return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
});
//console.log('After www URLs:', processedText);
//console.log('WWW URLs changed:', processedText !== beforeWwwUrls);
//console.log('=== FINAL RESULT ===');
//console.log('Final processed text:', processedText);
//console.log('Total transformation:', inputText !== processedText);
//console.log('=== LINKIFY DEBUG END ===');
return processedText;
}
function formatMarkdownHeaders(text) {
// Handle h1 to h6 headers
return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
const level = hashes.length;
return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`;
});
}
function formatTextStyling(text) {
// Handle bold text (**text**)
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Handle italic text (*text* or _text_) - avoid conflicts with bold
text = text.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
text = text.replace(/(?<!_)_([^_\n]+)_(?!_)/g, '<em>$1</em>');
// Handle strikethrough (~~text~~)
text = text.replace(/~~(.*?)~~/g, '<del>$1</del>');
return text;
}
function formatBoldText(text) {
// This function is kept for compatibility but now uses formatTextStyling
return formatTextStyling(text);
}
function convertNewlinesToBreaks(text) {
// Split the text into paragraphs (marked by double newlines or multiple <br> tags)
const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
// Filter out empty paragraphs and wrap each paragraph in <p> tags
return paragraphs
.map(para => para.trim())
.filter(para => para.length > 0) // Remove empty paragraphs
.map(para => `<p>${para}</p>`)
.join('');
}
function formatCodeBlocks(text) {
// Handle fenced code blocks with language specification (```language)
text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, language, code) => {
const lang = language || 'text';
const escapedCode = escapeHtml(code.trim());
return `<div class="mxchat-code-block-container">
<div class="mxchat-code-header">
<span class="mxchat-code-language">${lang}</span>
<button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
</div>
<pre class="mxchat-code-block"><code class="language-${lang}">${escapedCode}</code></pre>
</div>`;
});
// Handle inline code with single backticks
text = text.replace(/`([^`\n]+)`/g, '<code class="mxchat-inline-code">$1</code>');
// Handle raw PHP tags (legacy support)
text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
const escapedCode = escapeHtml(match);
return `<div class="mxchat-code-block-container">
<div class="mxchat-code-header">
<span class="mxchat-code-language">php</span>
<button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
</div>
<pre class="mxchat-code-block"><code class="language-php">${escapedCode}</code></pre>
</div>`;
});
return text;
}
function sanitizeUserInput(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeHtml(unsafe) {
// Skip escaping if it's already escaped or contains HTML code block markup
if (unsafe.includes('<') || unsafe.includes('>') ||
unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
return unsafe;
}
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function decodeHTMLEntities(text) {
var textArea = document.createElement('textarea');
textArea.innerHTML = text;
return textArea.value;
}
// ====================================
// UI & SCROLLING CONTROLS
// ====================================
function scrollToBottom(instant = false) {
var chatBox = $('#chat-box');
if (instant) {
// Instantly set the scroll position to the bottom
chatBox.scrollTop(chatBox.prop("scrollHeight"));
} else {
// Use requestAnimationFrame for smoother scrolling if needed
let start = null;
const scrollHeight = chatBox.prop("scrollHeight");
const initialScroll = chatBox.scrollTop();
const distance = scrollHeight - initialScroll;
const duration = 500; // Duration in ms
function smoothScroll(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
const currentScroll = initialScroll + (distance * (progress / duration));
chatBox.scrollTop(currentScroll);
if (progress < duration) {
requestAnimationFrame(smoothScroll);
} else {
chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
}
}
requestAnimationFrame(smoothScroll);
}
}
function scrollElementToTop(element) {
var chatBox = $('#chat-box');
var elementTop = element.position().top + chatBox.scrollTop();
chatBox.animate({ scrollTop: elementTop }, 500);
}
function showChatWidget() {
// First ensure display is set
$('#floating-chatbot-button').css('display', 'flex');
// Then handle the fade
$('#floating-chatbot-button').fadeTo(500, 1);
// Force visibility
$('#floating-chatbot-button').removeClass('hidden');
//console.log('Showing widget');
}
function hideChatWidget() {
$('#floating-chatbot-button').css('display', 'none');
$('#floating-chatbot-button').addClass('hidden');
//console.log('Hiding widget');
}
function disableScroll() {
if (isMobile()) {
$('body').css('overflow', 'hidden');
}
}
function enableScroll() {
if (isMobile()) {
$('body').css('overflow', '');
}
}
function isMobile() {
// This can be a simple check, or more sophisticated detection of mobile devices
return window.innerWidth <= 768; // Example threshold for mobile devices
}
function setFullHeight() {
var vh = $(window).innerHeight() * 0.01;
$(':root').css('--vh', vh + 'px');
}
// ====================================
// NOTIFICATION SYSTEM
// ====================================
function createNotificationBadge() {
//console.log("Creating notification badge...");
const chatButton = document.getElementById('floating-chatbot-button');
//console.log("Chat button found:", !!chatButton);
if (!chatButton) return;
// Remove any existing badge first
const existingBadge = chatButton.querySelector('.chat-notification-badge');
if (existingBadge) {
//console.log("Removing existing badge");
existingBadge.remove();
}
notificationBadge = document.createElement('div');
notificationBadge.className = 'chat-notification-badge';
notificationBadge.style.cssText = `
display: none;
position: absolute;
top: -5px;
right: -5px;
background-color: red;
color: white;
border-radius: 50%;
padding: 4px 8px;
font-size: 12px;
font-weight: bold;
z-index: 10001;
`;
chatButton.style.position = 'relative';
chatButton.appendChild(notificationBadge);
}
function showNotification() {
const badge = document.getElementById('chat-notification-badge');
if (badge && $('#floating-chatbot').hasClass('hidden')) {
badge.style.display = 'block';
badge.textContent = '1';
}
}
function hideNotification() {
const badge = document.getElementById('chat-notification-badge');
if (badge) {
badge.style.display = 'none';
}
}
function startNotificationChecking() {
const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
if (!chatPersistenceEnabled) return;
createNotificationBadge();
notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
}
function stopNotificationChecking() {
if (notificationCheckInterval) {
clearInterval(notificationCheckInterval);
}
}
function checkForNewMessages() {
const sessionId = getChatSession();
const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
if (!chatPersistenceEnabled) return;
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
data: {
action: 'mxchat_check_new_messages',
session_id: sessionId,
last_seen_id: lastSeenMessageId,
nonce: mxchatChat.nonce
},
success: function(response) {
if (response.success && response.data.hasNewMessages) {
showNotification();
}
}
});
}
// ====================================
// LIVE AGENT FUNCTIONALITY
// ====================================
function updateChatModeIndicator(mode) {
//console.log("updateChatModeIndicator called with mode:", mode);
const indicator = document.getElementById('chat-mode-indicator');
if (indicator) {
const oldText = indicator.textContent;
if (mode === 'agent') {
indicator.textContent = 'Live Agent';
startPolling();
//console.log("Set indicator to Live Agent");
} else {
// Everything else is AI mode
const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
indicator.textContent = customAiText;
stopPolling();
//console.log("Set indicator to AI Agent:", customAiText);
}
// Force immediate DOM update
if (oldText !== indicator.textContent) {
// Force a reflow to ensure the change is visible immediately
indicator.offsetHeight;
// Double-check after a brief moment
setTimeout(() => {
//console.log("Final indicator text:", indicator.textContent);
}, 50);
}
} else {
console.error("Chat mode indicator element not found!");
}
}
function startPolling() {
// Clear any existing interval first
stopPolling();
// Start new polling interval
pollingInterval = setInterval(checkForAgentMessages, 5000);
//console.log("Started agent message polling");
}
function stopPolling() {
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
//console.log("Stopped agent message polling");
}
}
function checkForAgentMessages() {
const sessionId = getChatSession();
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
dataType: 'json',
data: {
action: 'mxchat_fetch_new_messages',
session_id: sessionId,
last_seen_id: lastSeenMessageId,
persistence_enabled: 'true', // Add this too
nonce: mxchatChat.nonce
},
success: function (response) {
if (response.success && response.data?.new_messages) {
let hasNewMessage = false;
response.data.new_messages.forEach(function (message) {
if (message.role === "agent" && !processedMessageIds.has(message.id)) {
hasNewMessage = true;
// CHANGE THIS LINE:
appendMessage("agent", message.content); // Instead of replaceLastMessage
lastSeenMessageId = message.id;
processedMessageIds.add(message.id);
}
});
if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
showNotification();
}
scrollToBottom(true);
}
},
error: function (xhr, status, error) {
//console.error("Polling error:", xhr, status, error);
}
});
}
// ====================================
// CHAT HISTORY & PERSISTENCE
// ====================================
function loadChatHistory() {
var sessionId = getChatSession();
var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
if (chatPersistenceEnabled && sessionId) {
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
dataType: 'json',
data: {
action: 'mxchat_fetch_conversation_history',
session_id: sessionId
},
success: function(response) {
// Check if the response indicates success
if (response.success) {
// Handle case where conversation data exists and is an array
if (response.data && Array.isArray(response.data.conversation)) {
var $chatBox = $('#chat-box');
var $fragment = $(document.createDocumentFragment());
let highestMessageId = lastSeenMessageId;
// Update chat mode if provided
if (response.data.chat_mode) {
updateChatModeIndicator(response.data.chat_mode);
}
// Only process if there are actual messages
if (response.data.conversation.length > 0) {
$.each(response.data.conversation, function(index, message) {
// Skip agent messages if persistence is off
if (!chatPersistenceEnabled && message.role === 'agent') {
return;
}
var messageClass, messageBgColor, messageFontColor;
switch (message.role) {
case 'user':
messageClass = 'user-message';
messageBgColor = userMessageBgColor;
messageFontColor = userMessageFontColor;
break;
case 'agent':
messageClass = 'agent-message';
messageBgColor = liveAgentMessageBgColor;
messageFontColor = liveAgentMessageFontColor;
break;
default:
messageClass = 'bot-message';
messageBgColor = botMessageBgColor;
messageFontColor = botMessageFontColor;
break;
}
var messageElement = $('<div>').addClass(messageClass)
.css({
'background': messageBgColor,
'color': messageFontColor
});
var content = message.content;
content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
content = decodeHTMLEntities(content);
if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
messageElement.html(content);
} else {
var formattedContent = linkify(content);
messageElement.html(formattedContent);
}
$fragment.append(messageElement);
// Track message IDs
if (message.id) {
highestMessageId = Math.max(highestMessageId, message.id);
processedMessageIds.add(message.id);
}
});
// Only append messages and scroll if we have content
$chatBox.append($fragment);
scrollToBottom(true);
// Collapse quick questions if we have conversation history
if (hasQuickQuestions()) {
collapseQuickQuestions();
}
// Update lastSeenMessageId after history loads
lastSeenMessageId = highestMessageId;
// Only update chat mode if persistence is enabled and we have messages
if (chatPersistenceEnabled) {
var lastMessage = response.data.conversation[response.data.conversation.length - 1];
if (lastMessage.role === 'agent') {
updateChatModeIndicator('agent');
}
}
} else {
// No conversation history - this is normal for new chats
//console.log("No conversation history found for this session - starting fresh chat.");
}
} else {
// Response successful but no conversation data - this is also normal
//console.log("No conversation data in response - starting fresh chat.");
}
} else {
// Only show error if the response explicitly indicates an error
console.warn("Failed to load chat history:", response.message || "Unknown error");
// Don't show error message to user for failed history loads
// Just start with a fresh chat instead
}
},
error: function(xhr, status, error) {
// Only log AJAX errors, don't show them to the user
console.error("AJAX error loading chat history:", status, error);
// Start with fresh chat - don't show error to user
}
});
} else {
//console.log("Chat persistence is disabled or no session ID found. Starting fresh chat.");
}
}
// ====================================
// FILE UPLOAD FUNCTIONALITY
// ====================================
function addSafeEventListener(elementId, eventType, handler) {
const element = document.getElementById(elementId);
if (element) {
element.addEventListener(eventType, handler);
}
}
function showActivePdf(filename) {
const container = document.getElementById('active-pdf-container');
const nameElement = document.getElementById('active-pdf-name');
if (!container || !nameElement) {
//console.error('PDF container elements not found');
return;
}
nameElement.textContent = filename;
container.style.display = 'flex';
}
function showActiveWord(filename) {
const container = document.getElementById('active-word-container');
const nameElement = document.getElementById('active-word-name');
if (!container || !nameElement) {
//console.error('Word document container elements not found');
return;
}
nameElement.textContent = filename;
container.style.display = 'flex';
}
function removeActivePdf() {
const container = document.getElementById('active-pdf-container');
const nameElement = document.getElementById('active-pdf-name');
if (!container || !nameElement || !activePdfFile) return;
fetch(mxchatChat.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
'action': 'mxchat_remove_pdf',
'session_id': sessionId,
'nonce': mxchatChat.nonce
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
container.style.display = 'none';
nameElement.textContent = '';
activePdfFile = null;
appendMessage('bot', 'PDF removed.');
}
})
.catch(error => {
//console.error('Error removing PDF:', error);
});
}
function removeActiveWord() {
const container = document.getElementById('active-word-container');
const nameElement = document.getElementById('active-word-name');
if (!container || !nameElement || !activeWordFile) return;
fetch(mxchatChat.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
'action': 'mxchat_remove_word',
'session_id': sessionId,
'nonce': mxchatChat.nonce
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
container.style.display = 'none';
nameElement.textContent = '';
activeWordFile = null;
appendMessage('bot', 'Word document removed.');
}
})
.catch(error => {
//console.error('Error removing Word document:', error);
});
}
// ====================================
// CONSENT & COMPLIANCE (GDPR)
// ====================================
function initializeChatVisibility() {
//console.log('Initializing chat visibility');
const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
mxchatChat.complianz_toggle === '1' ||
mxchatChat.complianz_toggle === 1;
if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
// Initial check
checkConsentAndShowChat();
// Listen for consent changes
$(document).on('cmplz_status_change', function(event) {
//console.log('Status change detected');
checkConsentAndShowChat();
});
} else {
// If Complianz is not enabled, always show
$('#floating-chatbot-button')
.css('display', 'flex')
.removeClass('hidden no-consent')
.fadeTo(500, 1);
// Also check pre-chat message when Complianz is not enabled
checkPreChatDismissal();
}
}
function checkConsentAndShowChat() {
var consentStatus = cmplz_has_consent('marketing');
var consentType = complianz.consenttype;
//console.log('Checking consent:', {status: consentStatus,type: consentType});
let $widget = $('#floating-chatbot-button');
let $chatbot = $('#floating-chatbot');
let $preChat = $('#pre-chat-message');
if (consentStatus === true) {
//console.log('Consent granted - showing widget');
$widget
.removeClass('no-consent')
.css('display', 'flex')
.removeClass('hidden')
.fadeTo(500, 1);
$chatbot.removeClass('no-consent');
// Show pre-chat message if not dismissed
checkPreChatDismissal();
} else {
//console.log('No consent - hiding widget');
$widget
.addClass('no-consent')
.fadeTo(500, 0, function() {
$(this)
.css('display', 'none')
.addClass('hidden');
});
$chatbot.addClass('no-consent');
// Hide pre-chat message when no consent
$preChat.hide();
}
}
// ====================================
// PRE-CHAT MESSAGE HANDLING
// ====================================
function checkPreChatDismissal() {
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
data: {
action: 'mxchat_check_pre_chat_message_status',
_ajax_nonce: mxchatChat.nonce
},
success: function(response) {
if (response.success && !response.data.dismissed) {
$('#pre-chat-message').fadeIn(250);
} else {
$('#pre-chat-message').hide();
}
},
error: function() {
//console.error('Failed to check pre-chat message dismissal status.');
}
});
}
function handlePreChatDismissal() {
$('#pre-chat-message').fadeOut(200);
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
data: {
action: 'mxchat_dismiss_pre_chat_message',
_ajax_nonce: mxchatChat.nonce
},
success: function() {
$('#pre-chat-message').hide();
},
error: function() {
//console.error('Failed to dismiss pre-chat message.');
}
});
}
// ====================================
// UTILITY FUNCTIONS
// ====================================
function copyToClipboard(text) {
var tempInput = $('<input>');
$('body').append(tempInput);
tempInput.val(text).select();
document.execCommand('copy');
tempInput.remove();
}
function isImageHtml(str) {
return str.startsWith('<img') && str.endsWith('>');
}
// ====================================
// EVENT HANDLERS & INITIALIZATION
// ====================================
$(document).on('click', '.mxchat-popular-question', function () {
var question = $(this).text();
// Append the question as if the user typed it
appendMessage("user", question);
// Only collapse if there are questions
if (hasQuickQuestions()) {
collapseQuickQuestions();
}
// Send the question to the server
sendMessageToChatbot(question);
});
$(document).on('click', '.questions-toggle-btn', function(e) {
e.preventDefault();
e.stopPropagation();
expandQuickQuestions();
});
$(document).on('click', '.questions-collapse-btn', function(e) {
e.preventDefault();
e.stopPropagation();
collapseQuickQuestions();
});
// Chatbot visibility toggle handlers
$(document).on('click', '#floating-chatbot-button', function() {
var chatbot = $('#floating-chatbot');
if (chatbot.hasClass('hidden')) {
chatbot.removeClass('hidden').addClass('visible');
$(this).addClass('hidden');
$('#chat-notification-badge').hide(); // Hide notification when opening chat
disableScroll();
$('#pre-chat-message').fadeOut(250);
} else {
chatbot.removeClass('visible').addClass('hidden');
$(this).removeClass('hidden');
enableScroll();
checkPreChatDismissal();
}
});
$(document).on('click', '#exit-chat-button', function() {
$('#floating-chatbot').addClass('hidden').removeClass('visible');
$('#floating-chatbot-button').removeClass('hidden');
enableScroll();
});
$(document).on('click', '.close-pre-chat-message', function(e) {
e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
$('#pre-chat-message').fadeOut(200, function() {
$(this).remove();
});
});
// PDF upload button handlers
if (document.getElementById('pdf-upload-btn')) {
document.getElementById('pdf-upload-btn').addEventListener('click', function() {
document.getElementById('pdf-upload').click();
});
}
// Word upload button handlers
if (document.getElementById('word-upload-btn')) {
document.getElementById('word-upload-btn').addEventListener('click', function() {
document.getElementById('word-upload').click();
});
}
// PDF file input change handler
addSafeEventListener('pdf-upload', 'change', async function(e) {
const file = e.target.files[0];
if (!file || file.type !== 'application/pdf') {
alert('Please select a valid PDF file.');
return;
}
if (!sessionId) {
//console.error('No session ID found');
alert('Error: No session ID found');
return;
}
if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
//console.error('mxchatChat not properly configured:', mxchatChat);
alert('Error: Ajax configuration missing');
return;
}
// Disable buttons and show loading state
const uploadBtn = document.getElementById('pdf-upload-btn');
const sendBtn = document.getElementById('send-button');
const originalBtnContent = uploadBtn.innerHTML;
try {
const formData = new FormData();
formData.append('action', 'mxchat_upload_pdf');
formData.append('pdf_file', file);
formData.append('session_id', sessionId);
formData.append('nonce', mxchatChat.nonce);
uploadBtn.disabled = true;
sendBtn.disabled = true;
uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
<circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
</svg>`;
const response = await fetch(mxchatChat.ajax_url, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
// Hide popular questions if they exist
const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
if (hasQuickQuestions()) {
collapseQuickQuestions();
}
// Show the active PDF name
showActivePdf(data.data.filename);
appendMessage('bot', data.data.message);
scrollToBottom();
activePdfFile = data.data.filename;
} else {
//console.error('Upload failed:', data.data);
alert('Failed to upload PDF. Please try again.');
}
} catch (error) {
//console.error('Upload error:', error);
alert('Error uploading file. Please try again.');
} finally {
uploadBtn.disabled = false;
sendBtn.disabled = false;
uploadBtn.innerHTML = originalBtnContent;
this.value = ''; // Reset file input
}
});
// Word file input change handler
addSafeEventListener('word-upload', 'change', async function(e) {
const file = e.target.files[0];
if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
alert('Please select a valid Word document (.docx).');
return;
}
if (!sessionId) {
//console.error('No session ID found');
alert('Error: No session ID found');
return;
}
// Disable buttons and show loading state
const uploadBtn = document.getElementById('word-upload-btn');
const sendBtn = document.getElementById('send-button');
const originalBtnContent = uploadBtn.innerHTML;
try {
const formData = new FormData();
formData.append('action', 'mxchat_upload_word');
formData.append('word_file', file);
formData.append('session_id', sessionId);
formData.append('nonce', mxchatChat.nonce);
uploadBtn.disabled = true;
sendBtn.disabled = true;
uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
<circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
</svg>`;
const response = await fetch(mxchatChat.ajax_url, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
// Hide popular questions if they exist
const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
if (hasQuickQuestions()) {
collapseQuickQuestions();
}
// Show the active Word document name
showActiveWord(data.data.filename);
appendMessage('bot', data.data.message);
scrollToBottom();
activeWordFile = data.data.filename;
} else {
//console.error('Upload failed:', data.data);
alert('Failed to upload Word document. Please try again.');
}
} catch (error) {
//console.error('Upload error:', error);
alert('Error uploading file. Please try again.');
} finally {
uploadBtn.disabled = false;
sendBtn.disabled = false;
uploadBtn.innerHTML = originalBtnContent;
this.value = ''; // Reset file input
}
});
// Remove button click handlers
document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
removeActivePdf();
});
document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
removeActiveWord();
});
// Window resize handlers
$(window).on('resize orientationchange', function() {
setFullHeight();
});
// ====================================
// TOOLBAR & STYLING SETUP
// ====================================
// Apply toolbar settings
if (mxchatChat.chat_toolbar_toggle === 'on') {
$('.chat-toolbar').show();
} else {
$('.chat-toolbar').hide();
}
// Apply toolbar icon colors
const toolbarElements = [
'#mxchat-chatbot .toolbar-btn svg',
'#mxchat-chatbot .active-pdf-name',
'#mxchat-chatbot .active-word-name',
'#mxchat-chatbot .remove-pdf-btn svg',
'#mxchat-chatbot .remove-word-btn svg',
'#mxchat-chatbot .toolbar-perplexity svg'
];
toolbarElements.forEach(selector => {
$(selector).css({
'fill': toolbarIconColor,
'stroke': toolbarIconColor,
'color': toolbarIconColor
});
});
// ====================================
// EMAIL COLLECTION SETUP - FIXED VERSION
// ====================================
// Only run email collection setup if it's enabled
if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
//console.log('Email collection is enabled, setting up handlers...');
// Email collection form setup and handlers
const emailForm = document.getElementById('email-collection-form');
const emailBlocker = document.getElementById('email-blocker');
const chatbotWrapper = document.getElementById('chat-container');
if (emailForm && emailBlocker && chatbotWrapper) {
// Add loading state management
let isSubmitting = false;
// Optimized UI transition functions
function showEmailForm() {
emailBlocker.style.display = 'flex';
chatbotWrapper.style.display = 'none';
}
function showChatContainer() {
// Show chat immediately without delay
emailBlocker.style.display = 'none';
chatbotWrapper.style.display = 'flex';
// Load chat history only after showing chat container
if (typeof loadChatHistory === 'function') {
loadChatHistory();
}
}
// Enhanced email validation
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
}
// Enhanced name validation
function isValidName(name) {
return name && name.trim().length >= 2 && name.trim().length <= 100;
}
// Show loading state with spinner
function setSubmissionState(loading) {
const submitButton = document.getElementById('email-submit-button');
const emailInput = document.getElementById('user-email');
const nameInput = document.getElementById('user-name');
if (loading) {
isSubmitting = true;
if (submitButton) submitButton.disabled = true;
if (emailInput) emailInput.disabled = true;
if (nameInput) nameInput.disabled = true;
// Store original content and add spinner
if (submitButton && !submitButton.getAttribute('data-original-html')) {
submitButton.setAttribute('data-original-html', submitButton.innerHTML);
// Add loading spinner while keeping original text
const originalText = submitButton.textContent;
submitButton.innerHTML = `
<svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
<animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
<animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
</circle>
</svg>
${originalText}
`;
submitButton.style.opacity = '0.8';
}
} else {
isSubmitting = false;
if (submitButton) submitButton.disabled = false;
if (emailInput) emailInput.disabled = false;
if (nameInput) nameInput.disabled = false;
// Restore original content
if (submitButton) {
const originalHtml = submitButton.getAttribute('data-original-html');
if (originalHtml) {
submitButton.innerHTML = originalHtml;
}
submitButton.style.opacity = '1';
}
}
}
// Error display functions
function showEmailError(message) {
clearEmailError();
const errorDiv = document.createElement('div');
errorDiv.className = 'email-error';
errorDiv.style.cssText = `
color: #e74c3c;
font-size: 12px;
margin-top: 8px;
padding: 4px 0;
animation: fadeInError 0.3s ease;
`;
errorDiv.textContent = message;
// Add CSS animation if not already present
if (!document.getElementById('email-error-styles')) {
const style = document.createElement('style');
style.id = 'email-error-styles';
style.textContent = `
@keyframes fadeInError {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
.email-input-shake {
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.email-spinner {
display: inline-block;
vertical-align: middle;
}
`;
document.head.appendChild(style);
}
emailForm.appendChild(errorDiv);
// Add shake animation to inputs
const emailInput = document.getElementById('user-email');
const nameInput = document.getElementById('user-name');
if (emailInput) {
emailInput.classList.add('email-input-shake');
setTimeout(() => {
emailInput.classList.remove('email-input-shake');
}, 500);
}
if (nameInput) {
nameInput.classList.add('email-input-shake');
setTimeout(() => {
nameInput.classList.remove('email-input-shake');
}, 500);
}
}
function clearEmailError() {
const existingErrors = emailForm.querySelectorAll('.email-error');
existingErrors.forEach(error => error.remove());
}
// MAIN FORM SUBMIT HANDLER - This is the critical fix
//console.log('Setting up form submit handler...');
// Remove any existing event listeners first
emailForm.removeEventListener('submit', handleFormSubmit);
// Add the form submit handler
emailForm.addEventListener('submit', handleFormSubmit);
function handleFormSubmit(event) {
//console.log('Form submit handler triggered!');
event.preventDefault();
event.stopPropagation();
// Prevent double submission
if (isSubmitting) {
//console.log('Already submitting, ignoring...');
return false;
}
const userEmail = document.getElementById('user-email').value.trim();
const nameInput = document.getElementById('user-name');
const userName = nameInput ? nameInput.value.trim() : '';
const sessionId = getChatSession();
//console.log('Form data:', { userEmail, userName, sessionId });
// Validate email before submission
if (!userEmail) {
showEmailError('Please enter your email address.');
return false;
}
if (!isValidEmail(userEmail)) {
showEmailError('Please enter a valid email address.');
return false;
}
// Validate name if field exists
if (nameInput && !isValidName(userName)) {
showEmailError('Please enter a valid name (2-100 characters).');
return false;
}
// Clear any existing errors
clearEmailError();
setSubmissionState(true);
//console.log('Sending AJAX request...');
// Prepare form data with optional name
const formData = new URLSearchParams({
action: 'mxchat_handle_save_email_and_response',
email: userEmail,
session_id: sessionId,
nonce: mxchatChat.nonce,
});
// Add name to form data if provided
if (userName) {
formData.append('name', userName);
}
fetch(mxchatChat.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData
})
.then((response) => {
//console.log('Response received:', response);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
//console.log('Response data:', data);
setSubmissionState(false);
if (data.success) {
// Show chat immediately
showChatContainer();
// Handle bot response if provided
if (data.message && typeof appendMessage === 'function') {
setTimeout(() => {
appendMessage('bot', data.message);
if (typeof scrollToBottom === 'function') {
scrollToBottom();
}
}, 100);
}
} else {
showEmailError(data.message || 'Failed to save email. Please try again.');
}
})
.catch((error) => {
console.error('Email submission error:', error);
setSubmissionState(false);
showEmailError('An error occurred. Please try again.');
});
return false; // Extra prevention
}
// Real-time email validation
const emailInput = document.getElementById('user-email');
if (emailInput) {
let validationTimeout;
emailInput.addEventListener('input', function() {
// Clear previous validation timeout
if (validationTimeout) {
clearTimeout(validationTimeout);
}
// Debounce validation
validationTimeout = setTimeout(() => {
const email = this.value.trim();
clearEmailError();
if (email && !isValidEmail(email)) {
showEmailError('Please enter a valid email address.');
}
}, 500);
});
// Handle Enter key
emailInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !isSubmitting) {
e.preventDefault();
emailForm.dispatchEvent(new Event('submit'));
}
});
}
// Real-time name validation
const nameInput = document.getElementById('user-name');
if (nameInput) {
let nameValidationTimeout;
nameInput.addEventListener('input', function() {
// Clear previous validation timeout
if (nameValidationTimeout) {
clearTimeout(nameValidationTimeout);
}
// Debounce validation
nameValidationTimeout = setTimeout(() => {
const name = this.value.trim();
clearEmailError();
if (name && !isValidName(name)) {
showEmailError('Name must be between 2 and 100 characters.');
}
}, 500);
});
// Handle Enter key
nameInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !isSubmitting) {
e.preventDefault();
emailForm.dispatchEvent(new Event('submit'));
}
});
}
// Initial state check
if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
//console.log('Using server-provided email state:', mxchatChat.initial_email_state);
const emailState = mxchatChat.initial_email_state;
if (emailState.show_email_form) {
showEmailForm();
} else {
showChatContainer();
}
} else {
// Check email status via AJAX
setTimeout(checkSessionAndEmail, 100);
}
// Check if email exists for the current session
function checkSessionAndEmail() {
const sessionId = getChatSession();
fetch(mxchatChat.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
action: 'mxchat_check_email_provided',
session_id: sessionId,
nonce: mxchatChat.nonce,
})
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
if (data.success) {
if (data.data.logged_in || data.data.email) {
showChatContainer();
} else {
showEmailForm();
}
} else {
// On error, default to showing email form
showEmailForm();
}
})
.catch((error) => {
console.warn('Email check failed, defaulting to email form:', error);
showEmailForm();
});
}
} else {
console.error('Email collection is enabled but essential elements are missing:', {
emailForm: !!emailForm,
emailBlocker: !!emailBlocker,
chatbotWrapper: !!chatbotWrapper
});
}
} else {
//console.log('Email collection is disabled');
}
// Open chatbot when pre-chat message is clicked
$(document).on('click', '#pre-chat-message', function() {
var chatbot = $('#floating-chatbot');
if (chatbot.hasClass('hidden')) {
chatbot.removeClass('hidden').addClass('visible');
$('#floating-chatbot-button').addClass('hidden');
$('#pre-chat-message').fadeOut(250); // Hide pre-chat message
disableScroll(); // Disable scroll when chatbot opens
}
});
var closeButton = document.querySelector('.close-pre-chat-message');
if (closeButton) {
closeButton.addEventListener('click', function() {
$('#pre-chat-message').fadeOut(200); // Hide the message
// Send an AJAX request to set the transient flag for 24 hours
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
data: {
action: 'mxchat_dismiss_pre_chat_message',
_ajax_nonce: mxchatChat.nonce
},
success: function() {
//console.log('Pre-chat message dismissed for 24 hours.');
// Ensure the message is hidden after dismissal
$('#pre-chat-message').hide();
},
error: function() {
////console.error('Failed to dismiss pre-chat message.');
}
});
});
}
function hasQuickQuestions() {
const questionButtons = document.querySelectorAll('#mxchat-popular-questions .mxchat-popular-question');
return questionButtons.length > 0;
}
function collapseQuickQuestions() {
const questionsContainer = document.getElementById('mxchat-popular-questions');
if (questionsContainer && hasQuickQuestions()) {
questionsContainer.classList.add('collapsed');
questionsContainer.classList.add('has-been-collapsed');
try {
sessionStorage.setItem('mxchat_questions_collapsed', 'true');
sessionStorage.setItem('mxchat_questions_has_been_collapsed', 'true');
} catch (e) {
// Ignore if sessionStorage is not available
}
}
}
function expandQuickQuestions() {
const questionsContainer = document.getElementById('mxchat-popular-questions');
if (questionsContainer && hasQuickQuestions()) {
questionsContainer.classList.remove('collapsed');
try {
sessionStorage.setItem('mxchat_questions_collapsed', 'false');
} catch (e) {
// Ignore if sessionStorage is not available
}
}
}
function checkQuickQuestionsState() {
if (!hasQuickQuestions()) {
return; // Don't do anything if no questions exist
}
try {
const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed');
const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed');
const questionsContainer = document.getElementById('mxchat-popular-questions');
if (questionsContainer) {
if (hasBeenCollapsed === 'true') {
questionsContainer.classList.add('has-been-collapsed');
}
if (isCollapsed === 'true') {
questionsContainer.classList.add('collapsed');
}
}
} catch (e) {
// Ignore if sessionStorage is not available
}
}
// Global delegation for dynamically added links as fallback
$(document).on('click', '#chat-box a[href]:not([data-tracked])', function(e) {
const $link = $(this);
const messageDiv = $link.closest('.bot-message, .agent-message');
// Only process bot/agent message links
if (messageDiv.length > 0) {
const originalHref = $link.attr('href');
if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
e.preventDefault();
e.stopPropagation();
// Mark as tracked
$link.attr('data-tracked', 'true');
// Get message context from the message div
const messageText = messageDiv.text().substring(0, 200);
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
data: {
action: 'mxchat_track_url_click',
session_id: getChatSession(),
url: originalHref,
message_context: messageText,
nonce: mxchatChat.nonce
},
complete: function() {
if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
window.open(originalHref, '_blank');
} else {
window.location.href = originalHref;
}
}
});
return false;
}
}
});
// ====================================
// MAIN INITIALIZATION
// ====================================
if ($('#floating-chatbot').hasClass('hidden')) {
$('#floating-chatbot-button').removeClass('hidden');
}
// Initialize when document is ready
setFullHeight();
initializeChatVisibility();
loadChatHistory();
trackOriginatingPage();
// Make functions globally available for add-ons
window.hasQuickQuestions = hasQuickQuestions;
window.collapseQuickQuestions = collapseQuickQuestions;
window.appendMessage = appendMessage;
window.appendThinkingMessage = appendThinkingMessage;
window.scrollToBottom = scrollToBottom;
window.scrollElementToTop = scrollElementToTop;
window.replaceLastMessage = replaceLastMessage;
window.callMxChat = callMxChat;
window.callMxChatStream = callMxChatStream;
window.shouldUseStreaming = shouldUseStreaming;
window.getChatSession = getChatSession;
window.getPageContext = getPageContext;
window.updateStreamingMessage = updateStreamingMessage;
}); // End of jQuery ready
// ====================================
// GLOBAL EVENT LISTENERS (Outside jQuery)
// ====================================
// Event listener for copy button (code blocks)
document.addEventListener("click", (e) => {
if (e.target.classList.contains("mxchat-copy-button")) {
const copyButton = e.target;
const codeBlock = copyButton
.closest(".mxchat-code-block-container")
.querySelector(".mxchat-code-block code");
if (codeBlock) {
// Preserve formatting using innerText
navigator.clipboard.writeText(codeBlock.innerText).then(() => {
copyButton.textContent = "Copied!";
copyButton.setAttribute("aria-label", "Copied to clipboard");
setTimeout(() => {
copyButton.textContent = "Copy";
copyButton.setAttribute("aria-label", "Copy to clipboard");
}, 2000);
});
}
}
});