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 to check if streaming is supported for the current model function isStreamingSupported(model) { if (!model) return false; //console.log("Checking streaming support for model:", model); // Debug log // Get the model prefix const modelPrefix = model.split('-')[0].toLowerCase(); //console.log("Model prefix:", modelPrefix); // Debug log // Support streaming for OpenAI, Claude, Grok, and DeepSeek models const isSupported = modelPrefix === 'gpt' || modelPrefix === 'o1' || modelPrefix === 'claude' || modelPrefix === 'grok' || modelPrefix === 'deepseek'; //console.log("Streaming supported:", isSupported); // Debug log 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 = $('
tags return paragraphs .map(para => para.trim()) .filter(para => para.length > 0) // Remove empty paragraphs .map(para => `
${para}
`) .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 `${escapedCode}
$1');
// Handle raw PHP tags (legacy support)
text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
const escapedCode = escapeHtml(match);
return `${escapedCode}
')) {
return unsafe;
}
return unsafe
.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 = $('