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 = $('
') .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 += ''; } // Append HTML content if provided if (messageHtml && sender !== "user") { fullMessage += '

' + 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 += '

' + responseHtml; } if (images.length > 0) { fullMessage += ''; } 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 = '
' + '
' + '' + '' + '' + '
' + '
'; // Append the thinking dots to the chat container (or within the temporary message div) $("#chat-box").append('
' + thinkingHtml + '
'); 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 `${safeText}`; }); //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 `${safeText}`; }); //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 `${safeText}`; }); //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}${url}`; }); //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}${url}`; }); //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 `${content.trim()}`; }); } function formatTextStyling(text) { // Handle bold text (**text**) text = text.replace(/\*\*(.*?)\*\*/g, '$1'); // Handle italic text (*text* or _text_) - avoid conflicts with bold text = text.replace(/(?$1'); text = text.replace(/(?$1'); // Handle strikethrough (~~text~~) text = text.replace(/~~(.*?)~~/g, '$1'); 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
tags) const paragraphs = text.split(/(?:\n\n|\\s*\)/g); // Filter out empty paragraphs and wrap each paragraph in

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 `
${lang}
${escapedCode}
`; }); // Handle inline code with single backticks text = text.replace(/`([^`\n]+)`/g, '$1'); // Handle raw PHP tags (legacy support) text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => { const escapedCode = escapeHtml(match); return `
php
${escapedCode}
`; }); 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('
')) { 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 = $('
').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 = $(''); $('body').append(tempInput); tempInput.val(text).select(); document.execCommand('copy'); tempInput.remove(); } function isImageHtml(str) { return str.startsWith(''); } // ==================================== // 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 = ` `; 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 = ` `; 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 = ` ${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); }); } } });