PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.1.5
MxChat – AI Chatbot & Content Generation for WordPress v2.1.5
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/chat-script.js +1202 -3319 3.2.52.1.5 View file →
@@ -1,754 +1,438 @@
1 1 jQuery(document).ready(function($) {
2 +//console.log('mxchatChat object:', mxchatChat);
3 +//console.log('Link Target Toggle Value:', mxchatChat.link_target_toggle);
4 +// Add these variables at the top of your chat-script.js file
5 + const toolbarIconColor = mxchatChat.toolbar_icon_color || '#212121';
2 6
3 - // Nonce refresh is deferred until first user interaction (ensureSession)
4 - // to avoid admin-ajax calls on passive page loads.
5 - var nonceRefreshed = false;
6 - function refreshNonceIfNeeded(callback) {
7 - if (nonceRefreshed || typeof mxchatChat === 'undefined' || !mxchatChat.ajax_url) {
8 - if (callback) callback();
9 - return;
10 - }
11 - nonceRefreshed = true;
12 - $.post(mxchatChat.ajax_url, { action: 'mxchat_refresh_nonce' }, function(res) {
13 - if (res && res.success && res.data && res.data.nonce) {
14 - mxchatChat.nonce = res.data.nonce;
15 - }
16 - if (callback) callback();
17 - });
18 - }
7 + // Initialize color settings
8 + var userMessageBgColor = mxchatChat.user_message_bg_color;
9 + var userMessageFontColor = mxchatChat.user_message_font_color;
10 + var botMessageBgColor = mxchatChat.bot_message_bg_color;
11 + var botMessageFontColor = mxchatChat.bot_message_font_color;
12 + // Add live agent message colors
13 + var liveAgentMessageBgColor = mxchatChat.live_agent_message_bg_color;
14 + var liveAgentMessageFontColor = mxchatChat.live_agent_message_font_color;
19 15
20 - // ====================================
21 - // MULTI-INSTANCE MANAGEMENT SYSTEM
22 - // ====================================
23 16
24 - // Instance registry - tracks all chatbot instances on the page
25 - const MxChatInstances = {
26 - instances: {},
17 + var linkTarget = mxchatChat.link_target_toggle === 'on' ? '_blank' : '_self';
18 + let lastSeenMessageId = '';
19 + let notificationCheckInterval;
20 + let notificationBadge;
21 + // Initialize session ID
22 + var sessionId = getChatSession();
27 23
28 - // Initialize an instance for a bot
29 - init: function(botId) {
30 - if (!this.instances[botId]) {
31 - // When persistence is OFF, track when this session started
32 - // so the AI only sees messages from this page load
33 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
24 + let pollingInterval; // Variable to store the interval ID
25 + let processedMessageIds = new Set(); // Add this at the top with your other variables
26 +//console.log('Live Agent BG Color:', liveAgentMessageBgColor);
27 +//console.log('Live Agent Font Color:', liveAgentMessageFontColor);
28 + let activePdfFile = null;
29 + let activeWordFile = null;
34 30
35 - this.instances[botId] = {
36 - botId: botId,
37 - sessionId: null,
38 - lastSeenMessageId: '',
39 - notificationCheckInterval: null,
40 - pollingInterval: null,
41 - processedMessageIds: new Set(),
42 - activePdfFile: null,
43 - activeWordFile: null,
44 - chatHistoryLoaded: false,
45 - isStreaming: false,
46 - // Fresh context timestamp - only used when persistence is OFF
47 - sessionStartTimestamp: chatPersistenceEnabled ? 0 : Date.now()
48 - };
49 - }
50 - return this.instances[botId];
51 - },
52 31
53 - // Get instance by botId
54 - get: function(botId) {
55 - return this.instances[botId] || this.init(botId);
56 - },
32 +// Function to create and append notification badge
33 +// Function to create and append notification badge
34 +function createNotificationBadge() {
35 + console.log("Creating notification badge...");
36 + const chatButton = document.getElementById('floating-chatbot-button');
37 + console.log("Chat button found:", !!chatButton);
38 +
39 + if (!chatButton) return;
57 40
58 - // Get all active bot IDs
59 - getAllBotIds: function() {
60 - return Object.keys(this.instances);
61 - },
41 + // Remove any existing badge first
42 + const existingBadge = chatButton.querySelector('.chat-notification-badge');
43 + if (existingBadge) {
44 + console.log("Removing existing badge");
45 + existingBadge.remove();
46 + }
62 47
63 - // Session management per bot
64 - // Returns existing session ID from cookie or localStorage (with in-memory fallback),
65 - // or null if none exists. Does NOT create a new session — use ensureSession() for that.
66 - getChatSession: function(botId) {
67 - var cookieName = 'mxchat_session_id_' + botId;
68 - var storageKey = 'mxchat_session_id_' + botId;
69 - var sessionId = getCookie(cookieName);
48 + notificationBadge = document.createElement('div');
49 + notificationBadge.className = 'chat-notification-badge';
50 + notificationBadge.style.cssText = `
51 + display: none;
52 + position: absolute;
53 + top: -5px;
54 + right: -5px;
55 + background-color: red;
56 + color: white;
57 + border-radius: 50%;
58 + padding: 4px 8px;
59 + font-size: 12px;
60 + font-weight: bold;
61 + z-index: 10001;
62 + `;
63 + chatButton.style.position = 'relative';
64 + chatButton.appendChild(notificationBadge);
65 +
66 + console.log("Notification badge created and appended:", {
67 + exists: !!notificationBadge,
68 + parent: notificationBadge?.parentNode?.id,
69 + display: notificationBadge?.style?.display
70 + });
71 +}
70 72
71 - // Fallback to localStorage if cookie is missing (e.g. cleared by browser/consent)
72 - if (!sessionId) {
73 - try { sessionId = localStorage.getItem(storageKey); } catch (e) {}
74 - }
73 +// Function to check for new messages
74 +function checkForNewMessages() {
75 + const sessionId = getChatSession();
76 + const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
77 +
78 + if (!chatPersistenceEnabled) return;
75 79
76 - // Fallback to in-memory instance when cookie AND localStorage are both blocked
77 - // (Safari ITP, strict tracking prevention, cross-origin iframes with partitioned
78 - // storage). Without this, ensureSession() can generate and store an ID that
79 - // getChatSession() then can't read back, causing null session_ids on send.
80 - if (!sessionId && this.instances[botId] && this.instances[botId].sessionId) {
81 - sessionId = this.instances[botId].sessionId;
80 + $.ajax({
81 + url: mxchatChat.ajax_url,
82 + type: 'POST',
83 + data: {
84 + action: 'mxchat_check_new_messages',
85 + session_id: sessionId,
86 + last_seen_id: lastSeenMessageId,
87 + nonce: mxchatChat.nonce
88 + },
89 + success: function(response) {
90 + if (response.success && response.data.hasNewMessages) {
91 + showNotification();
82 92 }
93 + }
94 + });
95 +}
83 96
84 - // Guard against stored sentinel values that indicate earlier broken writes.
85 - if (sessionId === 'null' || sessionId === 'undefined') {
86 - sessionId = null;
87 - }
97 +// Function to show notification
98 +function showNotification() {
99 + const badge = document.getElementById('chat-notification-badge');
100 + if (badge && $('#floating-chatbot').hasClass('hidden')) {
101 + badge.style.display = 'block';
102 + badge.textContent = '1';
103 + }
104 +}
88 105
89 - // Re-sync cookie from localStorage if cookie was lost
90 - if (sessionId && !getCookie(cookieName)) {
91 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
92 - }
106 +function hideNotification() {
107 + const badge = document.getElementById('chat-notification-badge');
108 + if (badge) {
109 + badge.style.display = 'none';
110 + }
111 +}
112 +// Function to start notification checking
113 +function startNotificationChecking() {
114 + const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
115 + if (!chatPersistenceEnabled) return;
93 116
94 - return sessionId || null;
95 - },
117 + createNotificationBadge();
118 + notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
119 +}
96 120
97 - // Lazy session initializer — called on first user interaction
98 - ensureSession: function(botId) {
99 - botId = botId || 'default';
100 - var instance = this.instances[botId] || this.init(botId);
121 +// Function to stop notification checking
122 +function stopNotificationChecking() {
123 + if (notificationCheckInterval) {
124 + clearInterval(notificationCheckInterval);
125 + }
126 +}
101 127
102 - if (instance.sessionId) {
103 - return instance.sessionId;
104 - }
105 128
106 - // Check for existing session from cookie or localStorage
107 - var existingSession = this.getChatSession(botId);
108 129
109 - if (existingSession) {
110 - instance.sessionId = existingSession;
111 - } else {
112 - // Brand new session
113 - var newId = generateSessionId();
114 - this.setChatSession(botId, newId);
115 - instance.sessionId = newId;
116 - }
117 130
118 - // Now that we have a session, do the deferred work
119 - refreshNonceIfNeeded();
120 - trackOriginatingPage();
131 +function getChatSession() {
132 + var sessionId = getCookie('mxchat_session_id');
133 + //console.log("Session ID retrieved from cookie: ", sessionId);
121 134
122 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
123 - // so we do NOT call it here to avoid a race condition.
135 + if (!sessionId) {
136 + sessionId = generateSessionId();
137 + //console.log("Generated new session ID: ", sessionId);
138 + setChatSession(sessionId);
139 + }
124 140
125 - return instance.sessionId;
126 - },
141 + //console.log("Final session ID: ", sessionId);
142 + return sessionId;
143 +}
127 144
128 - setChatSession: function(botId, sessionId) {
129 - var cookieName = 'mxchat_session_id_' + botId;
130 - var storageKey = 'mxchat_session_id_' + botId;
131 - document.cookie = cookieName + "=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
132 - try { localStorage.setItem(storageKey, sessionId); } catch (e) {}
133 - if (this.instances[botId]) {
134 - this.instances[botId].sessionId = sessionId;
135 - }
136 - },
145 +function setChatSession(sessionId) {
146 + // Set the cookie with a 24-hour expiration (86400 seconds)
147 + document.cookie = "mxchat_session_id=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
148 +}
137 149
138 - resetChatSession: function(botId) {
139 - // Clear old session from localStorage before setting new one
140 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
141 - var newSessionId = generateSessionId();
142 - this.setChatSession(botId, newSessionId);
143 - var $chatBox = getElement(botId, 'chat-box');
144 - if ($chatBox.length) {
145 - $chatBox.find('.user-message, .bot-message:not(:first), .agent-message').remove();
146 - }
147 - if (this.instances[botId]) {
148 - this.instances[botId].chatHistoryLoaded = false;
149 - this.instances[botId].processedMessageIds = new Set();
150 - }
151 - },
150 +// Get cookie value by name
151 +function getCookie(name) {
152 + let value = "; " + document.cookie;
153 + let parts = value.split("; " + name + "=");
154 + if (parts.length == 2) return parts.pop().split(";").shift();
155 +}
152 156
153 - // Silent reset — new session ID without clearing the chat UI
154 - // Used when IP changes mid-conversation so the user doesn't see messages vanish
155 - silentResetSession: function(botId) {
156 - try { localStorage.removeItem('mxchat_session_id_' + botId); } catch (e) {}
157 - var newSessionId = generateSessionId();
158 - this.setChatSession(botId, newSessionId);
159 - if (this.instances[botId]) {
160 - this.instances[botId].sessionId = newSessionId;
161 - }
162 - return newSessionId;
163 - }
164 - };
157 +// Generate a new session ID
158 +function generateSessionId() {
159 + return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
160 +}
165 161
166 - // ====================================
167 - // ELEMENT SELECTOR HELPERS
168 - // ====================================
162 +// Function to send the message to the chatbot (backend)
163 +function sendMessageToChatbot(message) {
164 + var sessionId = getChatSession(); // Reuse the session ID logic
169 165
170 - // Check if a specific bot has an AI theme assigned (skip inline colors)
171 - function shouldSkipInlineColors(botId) {
172 - // If global AI theme is active, skip inline colors for all bots
173 - if (mxchatChat.skip_inline_colors) {
174 - return true;
175 - }
176 - // Check if this specific bot has a theme assignment
177 - var botAssignments = mxchatChat.bot_theme_assignments || {};
178 - return botAssignments.hasOwnProperty(botId);
179 - }
166 + // Hide the popular questions section
167 + $('#mxchat-popular-questions').hide();
180 168
181 - // Get element by ID with bot suffix - returns jQuery object
182 - function getElement(botId, elementName) {
183 - return $('#' + elementName + '-' + botId);
184 - }
169 + // Show thinking indicator (no need to append the user's message again)
170 + appendThinkingMessage();
171 + scrollToBottom();
185 172
186 - // Get element by ID with bot suffix - returns DOM element
187 - function getElementDOM(botId, elementName) {
188 - return document.getElementById(elementName + '-' + botId);
189 - }
173 + //console.log("Sending message to chatbot:", message); // Log the message
174 + //console.log("Session ID:", sessionId); // Log the session ID
190 175
191 - // Get bot ID from any element within a chatbot instance
192 - function getBotIdFromElement(element) {
193 - var $wrapper = $(element).closest('.mxchat-chatbot-wrapper');
194 - if ($wrapper.length) {
195 - return $wrapper.data('bot-id') || 'default';
196 - }
197 - // Fallback: try to find from floating container
198 - var $floating = $(element).closest('.floating-chatbot');
199 - if ($floating.length) {
200 - var id = $floating.attr('id') || '';
201 - var match = id.match(/floating-chatbot-(.+)/);
202 - if (match) return match[1];
203 - }
204 - // Fallback: check if element itself has an ID with bot suffix (e.g., floating-chatbot-button-{bot_id})
205 - var elementId = $(element).attr('id') || '';
206 - if (elementId) {
207 - // Match patterns like: floating-chatbot-button-{bot_id}, pre-chat-message-{bot_id}
208 - var idMatch = elementId.match(/^(?:floating-chatbot-button|pre-chat-message|chat-notification-badge)-(.+)$/);
209 - if (idMatch) return idMatch[1];
210 - }
211 - return 'default';
212 - }
176 + // Call the chatbot using the same call logic as sendMessage
177 + callMxChat(message, function(response) {
178 + // ** Ensure temporary thinking message is removed before adding new response **
179 + $('.temporary-message').remove();
213 180
214 - // Get wrapper element for a bot
215 - function getWrapper(botId) {
216 - return getElement(botId, 'mxchat-chatbot-wrapper');
217 - }
181 + // Replace thinking indicator with actual response
182 + replaceLastMessage("bot", response);
183 + });
184 +}
218 185
219 - // ====================================
220 - // GLOBAL VARIABLES & CONFIGURATION
221 - // ====================================
222 - const toolbarIconColor = mxchatChat.toolbar_icon_color || '#212121';
223 186
224 - // Initialize color settings (these are global as they come from PHP)
225 - var userMessageBgColor = mxchatChat.user_message_bg_color;
226 - var userMessageFontColor = mxchatChat.user_message_font_color;
227 - var botMessageBgColor = mxchatChat.bot_message_bg_color;
228 - var botMessageFontColor = mxchatChat.bot_message_font_color;
229 - var liveAgentMessageBgColor = mxchatChat.live_agent_message_bg_color;
230 - var liveAgentMessageFontColor = mxchatChat.live_agent_message_font_color;
231 187
232 - var linkTarget = mxchatChat.link_target_toggle === 'on' ? '_blank' : '_self';
233 188
234 - // ====================================
235 - // SESSION MANAGEMENT (Legacy compatibility)
236 - // ====================================
189 +function sendMessage() {
190 + var message = $('#chat-input').val(); // Get value from textarea
191 + if (message) {
192 + appendMessage("user", message); // Append user's message
193 + $('#chat-input').val(''); // Clear the textarea
194 + $('#chat-input').css('height', 'auto'); // Reset height after clearing content
237 195
238 - function getCookie(name) {
239 - let value = "; " + document.cookie;
240 - let parts = value.split("; " + name + "=");
241 - if (parts.length == 2) return parts.pop().split(";").shift();
242 - }
196 + // Hide the popular questions section
197 + $('#mxchat-popular-questions').hide();
243 198
244 - function generateSessionId() {
245 - return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
246 - }
199 + // Show typing indicator
200 + appendThinkingMessage();
201 + scrollToBottom();
247 202
248 - // Legacy function - now delegates to instance manager
249 - function getChatSession(botId) {
250 - botId = botId || 'default';
251 - return MxChatInstances.getChatSession(botId);
203 + callMxChat(message, function(response) {
204 + // Replace typing indicator with actual response
205 + replaceLastMessage("bot", response);
206 + });
252 207 }
208 +}
253 209
254 - function setChatSession(sessionId, botId) {
255 - botId = botId || 'default';
256 - MxChatInstances.setChatSession(botId, sessionId);
257 - }
258 210
259 - function resetChatSession(botId) {
260 - botId = botId || 'default';
261 - MxChatInstances.resetChatSession(botId);
262 - }
263 211
264 - // ====================================
265 - // INITIALIZE ALL CHATBOT INSTANCES
266 - // ====================================
212 + // Function to append a thinking message with animation
213 + function appendThinkingMessage() {
214 + // Remove any existing thinking dots first
215 + $('.thinking-dots').remove();
267 216
268 - function initializeAllInstances() {
269 - // Find all chatbot wrappers on the page
270 - $('.mxchat-chatbot-wrapper').each(function() {
271 - var botId = $(this).data('bot-id') || 'default';
272 - MxChatInstances.init(botId);
273 - initializeBotInstance(botId);
274 - });
275 - }
217 + // Retrieve the bot message font color and background color
218 + var botMessageFontColor = mxchatChat.bot_message_font_color;
219 + var botMessageBgColor = mxchatChat.bot_message_bg_color;
276 220
277 - function initializeBotInstance(botId) {
278 - var instance = MxChatInstances.get(botId);
279 221
280 - // Initialize quick questions state for this bot
281 - checkQuickQuestionsState(botId);
222 + var thinkingHtml = '<div class="thinking-dots-container">' +
223 + '<div class="thinking-dots">' +
224 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
225 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
226 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
227 + '</div>' +
228 + '</div>';
282 229
283 - // Note: Event handlers use event delegation with class selectors,
284 - // so they work automatically for all instances without per-bot setup
230 + // Append the thinking dots to the chat container (or within the temporary message div)
231 + $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
232 + scrollToBottom();
285 233 }
286 234
287 -// ====================================
288 -// CONTEXTUAL AWARENESS FUNCTIONALITY
289 -// ====================================
290 -
291 -function getPageContext() {
292 - // Check if contextual awareness is enabled
293 - if (mxchatChat.contextual_awareness_toggle !== 'on') {
294 - return null;
295 - }
296 -
297 - // Get page URL
298 - const pageUrl = window.location.href;
299 -
300 - // Get page title
301 - const pageTitle = document.title || '';
302 -
303 - // Get main content from the page
304 - let pageContent = '';
305 -
306 - // Try to get content from common content areas
307 - const contentSelectors = [
308 - 'main',
309 - '[role="main"]',
310 - '.content',
311 - '.main-content',
312 - '.post-content',
313 - '.entry-content',
314 - '.page-content',
315 - 'article',
316 - '#content',
317 - '#main'
318 - ];
319 -
320 - let contentElement = null;
321 - for (const selector of contentSelectors) {
322 - contentElement = document.querySelector(selector);
323 - if (contentElement) {
324 - break;
325 - }
326 - }
327 -
328 - // If no specific content area found, use body but exclude header, footer, nav, sidebar
329 - if (!contentElement) {
330 - contentElement = document.body;
331 - }
332 -
333 - if (contentElement) {
334 - // Clone the element to avoid modifying the original
335 - const clone = contentElement.cloneNode(true);
336 -
337 - // Remove unwanted elements
338 - const unwantedSelectors = [
339 - 'header',
340 - 'footer',
341 - 'nav',
342 - '.navigation',
343 - '.sidebar',
344 - '.widget',
345 - '.menu',
346 - 'script',
347 - 'style',
348 - '.comments',
349 - '#comments',
350 - '.breadcrumb',
351 - '.breadcrumbs',
352 - '#floating-chatbot',
353 - '#floating-chatbot-button',
354 - '.mxchat',
355 - '[class*="chat"]',
356 - '[id*="chat"]'
357 - ];
358 -
359 - unwantedSelectors.forEach(selector => {
360 - const elements = clone.querySelectorAll(selector);
361 - elements.forEach(el => el.remove());
362 - });
363 -
364 - // Extract MxChat context data attributes before getting text content
365 - const contextData = [];
366 - clone.querySelectorAll('[data-mxchat-context]').forEach(el => {
367 - const contextValue = el.dataset.mxchatContext;
368 - if (contextValue && contextValue.trim()) {
369 - contextData.push(contextValue);
235 + // Trigger send button click when "Enter" key is pressed in the textarea
236 + $('#chat-input').keypress(function(e) {
237 + if (e.which == 13 && !e.shiftKey) { // Check if "Enter" is pressed without Shift
238 + e.preventDefault(); // Prevent default "Enter" behavior
239 + $('#send-button').click(); // Trigger send button click
370 240 }
371 241 });
372 -
373 - // Get text content and clean it up
374 - pageContent = clone.textContent || clone.innerText || '';
375 -
376 - // Add context data to page content if any were found
377 - if (contextData.length > 0) {
378 - pageContent += '\n\nAdditional Context:\n' + contextData.join('\n');
379 - }
380 -
381 - // Clean up whitespace and limit length
382 - pageContent = pageContent
383 - .replace(/\s+/g, ' ')
384 - .trim()
385 - .substring(0, 3000); // Limit to 3000 characters to avoid token limits
386 - }
387 -
388 - // Only return context if we have meaningful content
389 - if (!pageContent || pageContent.length < 50) {
390 - return null;
391 - }
392 -
393 - return {
394 - url: pageUrl,
395 - title: pageTitle,
396 - content: pageContent
397 - };
398 -}
399 242
400 -// Track originating page when chat starts
401 -function trackOriginatingPage() {
402 - const sessionId = getChatSession();
403 - const pageUrl = window.location.href;
404 - const pageTitle = document.title || 'Untitled Page';
405 -
406 - // Only track once per session
407 - const trackingKey = 'mxchat_originating_tracked_' + sessionId;
408 - if (sessionStorage.getItem(trackingKey)) {
409 - return;
410 - }
411 -
412 - $.ajax({
413 - url: mxchatChat.ajax_url,
414 - type: 'POST',
415 - data: {
416 - action: 'mxchat_track_originating_page',
417 - session_id: sessionId,
418 - page_url: pageUrl,
419 - page_title: pageTitle,
420 - nonce: mxchatChat.nonce
421 - },
422 - success: function(response) {
423 - if (response.success) {
424 - sessionStorage.setItem(trackingKey, 'true');
425 - }
426 - }
243 + // Handle send button click
244 + $('#send-button').click(function() {
245 + sendMessage();
427 246 });
428 -}
429 247
430 -// ====================================
431 -// CORE CHAT FUNCTIONALITY
432 -// ====================================
248 + // Handle click on popular questions
249 + $('.mxchat-popular-question').on('click', function () {
250 + var question = $(this).text(); // Get the text of the clicked question
433 251
434 -// Helper functions to disable/enable chat input while waiting for response
435 -function disableChatInput(botId) {
436 - botId = botId || 'default';
437 - var chatInput = getElementDOM(botId, 'chat-input');
438 - var sendButton = getElementDOM(botId, 'send-button');
439 - if (chatInput) {
440 - chatInput.disabled = true;
441 - chatInput.style.opacity = '0.6';
442 - }
443 - if (sendButton) {
444 - sendButton.disabled = true;
445 - sendButton.style.opacity = '0.5';
446 - sendButton.style.pointerEvents = 'none';
447 - }
448 -}
252 + // Append the question as if the user typed it
253 + appendMessage("user", question);
449 254
450 -function enableChatInput(botId) {
451 - botId = botId || 'default';
452 - var chatInput = getElementDOM(botId, 'chat-input');
453 - var sendButton = getElementDOM(botId, 'send-button');
454 - if (chatInput) {
455 - chatInput.disabled = false;
456 - chatInput.style.opacity = '1';
457 - chatInput.focus();
458 - }
459 - if (sendButton) {
460 - sendButton.disabled = false;
461 - sendButton.style.opacity = '1';
462 - sendButton.style.pointerEvents = 'auto';
463 - }
464 -}
255 + // Send the question to the server (backend)
256 + sendMessageToChatbot(question);
257 + });
465 258
466 -// Update your existing sendMessage function
467 -function sendMessage(botId) {
468 - botId = botId || 'default';
469 - MxChatInstances.ensureSession(botId);
470 - var $chatInput = getElement(botId, 'chat-input');
471 - var message = $chatInput.val();
472 259
473 - // ADD PROMPT HOOK HERE
474 - if (typeof customMxChatFilter === 'function') {
475 - message = customMxChatFilter(message, "prompt");
476 - }
260 +// Add this new function to handle markdown headers
261 +function formatMarkdownHeaders(text) {
262 + // Handle h1 to h6 headers
263 + return text.replace(/^(#{1,6})\s(.+)$/gm, function(match, hashes, content) {
264 + const level = hashes.length;
265 + return `<h${level} class="chat-heading">${content}</h${level}>`;
266 + });
267 +}
477 268
478 - if (message) {
479 - // Don't disable input in live agent mode - let users chat freely
480 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
481 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
482 - if (!isAgentMode) {
483 - disableChatInput(botId);
484 - }
269 +// Update the linkify function to handle URLs, markdown, and phone numbers
270 +function linkify(inputText) {
271 + if (!inputText) return '';
272 +
273 + // Process markdown headers
274 + let processedText = formatMarkdownHeaders(inputText);
275 +
276 + // Process markdown links
277 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s]+)\)/g;
278 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
279 + const safeUrl = encodeURI(url);
280 + const safeText = sanitizeUserInput(text);
281 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
282 + });
485 283
486 - appendMessage("user", message, '', [], false, botId);
487 - $chatInput.val('');
488 - $chatInput.css('height', 'auto');
284 + // Process phone numbers (tel:)
285 + const phonePattern = /\[([^\]]+)\]\((tel:[\d+]+)\)/g;
286 + processedText = processedText.replace(phonePattern, (match, text, phone) => {
287 + const safePhone = encodeURI(phone);
288 + const safeText = sanitizeUserInput(text);
289 + return `<a href="${safePhone}">${safeText}</a>`;
290 + });
489 291
490 - if (hasQuickQuestions(botId)) {
491 - collapseQuickQuestions(botId);
492 - }
493 - appendThinkingMessage(botId);
494 - scrollToBottom(botId);
292 + // Process standalone URLs
293 + const urlPattern = /(^|[^">])(https?:\/\/[^\s<]+)/gim;
294 + processedText = processedText.replace(urlPattern, (match, prefix, url) => {
295 + const safeUrl = encodeURI(url);
296 + return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
297 + });
495 298
496 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
299 + // Process www. URLs
300 + const wwwPattern = /(^|[^">])(www\.[\S]+(\b|$))(?![^<]*<\/a>)/gim;
301 + processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
302 + const safeUrl = encodeURI(`http://${url}`);
303 + return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
304 + });
497 305
498 - // Check if streaming is enabled AND supported for this model
499 - if (shouldUseStreaming(currentModel)) {
500 - callMxChatStream(message, function(response) {
501 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
502 - }, botId);
503 - } else {
504 - callMxChat(message, function(response) {
505 - replaceLastMessage("bot", response, '', [], botId);
506 - }, botId);
507 - }
508 - }
306 + return processedText;
509 307 }
510 308
511 -// Update your existing sendMessageToChatbot function
512 -function sendMessageToChatbot(message, botId) {
513 - botId = botId || 'default';
514 - MxChatInstances.ensureSession(botId);
309 +function scrollElementToTop(element) {
310 + var chatBox = $('#chat-box');
311 + var elementTop = element.position().top + chatBox.scrollTop();
312 + chatBox.animate({ scrollTop: elementTop }, 500);
313 +}
515 314
516 - // ADD PROMPT HOOK HERE
517 - if (typeof customMxChatFilter === 'function') {
518 - message = customMxChatFilter(message, "prompt");
519 - }
520 315
521 - // Don't disable input in live agent mode - let users chat freely
522 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
523 - var isAgentMode = modeIndicator && modeIndicator.textContent === 'Live Agent';
524 - if (!isAgentMode) {
525 - disableChatInput(botId);
526 - }
316 +// Optimized scrollToBottom function for instant scrolling
317 +function scrollToBottom(instant = false) {
318 + var chatBox = $('#chat-box');
319 + if (instant) {
320 + // Instantly set the scroll position to the bottom
321 + chatBox.scrollTop(chatBox.prop("scrollHeight"));
322 + } else {
323 + // Use requestAnimationFrame for smoother scrolling if needed
324 + let start = null;
325 + const scrollHeight = chatBox.prop("scrollHeight");
326 + const initialScroll = chatBox.scrollTop();
327 + const distance = scrollHeight - initialScroll;
328 + const duration = 500; // Duration in ms
527 329
528 - var sessionId = getChatSession(botId);
330 + function smoothScroll(timestamp) {
331 + if (!start) start = timestamp;
332 + const progress = timestamp - start;
333 + const currentScroll = initialScroll + (distance * (progress / duration));
334 + chatBox.scrollTop(currentScroll);
529 335
530 - if (hasQuickQuestions(botId)) {
531 - collapseQuickQuestions(botId);
532 - }
533 - appendThinkingMessage(botId);
534 - scrollToBottom(botId);
336 + if (progress < duration) {
337 + requestAnimationFrame(smoothScroll);
338 + } else {
339 + chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
340 + }
341 + }
535 342
536 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
537 -
538 - // Check if streaming is enabled AND supported for this model
539 - if (shouldUseStreaming(currentModel)) {
540 - callMxChatStream(message, function(response) {
541 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
542 - }, botId);
543 - } else {
544 - callMxChat(message, function(response) {
545 - getElement(botId, 'chat-box').find('.temporary-message').remove();
546 - replaceLastMessage("bot", response, '', [], botId);
547 - }, botId);
343 + requestAnimationFrame(smoothScroll);
548 344 }
549 345 }
550 346
551 -// Updated shouldUseStreaming function with debugging
552 -function shouldUseStreaming(model) {
553 - // Check if streaming is enabled in settings (using your toggle naming pattern)
554 - const streamingEnabled = mxchatChat.enable_streaming_toggle === 'on';
555 -
556 - // Check if model supports streaming
557 - const streamingSupported = isStreamingSupported(model);
558 -
559 -
560 - // Only use streaming if both enabled and supported
561 - return streamingEnabled && streamingSupported;
562 -}
563 347
564 -// Helper function to handle chat mode updates
565 -function handleChatModeUpdates(response, responseText) {
566 - // Check for explicit chat mode in response (THIS IS THE KEY FIX)
567 - if (response.chat_mode) {
568 - updateChatModeIndicator(response.chat_mode);
569 - return; // Return early since we found explicit mode
348 + // Function to format text with **bold** inside double asterisks
349 + function formatBoldText(text) {
350 + return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
570 351 }
571 - // Check for fallback response chat mode
572 - else if (response.fallbackResponse && response.fallbackResponse.chat_mode) {
573 - updateChatModeIndicator(response.fallbackResponse.chat_mode);
574 - return; // Return early since we found explicit mode
575 - }
352 +
353 + // Function to convert newline characters to HTML line breaks and handle paragraph spacing
354 +function convertNewlinesToBreaks(text) {
355 + // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
356 + const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
576 357
577 - // Only do text-based detection if no explicit mode was provided
578 - // Check for specific AI chatbot response text
579 - if (responseText === 'You are now chatting with the AI chatbot.' ||
580 - responseText.includes('now chatting with the AI') ||
581 - responseText.includes('switched to AI mode') ||
582 - responseText.includes('AI chatbot is now')) {
583 - updateChatModeIndicator('ai');
584 - }
585 - // Check for agent transfer messages
586 - else if (responseText.includes('agent') &&
587 - (responseText.includes('transfer') || responseText.includes('connected'))) {
588 - updateChatModeIndicator('agent');
589 - }
358 + // Wrap each paragraph in <p> tags
359 + return paragraphs
360 + .map(para => `<p>${para.trim()}</p>`)
361 + .join('');
590 362 }
591 -
592 -// Function to get bot ID from any element or wrapper
593 -// If element is provided, finds the bot ID from its wrapper
594 -// If no element, returns 'default' (for backward compatibility)
595 -function getMxChatBotId(element) {
596 - if (element) {
597 - return getBotIdFromElement(element);
363 + // Copy to clipboard function
364 + // Function to copy text to clipboard
365 + function copyToClipboard(text) {
366 + var tempInput = $('<input>');
367 + $('body').append(tempInput);
368 + tempInput.val(text).select();
369 + document.execCommand('copy');
370 + tempInput.remove();
598 371 }
599 - // Fallback: find first chatbot wrapper on page
600 - const chatbotWrapper = document.querySelector('.mxchat-chatbot-wrapper');
601 - return chatbotWrapper ? chatbotWrapper.getAttribute('data-bot-id') || 'default' : 'default';
602 -}
603 372
604 -function callMxChat(message, callback, botId) {
605 - botId = botId || getMxChatBotId();
606 373
607 - // Store the message in case we need to retry after session reset
608 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
609 -
610 - // Get page context if contextual awareness is enabled
611 - const pageContext = getPageContext();
612 -
613 - // Get instance for session start timestamp (used when persistence is OFF)
614 - var instance = MxChatInstances.get(botId);
615 -
616 - // Guarantee a non-null session_id before the AJAX leaves. ensureSession() is idempotent
617 - // and returns the guaranteed-present session id from the in-memory instance even when
618 - // cookie/localStorage writes are silently blocked by the browser.
619 - var sessionId = MxChatInstances.ensureSession(botId);
620 - if (!sessionId || sessionId === 'null' || sessionId === 'undefined') {
621 - // Last-resort generation to ensure we never POST a null marker.
622 - sessionId = generateSessionId();
623 - MxChatInstances.setChatSession(botId, sessionId);
374 +function updateChatModeIndicator(mode) {
375 + const indicator = document.getElementById('chat-mode-indicator');
376 + if (indicator) {
377 + // For Live Agent, keep as is; for AI mode, use the customized text
378 + if (mode === 'agent') {
379 + indicator.textContent = 'Live Agent';
380 + } else {
381 + // Get the custom AI agent text from a data attribute we'll add to the element
382 + const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
383 + indicator.textContent = customAiText;
384 + }
624 385 }
386 + // Start or stop polling based on mode
387 + if (mode === 'agent') {
388 + startPolling();
389 + } else {
390 + stopPolling();
391 + }
392 +}
625 393
626 - // Prepare AJAX data
627 - const ajaxData = {
628 - action: 'mxchat_handle_chat_request',
629 - message: message,
630 - session_id: sessionId,
631 - nonce: mxchatChat.nonce,
632 - current_page_url: window.location.href,
633 - current_page_title: document.title,
634 - bot_id: botId,
635 - // Pass session start timestamp so AI context matches what user sees
636 - session_start_timestamp: instance.sessionStartTimestamp || 0
637 - };
638 -
639 - // Add page context if available
640 - if (pageContext) {
641 - ajaxData.page_context = JSON.stringify(pageContext);
642 - }
643 -
644 - // CHECK FOR VISION FLAGS AND ADD THEM
645 - if (window.mxchatVisionProcessed) {
646 - ajaxData.vision_processed = true;
647 - ajaxData.original_user_message = window.mxchatOriginalMessage || message;
648 - ajaxData.vision_images_count = window.mxchatVisionImagesCount || 0;
649 - // Clear the flags after use
650 - window.mxchatVisionProcessed = false;
651 - window.mxchatOriginalMessage = null;
652 - window.mxchatVisionImagesCount = 0;
653 - }
654 -
394 +function callMxChat(message, callback) {
655 395 $.ajax({
656 396 url: mxchatChat.ajax_url,
657 397 type: 'POST',
658 398 dataType: 'json',
659 - data: ajaxData,
399 + data: {
400 + action: 'mxchat_handle_chat_request',
401 + message: message,
402 + session_id: getChatSession(),
403 + nonce: mxchatChat.nonce
404 + },
660 405 success: function(response) {
661 - // IMMEDIATE CHAT MODE UPDATE - This should be FIRST
662 - if (response.chat_mode) {
663 - updateChatModeIndicator(response.chat_mode, botId);
664 - }
665 -
666 - // Also check in data property if response is wrapped
667 - if (response.data && response.data.chat_mode) {
668 - updateChatModeIndicator(response.data.chat_mode, botId);
669 - }
670 -
671 - // SECURITY FIX: Check for errors FIRST before checking for success
672 - // This ensures API errors (quota exceeded, invalid key, rate limit) are properly displayed
673 - if (response.success === false || (response.data && response.data.error_message)) {
674 - let errorMessage = "";
675 - let errorCode = "";
676 -
677 - // Check various possible error locations in the response
678 - if (response.data && response.data.error_message) {
679 - errorMessage = response.data.error_message;
680 - errorCode = response.data.error_code || "";
681 - } else if (response.error_message) {
682 - errorMessage = response.error_message;
683 - errorCode = response.error_code || "";
684 - } else if (response.message) {
685 - errorMessage = response.message;
686 - } else if (typeof response.data === 'string') {
687 - errorMessage = response.data;
688 - } else {
689 - // Fallback for any other unexpected response format
690 - errorMessage = "An error occurred. Please try again or contact support.";
691 - }
692 -
693 - // Handle session reset action (IP changed, session expired, etc.)
694 - // Silent reset — keep chat UI intact, just get a new session and retry
695 - if (response.data && response.data.action === 'reset_session') {
696 - MxChatInstances.silentResetSession(botId);
697 - // Re-send the original message with the new session (user message is already displayed)
698 - var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
699 - if (originalMessage) {
700 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
701 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
702 - if (shouldUseStreaming(currentModel)) {
703 - callMxChatStream(originalMessage, function(response) {
704 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').removeClass('temporary-message');
705 - }, botId);
706 - } else {
707 - callMxChat(originalMessage, function(response) {
708 - replaceLastMessage("bot", response, '', [], botId);
709 - }, botId);
710 - }
711 - }
712 - return;
713 - }
714 -
715 - // Format user-friendly error message
716 - let displayMessage = errorMessage;
717 -
718 - // Customize message for admin users
719 - if (mxchatChat.is_admin) {
720 - // For admin users, show more technical details including error code
721 - displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
722 - }
723 -
724 - replaceLastMessage("bot", displayMessage, '', [], botId);
725 - return; // Exit early for errors
726 - }
727 -
728 - // NOW check if this is a successful response by looking for text, html, or message fields
406 + // Log the full response for debugging
407 + console.log("API Response:", response);
408 +
409 + // First check if this is a successful response by looking for text, html, or message fields
729 410 // This preserves compatibility with your server response format
730 - if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
411 + if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
731 412 (response.success === true && response.data && response.data.status === 'waiting_for_agent')) {
732 -
413 +
733 414 // Handle successful response - this is your original success handling code
734 -
735 - // Handle other responses
736 - let responseText = response.text || '';
737 - let responseHtml = response.html || '';
738 - let responseMessage = response.message || '';
739 -
415 +
416 + // Existing chat mode check
417 + if (response.chat_mode) {
418 + updateChatModeIndicator(response.chat_mode);
419 + }
420 + else if (response.fallbackResponse && response.fallbackResponse.chat_mode) {
421 + updateChatModeIndicator(response.fallbackResponse.chat_mode);
422 + }
423 +
740 424 // Add PDF filename handling
741 425 if (response.data && response.data.filename) {
742 - showActivePdf(response.data.filename, botId);
743 - var instance = MxChatInstances.get(botId);
744 - instance.activePdfFile = response.data.filename;
426 + showActivePdf(response.data.filename);
427 + activePdfFile = response.data.filename;
745 428 }
746 -
429 +
747 430 // Add redirect check here
748 431 if (response.redirect_url) {
432 + let responseText = response.text || '';
749 433 if (responseText) {
750 - replaceLastMessage("bot", responseText, '', [], botId);
434 + replaceLastMessage("bot", responseText);
751 435 }
752 436 setTimeout(() => {
753 437 window.location.href = response.redirect_url;
754 438 }, 1500);
@@ -753,73 +437,107 @@
753 437 window.location.href = response.redirect_url;
754 438 }, 1500);
755 439 return;
756 440 }
757 -
441 +
758 442 // Check for live agent response
759 443 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
760 - removeThinkingDots(botId);
761 - updateChatModeIndicator('agent', botId);
762 - enableChatInput(botId);
444 + updateChatModeIndicator('agent');
763 445 return;
764 446 }
765 -
447 +
448 + // Handle other responses
449 + let responseText = response.text || '';
450 + let responseHtml = response.html || '';
451 + let responseMessage = response.message || '';
452 +
453 + if (responseText === 'You are now chatting with the AI chatbot.') {
454 + updateChatModeIndicator('ai');
455 + }
456 +
766 457 // Handle the message and show notification if chat is hidden
767 458 if (responseText || responseHtml || responseMessage) {
768 -
769 - // ADD RESPONSE HOOKS HERE - BEFORE DISPLAYING
770 - if (responseText && typeof customMxChatFilter === 'function') {
771 - responseText = customMxChatFilter(responseText, "response");
772 - }
773 - if (responseMessage && typeof customMxChatFilter === 'function') {
774 - responseMessage = customMxChatFilter(responseMessage, "response");
775 - }
776 -
777 459 // Update the messages as before
778 460 if (responseText && responseHtml) {
779 - replaceLastMessage("bot", responseText, responseHtml, [], botId);
461 + replaceLastMessage("bot", responseText, responseHtml);
780 462 } else if (responseText) {
781 - replaceLastMessage("bot", responseText, '', [], botId);
463 + replaceLastMessage("bot", responseText);
782 464 } else if (responseHtml) {
783 - replaceLastMessage("bot", "", responseHtml, [], botId);
465 + replaceLastMessage("bot", "", responseHtml);
784 466 } else if (responseMessage) {
785 - replaceLastMessage("bot", responseMessage, '', [], botId);
467 + replaceLastMessage("bot", responseMessage);
786 468 }
787 -
469 +
788 470 // Check if chat is hidden and show notification
789 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
790 - if ($floatingChatbot.hasClass('hidden')) {
791 - var $badge = getElement(botId, 'chat-notification-badge');
792 - if ($badge.length) {
793 - $badge.show();
471 + if ($('#floating-chatbot').hasClass('hidden')) {
472 + const badge = $('#chat-notification-badge');
473 + if (badge.length) {
474 + badge.show();
794 475 }
795 476 }
796 477 } else {
797 - var emptyMsg = "I received an empty response. Please try again or contact support if this persists.";
798 - if (response.vectorstore_error) {
799 - emptyMsg = "I received an empty response. Debug info: " + response.vectorstore_error;
800 - }
801 - replaceLastMessage("bot", emptyMsg, '', [], botId);
478 + console.error("Unexpected response format:", response);
479 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.");
802 480 }
803 -
481 +
804 482 if (response.message_id) {
805 - var instance = MxChatInstances.get(botId);
806 - instance.lastSeenMessageId = response.message_id;
483 + lastSeenMessageId = response.message_id;
807 484 }
808 -
485 +
809 486 return;
810 487 }
811 -
812 - // Fallback for truly unexpected response formats
813 - replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.", '', [], botId);
488 +
489 + // If we got here, it's likely an error response
490 + // Now we can check for error conditions with our robust error handling
491 +
492 + let errorMessage = "";
493 + let errorCode = "";
494 +
495 + // Check various possible error locations in the response
496 + if (response.data && response.data.error_message) {
497 + errorMessage = response.data.error_message;
498 + errorCode = response.data.error_code || "";
499 + } else if (response.error_message) {
500 + errorMessage = response.error_message;
501 + errorCode = response.error_code || "";
502 + } else if (response.message) {
503 + errorMessage = response.message;
504 + } else if (typeof response.data === 'string') {
505 + errorMessage = response.data;
506 + } else if (!response.success) {
507 + // Explicit check for success: false without other error info
508 + errorMessage = "An error occurred. Please try again or contact support.";
509 + } else {
510 + // Fallback for any other unexpected response format
511 + errorMessage = "Unexpected response received. Please try again or contact support.";
512 + }
513 +
514 + // Log the error with code for debugging
515 + console.log("Response data:", response.data);
516 + console.error("API Error:", errorMessage, "Code:", errorCode);
517 +
518 + // Format user-friendly error message
519 + let displayMessage = errorMessage;
520 +
521 + // Customize message for admin users
522 + if (mxchatChat.is_admin) {
523 + // For admin users, show more technical details including error code
524 + displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
525 + }
526 +
527 + replaceLastMessage("bot", displayMessage);
814 528 },
815 529 error: function(xhr, status, error) {
530 + console.error("AJAX Error:", status, error);
531 + console.log("Response Text:", xhr.responseText);
532 +
816 533 let errorMessage = "An unexpected error occurred.";
817 -
534 +
818 535 // Try to parse the response if it's JSON
819 536 try {
820 537 const responseJson = JSON.parse(xhr.responseText);
821 -
538 + console.log("Parsed error response:", responseJson);
539 +
822 540 if (responseJson.data && responseJson.data.error_message) {
823 541 errorMessage = responseJson.data.error_message;
824 542 } else if (responseJson.message) {
825 543 errorMessage = responseJson.message;
@@ -837,738 +555,21 @@
837 555 } else if (xhr.status >= 500) {
838 556 errorMessage = "Server error: The server encountered an issue. Please try again later.";
839 557 }
840 558 }
841 -
842 - replaceLastMessage("bot", errorMessage, '', [], botId);
559 +
560 + replaceLastMessage("bot", errorMessage);
843 561 }
844 562 });
845 563 }
846 -
847 -function callMxChatStream(message, callback, botId) {
848 - botId = botId || getMxChatBotId();
849 -
850 - // Store the message in case we need to retry after session reset
851 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', message);
852 -
853 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
854 - if (!isStreamingSupported(currentModel)) {
855 - callMxChat(message, callback, botId);
856 - return;
857 - }
858 -
859 - // Get page context if contextual awareness is enabled
860 - const pageContext = getPageContext();
861 -
862 - // Get instance for session start timestamp (used when persistence is OFF)
863 - var instance = MxChatInstances.get(botId);
864 -
865 - // Guarantee a non-null session_id before the fetch. FormData.append() stringifies any
866 - // non-string value via String(), so passing `null` would POST the literal string "null"
867 - // and land in the transcripts table as a ghost session. ensureSession() always returns
868 - // a real string even when cookies/localStorage are blocked.
869 - var streamSessionId = MxChatInstances.ensureSession(botId);
870 - if (!streamSessionId || streamSessionId === 'null' || streamSessionId === 'undefined') {
871 - streamSessionId = generateSessionId();
872 - MxChatInstances.setChatSession(botId, streamSessionId);
873 - }
874 -
875 - const formData = new FormData();
876 - formData.append('action', 'mxchat_stream_chat');
877 - formData.append('message', message);
878 - formData.append('session_id', streamSessionId);
879 - formData.append('nonce', mxchatChat.nonce);
880 - formData.append('current_page_url', window.location.href);
881 - formData.append('current_page_title', document.title);
882 - formData.append('bot_id', botId);
883 - // Pass session start timestamp so AI context matches what user sees
884 - formData.append('session_start_timestamp', instance.sessionStartTimestamp || 0);
885 -
886 - // Add page context if available
887 - if (pageContext) {
888 - formData.append('page_context', JSON.stringify(pageContext));
889 - }
890 -
891 - // CHECK FOR VISION FLAGS AND ADD THEM
892 - if (window.mxchatVisionProcessed) {
893 - formData.append('vision_processed', 'true');
894 - formData.append('original_user_message', window.mxchatOriginalMessage || message);
895 - formData.append('vision_images_count', window.mxchatVisionImagesCount || '0');
896 - // Clear the flags after use
897 - window.mxchatVisionProcessed = false;
898 - window.mxchatOriginalMessage = null;
899 - window.mxchatVisionImagesCount = 0;
900 - }
901 -
902 - let accumulatedContent = '';
903 - let testingDataReceived = false;
904 - let streamingStarted = false;
905 -
906 - fetch(mxchatChat.ajax_url, {
907 - method: 'POST',
908 - body: formData,
909 - credentials: 'same-origin'
910 - })
911 - .then(response => {
912 - // Store the response for potential fallback handling
913 - const responseClone = response.clone();
914 -
915 - if (!response.ok) {
916 - // Try to get error details from response
917 - return responseClone.json().then(errorData => {
918 - throw { isServerError: true, data: errorData };
919 - }).catch(() => {
920 - throw new Error('Network response was not ok');
921 - });
922 - }
923 -
924 - // Check if response is JSON instead of streaming
925 - const contentType = response.headers.get('content-type');
926 - if (contentType && contentType.includes('application/json')) {
927 - return responseClone.json().then(data => {
928 - // IMMEDIATE CHAT MODE UPDATE for JSON response
929 - if (data.chat_mode) {
930 - updateChatModeIndicator(data.chat_mode, botId);
931 - }
932 -
933 - // Check for testing panel
934 - if (window.mxchatTestPanelInstance && data.testing_data) {
935 - window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
936 - }
937 -
938 - // Handle the JSON response directly
939 - handleNonStreamResponse(data, callback, botId);
940 - return Promise.resolve(); // Prevent further processing
941 - });
942 - }
943 -
944 - // Continue with streaming processing
945 - const reader = response.body.getReader();
946 - const decoder = new TextDecoder();
947 - let buffer = '';
948 -
949 - function processStream() {
950 - reader.read().then(({ done, value }) => {
951 - if (done) {
952 - // If streaming completed but no content was received, try to get response as fallback
953 - if (!streamingStarted || !accumulatedContent) {
954 - // Try to read the response as JSON
955 - responseClone.text().then(text => {
956 - try {
957 - const data = JSON.parse(text);
958 - if (data.text || data.message || data.html) {
959 - handleNonStreamResponse(data, callback, botId);
960 - } else {
961 - // No valid data, fall back to regular call
962 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
963 - callMxChat(message, callback, botId);
964 - }
965 - } catch (e) {
966 - // Could not parse, fall back to regular call
967 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
968 - callMxChat(message, callback, botId);
969 - }
970 - }).catch(() => {
971 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
972 - callMxChat(message, callback, botId);
973 - });
974 - return;
975 - }
976 -
977 - // Re-enable chat input when stream ends with content
978 - enableChatInput(botId);
979 -
980 - // Scroll the user's last message to the top now that the
981 - // bot's full reply has rendered (gives max reading room).
982 - var $chatBoxDone = getElement(botId, 'chat-box');
983 - var $lastUserMsgDone = $chatBoxDone.find('.user-message').last();
984 - if ($lastUserMsgDone.length) {
985 - scrollElementToTop($lastUserMsgDone, botId);
986 - }
987 -
988 - if (callback) {
989 - callback(accumulatedContent);
990 - }
991 - return;
992 - }
993 -
994 - buffer += decoder.decode(value, { stream: true });
995 - const lines = buffer.split('\n');
996 - buffer = lines.pop() || '';
997 -
998 - for (const line of lines) {
999 - if (line.startsWith('data: ')) {
1000 - const data = line.substring(6);
1001 -
1002 - if (data === '[DONE]') {
1003 - if (!accumulatedContent) {
1004 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1005 - callMxChat(message, callback, botId);
1006 - return;
1007 - }
1008 -
1009 - // Re-enable chat input after streaming completes
1010 - enableChatInput(botId);
1011 -
1012 - // Scroll the user's last message to the top now
1013 - // that the bot's full reply has rendered.
1014 - var $chatBoxStreamDone = getElement(botId, 'chat-box');
1015 - var $lastUserMsgStreamDone = $chatBoxStreamDone.find('.user-message').last();
1016 - if ($lastUserMsgStreamDone.length) {
1017 - scrollElementToTop($lastUserMsgStreamDone, botId);
1018 - }
1019 -
1020 - if (callback) {
1021 - callback(accumulatedContent);
1022 - }
1023 - return;
1024 - }
1025 -
1026 - try {
1027 - const json = JSON.parse(data);
1028 -
1029 - // IMMEDIATE CHAT MODE UPDATE FOR STREAMING
1030 - if (json.chat_mode) {
1031 - updateChatModeIndicator(json.chat_mode, botId);
1032 - }
1033 -
1034 - // Handle testing data
1035 - if (json.testing_data && !testingDataReceived) {
1036 - if (window.mxchatTestPanelInstance) {
1037 - window.mxchatTestPanelInstance.handleTestingData(json.testing_data);
1038 - testingDataReceived = true;
1039 - }
1040 - }
1041 - // Handle content streaming
1042 - else if (json.content) {
1043 - streamingStarted = true;
1044 - accumulatedContent += json.content;
1045 - updateStreamingMessage(accumulatedContent, botId);
1046 - }
1047 - // Handle complete response in stream (fallback response)
1048 - else if (json.text || json.message || json.html) {
1049 - handleNonStreamResponse(json, callback, botId);
1050 - return;
1051 - }
1052 - // Handle errors
1053 - else if (json.error) {
1054 -
1055 - // Get error message from various possible fields
1056 - let errorMessage = json.error_message || json.message || json.text ||
1057 - (typeof json.error === 'string' ? json.error : 'An error occurred. Please try again.');
1058 -
1059 - // Re-enable chat input on error
1060 - enableChatInput(botId);
1061 -
1062 - // Display the error directly in the chat
1063 - replaceLastMessage("bot", errorMessage, '', [], botId);
1064 -
1065 - if (callback) {
1066 - callback(errorMessage);
1067 - }
1068 - return;
1069 - }
1070 - } catch (e) {
1071 - // SSE data parsing error - silently continue
1072 - }
1073 - }
1074 - }
1075 -
1076 - processStream();
1077 - }).catch(streamError => {
1078 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1079 - callMxChat(message, callback, botId);
1080 - });
1081 - }
1082 -
1083 - processStream();
1084 - })
1085 - .catch(error => {
1086 - // Check if we have server error data with chat mode
1087 - if (error && error.isServerError && error.data) {
1088 - // Check for chat mode in error data
1089 - if (error.data.chat_mode) {
1090 - updateChatModeIndicator(error.data.chat_mode, botId);
1091 - }
1092 -
1093 - handleNonStreamResponse(error.data, callback, botId);
1094 - } else {
1095 - // Only fall back to regular call if we don't have any response data
1096 - getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
1097 - callMxChat(message, callback, botId);
1098 - }
1099 - });
564 +// Sanitize only user input
565 +function sanitizeUserInput(text) {
566 + const div = document.createElement('div');
567 + div.textContent = text;
568 + return div.innerHTML;
1100 569 }
1101 570
1102 -// Helper function to handle non-streaming responses
1103 -function handleNonStreamResponse(data, callback, botId) {
1104 - botId = botId || 'default';
1105 -
1106 - // IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES
1107 - if (data.chat_mode) {
1108 - updateChatModeIndicator(data.chat_mode, botId);
1109 - }
1110 -
1111 - // Also check in data property if response is wrapped
1112 - if (data.data && data.data.chat_mode) {
1113 - updateChatModeIndicator(data.data.chat_mode, botId);
1114 - }
1115 -
1116 - // NOTE: Don't remove temporary message here - let replaceLastMessage handle it
1117 - // This prevents a visual gap between thinking dots disappearing and content appearing
1118 -
1119 - // SECURITY FIX: Check for errors FIRST
1120 - if (data.success === false || (data.data && data.data.error_message)) {
1121 - let errorMessage = "";
1122 - let errorCode = "";
1123 -
1124 - // Check various possible error locations
1125 - if (data.data && data.data.error_message) {
1126 - errorMessage = data.data.error_message;
1127 - errorCode = data.data.error_code || "";
1128 - } else if (data.error_message) {
1129 - errorMessage = data.error_message;
1130 - errorCode = data.error_code || "";
1131 - } else if (data.message) {
1132 - errorMessage = data.message;
1133 - } else if (typeof data.data === 'string') {
1134 - errorMessage = data.data;
1135 - } else {
1136 - errorMessage = "An error occurred. Please try again or contact support.";
1137 - }
1138 -
1139 - // Handle session reset action (IP changed, session expired, etc.)
1140 - // Silent reset — keep chat UI intact, just get a new session and retry
1141 - if (data.data && data.data.action === 'reset_session') {
1142 - MxChatInstances.silentResetSession(botId);
1143 - // Re-send the original message with the new session (user message is already displayed)
1144 - var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
1145 - if (originalMessage) {
1146 - getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1147 - var currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
1148 - if (shouldUseStreaming(currentModel)) {
1149 - callMxChatStream(originalMessage, callback, botId);
1150 - } else {
1151 - callMxChat(originalMessage, callback, botId);
1152 - }
1153 - }
1154 - return;
1155 - }
1156 -
1157 - // Format user-friendly error message
1158 - let displayMessage = errorMessage;
1159 - if (mxchatChat.is_admin) {
1160 - displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
1161 - }
1162 -
1163 - replaceLastMessage("bot", displayMessage, '', [], botId);
1164 -
1165 - if (callback) {
1166 - callback('');
1167 - }
1168 - return; // Exit early for errors
1169 - }
1170 -
1171 - // Check for live agent response
1172 - if (data.success && data.data && data.data.status === 'waiting_for_agent') {
1173 - removeThinkingDots(botId);
1174 - // Also remove any leftover bot-message that lost its temporary-message class
1175 - var $chatBox = getElement(botId, 'chat-box');
1176 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1177 - updateChatModeIndicator('agent', botId);
1178 - enableChatInput(botId);
1179 - if (callback) {
1180 - callback('');
1181 - }
1182 - return;
1183 - }
1184 -
1185 - // Handle different response formats
1186 - if (data.text || data.html || data.message) {
1187 -
1188 - // Apply response hooks
1189 - if (data.text && typeof customMxChatFilter === 'function') {
1190 - data.text = customMxChatFilter(data.text, "response");
1191 - }
1192 - if (data.message && typeof customMxChatFilter === 'function') {
1193 - data.message = customMxChatFilter(data.message, "response");
1194 - }
1195 -
1196 - // Display the response
1197 - if (data.text && data.html) {
1198 - replaceLastMessage("bot", data.text, data.html, [], botId);
1199 - } else if (data.text) {
1200 - replaceLastMessage("bot", data.text, '', [], botId);
1201 - } else if (data.html) {
1202 - replaceLastMessage("bot", "", data.html, [], botId);
1203 - } else if (data.message) {
1204 - replaceLastMessage("bot", data.message, '', [], botId);
1205 - }
1206 - }
1207 -
1208 - // Handle other response properties
1209 - if (data.data && data.data.filename) {
1210 - showActivePdf(data.data.filename, botId);
1211 - var instance = MxChatInstances.get(botId);
1212 - instance.activePdfFile = data.data.filename;
1213 - }
1214 -
1215 - if (data.redirect_url) {
1216 - setTimeout(() => {
1217 - window.location.href = data.redirect_url;
1218 - }, 1500);
1219 - }
1220 -
1221 - // Ensure chat input is re-enabled (safety net for edge cases)
1222 - enableChatInput(botId);
1223 -
1224 - if (callback) {
1225 - callback(data.text || data.message || '');
1226 - }
1227 -}
1228 -
1229 -// Enhanced updateChatModeIndicator function for immediate DOM updates
1230 -function updateChatModeIndicator(mode, botId) {
1231 - botId = botId || 'default';
1232 - const indicator = getElementDOM(botId, 'chat-mode-indicator');
1233 - if (indicator) {
1234 - const oldText = indicator.textContent;
1235 -
1236 - if (mode === 'agent') {
1237 - indicator.textContent = 'Live Agent';
1238 - startPolling(botId);
1239 - } else {
1240 - // Everything else is AI mode
1241 - const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1242 - indicator.textContent = customAiText;
1243 - stopPolling(botId);
1244 - }
1245 -
1246 - // Force immediate DOM update and reflow
1247 - if (oldText !== indicator.textContent) {
1248 - // Force a reflow to ensure the change is visible immediately
1249 - indicator.style.display = 'none';
1250 - indicator.offsetHeight; // Trigger reflow
1251 - indicator.style.display = '';
1252 -
1253 - // Double-check after a brief moment to ensure the change stuck
1254 - setTimeout(() => {
1255 - if (mode === 'agent' && indicator.textContent !== 'Live Agent') {
1256 - indicator.textContent = 'Live Agent';
1257 - } else if (mode !== 'agent' && indicator.textContent === 'Live Agent') {
1258 - const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1259 - indicator.textContent = customAiText;
1260 - }
1261 - }, 50);
1262 - }
1263 - }
1264 -}
1265 -
1266 -// Function to update message during streaming
1267 -function updateStreamingMessage(content, botId) {
1268 - botId = botId || 'default';
1269 -
1270 - // ADD RESPONSE HOOK FOR REAL-TIME STREAMING
1271 - if (typeof customMxChatFilter === 'function') {
1272 - content = customMxChatFilter(content, "response");
1273 - }
1274 -
1275 - const formattedContent = linkify(content);
1276 -
1277 - // Find the temporary message in this bot's chat box
1278 - var $chatBox = getElement(botId, 'chat-box');
1279 - const tempMessage = $chatBox.find('.bot-message.temporary-message').last();
1280 -
1281 - if (tempMessage.length) {
1282 - // Update existing message
1283 - tempMessage.html(formattedContent);
1284 - } else {
1285 - // Create new temporary message if it doesn't exist
1286 - appendMessage("bot", content, '', [], true, botId);
1287 - }
1288 -}
1289 -
1290 -function isStreamingSupported(model) {
1291 - if (!model) return false;
1292 -
1293 - const modelPrefix = model.split('-')[0].toLowerCase();
1294 -
1295 - // Support streaming for OpenAI, Claude, Grok, DeepSeek, and OpenRouter models
1296 - const isSupported = modelPrefix === 'gpt' ||
1297 - modelPrefix === 'o1' ||
1298 - modelPrefix === 'claude' ||
1299 - modelPrefix === 'grok' ||
1300 - modelPrefix === 'deepseek' ||
1301 - model === 'openrouter'; // Add this line - check full model name for OpenRouter
1302 -
1303 - return isSupported;
1304 -}
1305 -
1306 -// Update the event handlers to use the correct function names (using event delegation)
1307 -// Use class-based selectors for multi-instance support
1308 -$(document).on('click', '.send-button', function() {
1309 - var botId = getBotIdFromElement(this);
1310 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1311 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1312 - disableChatInput(botId);
1313 - }
1314 - sendMessage(botId);
1315 -});
1316 -
1317 -// Override enter key handler (using event delegation)
1318 -$(document).on('keypress', '.chat-input', function(e) {
1319 - if (e.which == 13 && !e.shiftKey) {
1320 - e.preventDefault();
1321 - var botId = getBotIdFromElement(this);
1322 - var modeIndicator = getElementDOM(botId, 'chat-mode-indicator');
1323 - if (!(modeIndicator && modeIndicator.textContent === 'Live Agent')) {
1324 - disableChatInput(botId);
1325 - }
1326 - sendMessage(botId);
1327 - }
1328 -});
1329 -
1330 -
1331 -// Tags the chat-box transcript so the print stylesheet can target it,
1332 -// and lazily initializes the header overflow menu for this bot if the
1333 -// markup is present but not yet wired (covers dynamically-rendered widgets).
1334 -// Idempotent; safe to call on every appended message.
1335 -function mxchatEnsurePrintRoot(botId) {
1336 - var $chatBox = getElement(botId, 'chat-box');
1337 - if (!$chatBox || !$chatBox.length) return;
1338 - $chatBox.addClass('mxchat-conversation-print-root');
1339 - if (!$chatBox.attr('data-print-title')) {
1340 - var nowStr = new Date().toLocaleString();
1341 - var headerTitle = ((typeof mxchatChat !== 'undefined' && mxchatChat.print_header_title) || 'Chat transcript') + ' — ' + nowStr;
1342 - $chatBox.attr('data-print-title', headerTitle);
1343 - }
1344 - if (typeof mxchatInitHeaderMenu === 'function') {
1345 - mxchatInitHeaderMenu(botId);
1346 - }
1347 -}
1348 -
1349 -// Builds the list of overflow-menu items for a given bot.
1350 -// Adding a future item is one push to this array — do NOT hardcode "only download."
1351 -function mxchatGetHeaderMenuItems(botId) {
1352 - var items = [];
1353 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1354 -
1355 - // The `print_button_*` keys still gate this item for back-compat with
1356 - // existing user options. The action is now a transcript download, not print.
1357 - if (settings.print_button_enabled === 'on') {
1358 - items.push({
1359 - id: 'download-transcript',
1360 - label: settings.print_button_label || 'Download Transcript',
1361 - icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
1362 - action: function() {
1363 - mxchatDownloadTranscript(botId);
1364 - }
1365 - });
1366 - }
1367 -
1368 - return items;
1369 -}
1370 -
1371 -// Builds a clean markdown transcript of the current conversation and triggers
1372 -// a file download. Used by the "Download Transcript" menu item.
1373 -function mxchatDownloadTranscript(botId) {
1374 - var $chatBox = getElement(botId, 'chat-box');
1375 - if (!$chatBox || !$chatBox.length) return;
1376 -
1377 - var settings = (typeof mxchatChat !== 'undefined') ? mxchatChat : {};
1378 - var headerTitle = settings.print_header_title || 'Chat transcript';
1379 - var now = new Date();
1380 - var stamp = now.toLocaleString();
1381 -
1382 - var lines = [];
1383 - lines.push('# ' + headerTitle);
1384 - lines.push('');
1385 - lines.push('Exported: ' + stamp);
1386 - lines.push('');
1387 - lines.push('---');
1388 - lines.push('');
1389 -
1390 - $chatBox.find('.user-message, .bot-message, .agent-message').each(function() {
1391 - var $msg = $(this);
1392 - // Skip thinking placeholders and any in-flight temporary messages.
1393 - if ($msg.find('.thinking-dots').length) return;
1394 - if ($msg.hasClass('temporary-message')) return;
1395 -
1396 - var sender;
1397 - if ($msg.hasClass('user-message')) sender = 'User';
1398 - else if ($msg.hasClass('agent-message')) sender = 'Live Agent';
1399 - else sender = 'AI Agent';
1400 -
1401 - // Strip interactive UI from the cloned message so we get the conversation text.
1402 - var $clone = $msg.clone();
1403 - $clone.find('.copy-button, .message-toolbar, .mxchat-copy, button, script, style').remove();
1404 - var text = $clone.text().replace(/ /g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
1405 - if (!text) return;
1406 -
1407 - lines.push('**' + sender + '**');
1408 - lines.push('');
1409 - lines.push(text);
1410 - lines.push('');
1411 - });
1412 -
1413 - var content = lines.join('\n');
1414 - var iso = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
1415 - var fname = 'mxchat-transcript-' + iso + '.md';
1416 - var blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1417 - var url = URL.createObjectURL(blob);
1418 - var a = document.createElement('a');
1419 - a.href = url;
1420 - a.download = fname;
1421 - a.style.display = 'none';
1422 - document.body.appendChild(a);
1423 - a.click();
1424 - setTimeout(function() {
1425 - if (a.parentNode) a.parentNode.removeChild(a);
1426 - URL.revokeObjectURL(url);
1427 - }, 100);
1428 -}
1429 -
1430 -// Reads the bot bubble's actual computed bg+fg and writes them as CSS vars
1431 -// on the menu wrap, so the dropdown matches whatever paints the bubble —
1432 -// saved options, AI theme CSS, or the mxchat-theme add-on.
1433 -function mxchatSyncMenuColors(botId, $wrap) {
1434 - if (!$wrap || !$wrap.length) return;
1435 - var $bot = $wrap.closest('.mxchat-chatbot-wrapper').find('.bot-message').not('.temporary-message').first();
1436 - if (!$bot.length) return;
1437 - var cs = window.getComputedStyle($bot[0]);
1438 - if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
1439 - $wrap[0].style.setProperty('--mxchat-menu-bg', cs.backgroundColor);
1440 - }
1441 - // Bot text color usually lives on a child div, not .bot-message itself.
1442 - var $textChild = $bot.find('[style*="color"]').first();
1443 - var fg = ($textChild.length ? window.getComputedStyle($textChild[0]).color : cs.color);
1444 - if (fg) $wrap[0].style.setProperty('--mxchat-menu-fg', fg);
1445 -}
1446 -
1447 -// One-time per-widget init: renders menu items, wires open/close,
1448 -// outside-click, Escape, and arrow-key navigation. If no items, hides the trigger.
1449 -function mxchatInitHeaderMenu(botId) {
1450 - var $wrap = $('.mxchat-header-menu-wrap[data-bot-id="' + botId + '"]').first();
1451 - if (!$wrap.length || $wrap.data('mxchatMenuReady')) return;
1452 -
1453 - var $trigger = $wrap.find('.mxchat-menu-trigger');
1454 - var $menu = $wrap.find('.mxchat-header-menu');
1455 - var items = mxchatGetHeaderMenuItems(botId);
1456 -
1457 - // Initial color sync — covers normal page load.
1458 - mxchatSyncMenuColors(botId, $wrap);
1459 -
1460 - if (!items.length) {
1461 - $trigger.hide();
1462 - $menu.hide();
1463 - $wrap.data('mxchatMenuReady', true);
1464 - return;
1465 - }
1466 -
1467 - // Build the menu items.
1468 - $menu.empty();
1469 - items.forEach(function(item, idx) {
1470 - var $btn = $('<button>', {
1471 - type: 'button',
1472 - 'class': 'mxchat-menu-item',
1473 - 'role': 'menuitem',
1474 - 'tabindex': '-1',
1475 - 'data-menu-id': item.id,
1476 - html: '<span class="mxchat-menu-item-icon">' + item.icon + '</span>' +
1477 - '<span class="mxchat-menu-item-label"></span>'
1478 - });
1479 - $btn.find('.mxchat-menu-item-label').text(item.label);
1480 - $btn.on('click', function(e) {
1481 - e.preventDefault();
1482 - e.stopPropagation();
1483 - closeMenu();
1484 - try { item.action(); } catch (err) { /* no-op */ }
1485 - });
1486 - $menu.append($btn);
1487 - });
1488 -
1489 - function openMenu() {
1490 - // Re-sync each open in case the active theme changed since init.
1491 - mxchatSyncMenuColors(botId, $wrap);
1492 - $menu.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1493 - $trigger.attr('aria-expanded', 'true');
1494 - // Focus the first item for keyboard users
1495 - setTimeout(function() {
1496 - $menu.find('.mxchat-menu-item').first().attr('tabindex', '0').trigger('focus');
1497 - }, 0);
1498 - }
1499 - function closeMenu(returnFocus) {
1500 - $menu.prop('hidden', true).attr('aria-hidden', 'true').removeClass('is-open');
1501 - $trigger.attr('aria-expanded', 'false');
1502 - $menu.find('.mxchat-menu-item').attr('tabindex', '-1');
1503 - if (returnFocus) $trigger.trigger('focus');
1504 - }
1505 -
1506 - // Toggle on trigger click — stop propagation so the .chatbot-top-bar
1507 - // click-to-collapse handler does not fire.
1508 - $trigger.on('click', function(e) {
1509 - e.preventDefault();
1510 - e.stopPropagation();
1511 - if ($menu.hasClass('is-open')) closeMenu();
1512 - else openMenu();
1513 - });
1514 -
1515 - // Don't let clicks inside the menu bubble to the top-bar collapse handler.
1516 - $menu.on('click', function(e) {
1517 - e.stopPropagation();
1518 - });
1519 -
1520 - // Outside click closes the menu.
1521 - $(document).on('click.mxchatMenu-' + botId, function(e) {
1522 - if (!$menu.hasClass('is-open')) return;
1523 - if ($wrap.has(e.target).length || $wrap.is(e.target)) return;
1524 - closeMenu();
1525 - });
1526 -
1527 - // Keyboard: Escape closes and returns focus; arrow keys move focus; Enter activates.
1528 - $menu.on('keydown', '.mxchat-menu-item', function(e) {
1529 - var $items = $menu.find('.mxchat-menu-item');
1530 - var idx = $items.index(this);
1531 - if (e.key === 'Escape') {
1532 - e.preventDefault();
1533 - closeMenu(true);
1534 - } else if (e.key === 'ArrowDown') {
1535 - e.preventDefault();
1536 - var $next = $items.eq((idx + 1) % $items.length);
1537 - $items.attr('tabindex', '-1');
1538 - $next.attr('tabindex', '0').trigger('focus');
1539 - } else if (e.key === 'ArrowUp') {
1540 - e.preventDefault();
1541 - var $prev = $items.eq((idx - 1 + $items.length) % $items.length);
1542 - $items.attr('tabindex', '-1');
1543 - $prev.attr('tabindex', '0').trigger('focus');
1544 - } else if (e.key === 'Enter' || e.key === ' ') {
1545 - e.preventDefault();
1546 - $(this).trigger('click');
1547 - }
1548 - });
1549 - $trigger.on('keydown', function(e) {
1550 - if (e.key === 'Escape' && $menu.hasClass('is-open')) {
1551 - e.preventDefault();
1552 - closeMenu(true);
1553 - } else if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && !$menu.hasClass('is-open')) {
1554 - e.preventDefault();
1555 - openMenu();
1556 - }
1557 - });
1558 -
1559 - $wrap.data('mxchatMenuReady', true);
1560 -}
1561 -
1562 -// Initialize header menus for every rendered widget on DOM ready.
1563 -$(function() {
1564 - $('.mxchat-header-menu-wrap').each(function() {
1565 - var botId = $(this).data('bot-id');
1566 - if (botId) mxchatInitHeaderMenu(botId);
1567 - });
1568 -});
1569 -
1570 -function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
571 +function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
1571 572 try {
1572 573 // Determine styles based on sender type
1573 574 let messageClass, bgColor, fontColor;
1574 575
@@ -1589,37 +590,34 @@
1589 590 }
1590 591
1591 592 const messageDiv = $('<div>')
1592 593 .addClass(messageClass)
1593 - .attr('dir', 'auto');
1594 -
1595 - // Only apply inline colors if AI theme is not active (let CSS handle it)
1596 - var skipColors = shouldSkipInlineColors(botId);
1597 - if (skipColors) {
1598 - messageDiv.css({
1599 - 'margin-bottom': '1em'
1600 - });
1601 - } else {
1602 - messageDiv.css({
594 + .attr('dir', 'auto') // Add dir="auto" for automatic text direction
595 + .css({
1603 596 'background': bgColor,
1604 597 'color': fontColor,
1605 598 'margin-bottom': '1em'
1606 599 });
600 +
601 + // Process the message content based on sender
602 + let fullMessage;
603 + if (sender === "user") {
604 + // For user messages, apply linkify after sanitization
605 + fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(messageText))));
606 + } else {
607 + // For bot/agent messages, preserve HTML
608 + fullMessage = messageText;
1607 609 }
1608 610
1609 - // Process the message content - always run linkify to convert markdown
1610 - // links and format text. linkify() handles existing HTML safely via
1611 - // negative lookaheads that skip URLs already inside <a> tags.
1612 - let fullMessage = linkify(messageText);
1613 -
1614 611 // Add images if provided
1615 612 if (images && images.length > 0) {
1616 - fullMessage += '<div class="image-gallery" dir="auto">';
613 + fullMessage += '<div class="image-gallery" dir="auto">'; // Add dir="auto" to image gallery
1617 614 images.forEach(img => {
615 + // Ensure image URLs and titles are properly escaped
1618 616 const safeTitle = sanitizeUserInput(img.title);
1619 617 const safeUrl = encodeURI(img.image_url);
1620 618 const safeThumbnail = encodeURI(img.thumbnail_url);
1621 -
619 +
1622 620 fullMessage += `
1623 621 <div style="margin-bottom: 10px;">
1624 622 <strong>${safeTitle}</strong><br>
1625 623 <a href="${safeUrl}" target="_blank">
@@ -1631,14 +629,9 @@
1631 629 }
1632 630
1633 631 // Append HTML content if provided
1634 632 if (messageHtml && sender !== "user") {
1635 - // Only add line breaks if there's actual text content before the HTML
1636 - if (fullMessage && fullMessage.trim()) {
1637 - fullMessage += '<br><br>' + messageHtml;
1638 - } else {
1639 - fullMessage = messageHtml;
1640 - }
633 + fullMessage += '<br><br>' + messageHtml;
1641 634 }
1642 635
1643 636 messageDiv.html(fullMessage);
1644 637
@@ -1645,98 +638,29 @@
1645 638 if (isTemporary) {
1646 639 messageDiv.addClass('temporary-message');
1647 640 }
1648 641
1649 - // Append to the correct chatbot instance's chat-box
1650 - var $chatBox = getElement(botId, 'chat-box');
1651 - messageDiv.hide().appendTo($chatBox).fadeIn(300, function() {
1652 - // FIXED: Use event delegation for link tracking
1653 - if (sender === "bot" || sender === "agent") {
1654 - attachLinkTracking(messageDiv, messageText, botId);
1655 - }
1656 -
642 + messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
1657 643 if (sender === "bot") {
1658 - const lastUserMessage = $chatBox.find('.user-message').last();
644 + const lastUserMessage = $('#chat-box').find('.user-message').last();
1659 645 if (lastUserMessage.length) {
1660 - scrollElementToTop(lastUserMessage, botId);
646 + scrollElementToTop(lastUserMessage);
1661 647 }
1662 648 }
1663 -
1664 - if ((sender === "bot" || sender === "agent") && !isTemporary) {
1665 - mxchatEnsurePrintRoot(botId);
1666 - }
1667 649 });
1668 650
1669 651 if (messageText.id) {
1670 - var instance = MxChatInstances.get(botId);
1671 - instance.lastSeenMessageId = messageText.id;
1672 - hideNotification(botId);
652 + lastSeenMessageId = messageText.id;
653 + hideNotification();
1673 654 }
1674 655 } catch (error) {
1675 - // Error rendering message - silently continue
656 + console.error("Error rendering message:", error);
1676 657 }
1677 658 }
1678 659
1679 -// Helper function to attach link tracking with proper event handling
1680 -function attachLinkTracking(messageDiv, messageText, botId) {
1681 - botId = botId || 'default';
1682 - // Use a slight delay to ensure DOM is ready
1683 - setTimeout(function() {
1684 - const links = messageDiv.find('a[href]').not('[data-tracked]');
1685 -
1686 - links.each(function() {
1687 - const $link = $(this);
1688 - const originalHref = $link.attr('href');
1689 -
1690 - // Mark as tracked to avoid duplicate handlers
1691 - $link.attr('data-tracked', 'true');
1692 -
1693 - // Only track external URLs
1694 - if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
1695 - // Remove any existing click handlers first
1696 - $link.off('click.tracking');
1697 -
1698 - // Add new click handler with namespace
1699 - $link.on('click.tracking', function(e) {
1700 - e.preventDefault();
1701 - e.stopPropagation();
1702 -
1703 - const messageContext = typeof messageText === 'string'
1704 - ? messageText.substring(0, 200)
1705 - : '';
1706 -
1707 - // Track the click
1708 - $.ajax({
1709 - url: mxchatChat.ajax_url,
1710 - type: 'POST',
1711 - data: {
1712 - action: 'mxchat_track_url_click',
1713 - session_id: getChatSession(botId),
1714 - url: originalHref,
1715 - message_context: messageContext,
1716 - nonce: mxchatChat.nonce
1717 - },
1718 - complete: function() {
1719 - // Always redirect, even if tracking fails
1720 - if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
1721 - window.open(originalHref, '_blank');
1722 - } else {
1723 - window.location.href = originalHref;
1724 - }
1725 - }
1726 - });
1727 -
1728 - return false; // Extra insurance to prevent default
1729 - });
1730 - }
1731 - });
1732 - }, 100); // Small delay to ensure DOM is ready
1733 -}
1734 -
1735 -function replaceLastMessage(sender, responseText, responseHtml = '', images = [], botId = 'default') {
660 +function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
1736 661 var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
1737 - var $chatBox = getElement(botId, 'chat-box');
1738 - var lastMessageDiv = $chatBox.find('.bot-message.temporary-message, .agent-message.temporary-message').last();
662 + var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1739 663
1740 664 // Determine styles
1741 665 let bgColor, fontColor;
1742 666 if (sender === "user") {
@@ -1749,24 +673,15 @@
1749 673 bgColor = botMessageBgColor;
1750 674 fontColor = botMessageFontColor;
1751 675 }
1752 676
1753 - // Always run linkify to convert markdown links and format text.
1754 - // linkify() already handles existing HTML (its URL patterns use negative lookaheads
1755 - // to avoid double-processing URLs that are already inside <a> tags).
1756 - var fullMessage = linkify(responseText);
1757 -
677 + var fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(responseText))));
1758 678 if (responseHtml) {
1759 - // Only add line breaks if there's actual text content before the HTML
1760 - if (fullMessage && fullMessage.trim()) {
1761 - fullMessage += '<br><br>' + responseHtml;
1762 - } else {
1763 - fullMessage = responseHtml;
1764 - }
679 + fullMessage += '<br><br>' + responseHtml;
1765 680 }
1766 681
1767 682 if (images.length > 0) {
1768 - fullMessage += '<div class="image-gallery" dir="auto">';
683 + fullMessage += '<div class="image-gallery" dir="auto">'; // Add dir="auto" to image gallery
1769 684 images.forEach(img => {
1770 685 fullMessage += `
1771 686 <div style="margin-bottom: 10px;">
1772 687 <strong>${img.title}</strong><br>
@@ -1778,672 +693,59 @@
1778 693 fullMessage += '</div>';
1779 694 }
1780 695
1781 696 if (lastMessageDiv.length) {
1782 - // Replace content immediately to prevent visual gap between thinking dots and response
1783 - lastMessageDiv
1784 - .html(fullMessage)
1785 - .removeClass('bot-message user-message temporary-message')
1786 - .addClass(messageClass)
1787 - .attr('dir', 'auto');
1788 -
1789 - // Only apply inline colors if AI theme is not active (let CSS handle it)
1790 - var skipColors = mxchatChat.skip_inline_colors || shouldSkipInlineColors(botId);
1791 - if (!skipColors) {
1792 - lastMessageDiv.css({
1793 - 'background-color': bgColor,
1794 - 'color': fontColor,
1795 - });
1796 - }
1797 -
1798 - // Handle link tracking and scroll
1799 - if (sender === "bot" || sender === "agent") {
1800 - attachLinkTracking(lastMessageDiv, responseText, botId);
1801 -
1802 - const lastUserMessage = $chatBox.find('.user-message').last();
1803 - if (lastUserMessage.length) {
1804 - scrollElementToTop(lastUserMessage, botId);
1805 - }
1806 - // Show notification if chat is hidden
1807 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
1808 - if ($floatingChatbot.hasClass('hidden')) {
1809 - showNotification(botId);
1810 - }
1811 - }
1812 -
1813 - // Re-enable chat input after response is displayed
1814 - enableChatInput(botId);
1815 -
1816 - if (sender === "bot" || sender === "agent") {
1817 - mxchatEnsurePrintRoot(botId);
1818 - }
697 + lastMessageDiv.fadeOut(200, function() {
698 + $(this)
699 + .html(fullMessage)
700 + .removeClass('bot-message user-message')
701 + .addClass(messageClass)
702 + .attr('dir', 'auto') // Add dir="auto" for automatic text direction
703 + .css({
704 + 'background-color': bgColor,
705 + 'color': fontColor,
706 + })
707 + .removeClass('temporary-message')
708 + .fadeIn(200, function() {
709 + if (sender === "bot" || sender === "agent") {
710 + const lastUserMessage = $('#chat-box').find('.user-message').last();
711 + if (lastUserMessage.length) {
712 + scrollElementToTop(lastUserMessage);
713 + }
714 + // Show notification if chat is hidden
715 + if ($('#floating-chatbot').hasClass('hidden')) {
716 + showNotification();
717 + }
718 + }
719 + });
720 + });
1819 721 } else {
1820 - appendMessage(sender, responseText, responseHtml, images, false, botId);
1821 - // Re-enable chat input after response is displayed
1822 - enableChatInput(botId);
722 + appendMessage(sender, responseText, responseHtml, images);
1823 723 }
1824 724 }
1825 725
1826 726
1827 - function appendThinkingMessage(botId) {
1828 - botId = botId || 'default';
1829 727
1830 - // Don't show thinking dots in live agent mode - message is just forwarded to a human
1831 - var indicator = getElementDOM(botId, 'chat-mode-indicator');
1832 - if (indicator && indicator.textContent === 'Live Agent') {
1833 - return;
1834 - }
1835 -
1836 - var $chatBox = getElement(botId, 'chat-box');
1837 -
1838 - // Remove any existing thinking dots in this bot's chat first
1839 - $chatBox.find('.thinking-dots').remove();
1840 -
1841 - // Check if we should skip inline colors (AI theme is active)
1842 - var skipColors = shouldSkipInlineColors(botId);
1843 -
1844 - // Retrieve the bot message font color and background color
1845 - var botMessageFontColor = mxchatChat.bot_message_font_color;
1846 - var botMessageBgColor = mxchatChat.bot_message_bg_color;
1847 -
1848 - // Build thinking dots HTML - skip inline colors if AI theme is active
1849 - var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
1850 - var thinkingHtml = '<div class="thinking-dots-container">' +
1851 - '<div class="thinking-dots">' +
1852 - '<span class="dot"' + dotStyle + '></span>' +
1853 - '<span class="dot"' + dotStyle + '></span>' +
1854 - '<span class="dot"' + dotStyle + '></span>' +
1855 - '</div>' +
1856 - '</div>';
1857 -
1858 - // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1859 - var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
1860 - $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1861 - scrollToBottom(botId);
1862 - }
1863 -
1864 - function removeThinkingDots(botId) {
1865 - botId = botId || 'default';
1866 - var $chatBox = getElement(botId, 'chat-box');
1867 - // Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
1868 - $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1869 - $chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
1870 - }
1871 -
1872 - // ====================================
1873 - // TEXT FORMATTING & PROCESSING
1874 - // ====================================
1875 -
1876 - function linkify(inputText) {
1877 - if (!inputText) {
1878 - return '';
1879 - }
1880 -
1881 - // Helper function to check if URL is already encoded
1882 - function isUrlEncoded(url) {
1883 - // Check for % followed by exactly 2 hex digits
1884 - return /%[0-9a-fA-F]{2}/.test(url);
1885 - }
1886 -
1887 - // Helper function to safely encode URLs only if needed
1888 - function safeEncodeUrl(url) {
1889 - // If URL already contains encoded characters, return as-is
1890 - if (isUrlEncoded(url)) {
1891 - return url;
1892 - }
1893 - // Otherwise, encode it
1894 - return encodeURI(url);
1895 - }
1896 -
1897 - // Process markdown headers FIRST
1898 - let processedText = formatMarkdownHeaders(inputText);
1899 -
1900 - // Process text styling (bold, italic, strikethrough)
1901 - processedText = formatTextStyling(processedText);
1902 -
1903 - // Process code blocks BEFORE processing links
1904 - processedText = formatCodeBlocks(processedText);
1905 -
1906 - // Process markdown tables BEFORE converting newlines to paragraphs
1907 - processedText = formatMarkdownTables(processedText);
1908 -
1909 - // NOW convert to paragraphs
1910 - processedText = convertNewlinesToBreaks(processedText);
1911 -
1912 - // IMPORTANT: Handle citation-style brackets FIRST [URL]
1913 - // This prevents them from being processed as markdown links
1914 - // Match [URL] where URL is a complete URL in square brackets (common in AI citations)
1915 - processedText = processedText.replace(/\[(https?:\/\/[^\]]+)\]/g, (match, url) => {
1916 - // Clean the URL of any trailing punctuation
1917 - let cleanUrl = url.replace(/[.,;!?]+$/, '');
1918 - const safeUrl = safeEncodeUrl(cleanUrl);
1919 - // Return as a proper link without the brackets
1920 - return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1921 - });
1922 -
1923 - // Process markdown links: [text](url) and [](url)
1924 - // Uses balanced parenthesis matching to handle URLs containing parens
1925 - // (e.g. PDF filenames with dates like (2025-08-28).pdf)
1926 - processedText = (function(input) {
1927 - var result = '';
1928 - var i = 0;
1929 - while (i < input.length) {
1930 - // Look for [ at current position
1931 - if (input[i] === '[') {
1932 - // Find closing ]
1933 - var closeBracket = input.indexOf(']', i + 1);
1934 - if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
1935 - result += input[i];
1936 - i++;
1937 - continue;
1938 - }
1939 - var linkText = input.substring(i + 1, closeBracket);
1940 - // Check if URL starts with http
1941 - var urlStart = closeBracket + 2;
1942 - if (!input.substring(urlStart).match(/^https?:\/\//)) {
1943 - result += input[i];
1944 - i++;
1945 - continue;
1946 - }
1947 - // Find balanced closing paren
1948 - var depth = 1;
1949 - var j = urlStart;
1950 - while (j < input.length && depth > 0) {
1951 - if (input[j] === '(') depth++;
1952 - else if (input[j] === ')') depth--;
1953 - if (depth > 0) j++;
1954 - }
1955 - if (depth !== 0) {
1956 - result += input[i];
1957 - i++;
1958 - continue;
1959 - }
1960 - var url = input.substring(urlStart, j);
1961 - var cleanUrl = url.replace(/[\].,;!?]+$/, '');
1962 - var encodedUrl = safeEncodeUrl(cleanUrl);
1963 - if (!linkText || !linkText.trim()) {
1964 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + cleanUrl + '</a>';
1965 - } else {
1966 - var safeText = sanitizeUserInput(linkText);
1967 - result += '<a href="' + encodedUrl + '" target="' + linkTarget + '">' + safeText + '</a>';
1968 - }
1969 - i = j + 1; // Skip past the closing )
1970 - } else {
1971 - result += input[i];
1972 - i++;
1973 - }
1974 - }
1975 - return result;
1976 - })(processedText);
1977 -
1978 - // Process phone numbers: [text](tel:number)
1979 - const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1980 - processedText = processedText.replace(phonePattern, (match, text, phone) => {
1981 - const safePhone = safeEncodeUrl(phone);
1982 - const safeText = sanitizeUserInput(text);
1983 - return `<a href="${safePhone}">${safeText}</a>`;
1984 - });
1985 -
1986 - // Process mailto links: [text](mailto:email)
1987 - const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
1988 - processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
1989 - const safeMailto = safeEncodeUrl(mailto);
1990 - const safeText = sanitizeUserInput(text);
1991 - return `<a href="${safeMailto}">${safeText}</a>`;
1992 - });
1993 -
1994 - // Process standalone URLs - but NOT if they're already in <a> tags or brackets
1995 - // Updated pattern to be more careful about what it matches
1996 - const urlPattern = /(^|[^">=\[\]])(https?:\/\/[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim;
1997 - processedText = processedText.replace(urlPattern, (match, prefix, url) => {
1998 - // Extra check: make sure this isn't already linked
1999 - if (match.includes('href=') || match.includes('</a>')) {
2000 - return match;
2001 - }
2002 -
2003 - // Clean trailing punctuation
2004 - let cleanUrl = url.replace(/[.,;!?)]+$/, '');
2005 - const safeUrl = safeEncodeUrl(cleanUrl);
2006 - return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2007 - });
2008 -
2009 - // Process www. URLs - but NOT if they're already in <a> tags or brackets
2010 - const wwwPattern = /(^|[^">/\[\]])(www\.[\S]+)(?![^<]*<\/a>)(?!\])/gim;
2011 - processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
2012 - // Extra check: make sure this isn't already linked
2013 - if (match.includes('href=') || match.includes('</a>')) {
2014 - return match;
2015 - }
2016 -
2017 - // Clean trailing punctuation
2018 - let cleanUrl = url.replace(/[.,;!?)]+$/, '');
2019 - const safeUrl = safeEncodeUrl(`http://${cleanUrl}`);
2020 - return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
2021 - });
2022 -
2023 - return processedText;
728 +function startPolling() {
729 + // Clear any existing interval first
730 + stopPolling();
731 + // Start new polling interval
732 + pollingInterval = setInterval(checkForAgentMessages, 5000);
733 + //console.log("Started agent message polling");
2024 734 }
2025 -
2026 - function formatMarkdownHeaders(text) {
2027 - // Handle h1 to h6 headers
2028 - return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
2029 - const level = hashes.length;
2030 - return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`;
2031 - });
2032 - }
2033 -
2034 -function formatTextStyling(text) {
2035 - // IMPORTANT: Protect BOTH HTML href and Markdown URLs from formatting
2036 - const protectedSegments = [];
2037 - let protectedText = text;
2038 -
2039 - // Step 1a: Protect HTML href="..." attributes
2040 - protectedText = protectedText.replace(/href\s*=\s*["']([^"']+)["']/gi, function(match) {
2041 - const placeholder = `__PROTECTED_${protectedSegments.length}__`;
2042 - protectedSegments.push(match);
2043 - return placeholder;
2044 - });
2045 -
2046 - // Step 1b: Protect Markdown links [text](url)
2047 - // This is crucial - we need to protect the URLs in markdown format
2048 - protectedText = protectedText.replace(/\[([^\]]*)\]\(([^)]+)\)/g, function(match) {
2049 - const placeholder = `__PROTECTED_${protectedSegments.length}__`;
2050 - protectedSegments.push(match);
2051 - return placeholder;
2052 - });
2053 -
2054 - // Step 1c: Also protect bare URLs that might exist
2055 - protectedText = protectedText.replace(/(https?:\/\/[^\s<>"]+)/gi, function(match) {
2056 - const placeholder = `__PROTECTED_${protectedSegments.length}__`;
2057 - protectedSegments.push(match);
2058 - return placeholder;
2059 - });
2060 -
2061 - // Step 2: Now apply text styling to the protected text
2062 - // Handle bold text (**text**)
2063 - protectedText = protectedText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
2064 735
2065 - // Handle italic text (*text* or _text_) - Safari-compatible (no lookbehind)
2066 - // Match single asterisks that aren't part of bold (**) by checking they're not followed/preceded by another *
2067 - protectedText = protectedText.replace(/(?!\*\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
2068 -
2069 - // Handle underscores for italic - Safari-compatible (no lookbehind)
2070 - // Exclude __PROTECTED_N__ placeholders by checking the content doesn't contain PROTECTED
2071 - protectedText = protectedText.replace(/(?!__)_((?!PROTECTED)[^_\n]+)_(?!_)/g, '<em>$1</em>');
2072 -
2073 - // Handle strikethrough (~~text~~)
2074 - protectedText = protectedText.replace(/~~(.*?)~~/g, '<del>$1</del>');
2075 -
2076 - // Step 3: Restore all protected segments
2077 - protectedSegments.forEach((original, index) => {
2078 - const placeholder = `__PROTECTED_${index}__`;
2079 - protectedText = protectedText.replace(placeholder, original);
2080 - });
2081 -
2082 - return protectedText;
2083 -}
2084 - function formatBoldText(text) {
2085 - // This function is kept for compatibility but now uses formatTextStyling
2086 - return formatTextStyling(text);
736 +function stopPolling() {
737 + if (pollingInterval) {
738 + clearInterval(pollingInterval);
739 + pollingInterval = null;
740 + //console.log("Stopped agent message polling");
2087 741 }
2088 -
2089 -function convertNewlinesToBreaks(text) {
2090 - // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
2091 - const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
2092 -
2093 - // Filter out empty paragraphs and wrap each paragraph in <p> tags
2094 - return paragraphs
2095 - .map(para => para.trim())
2096 - .filter(para => para.length > 0) // Remove empty paragraphs
2097 - .map(para => `<p>${para}</p>`)
2098 - .join('');
2099 742 }
2100 - function formatCodeBlocks(text) {
2101 - // Handle fenced code blocks with language specification (```language)
2102 - text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, language, code) => {
2103 - const lang = language || 'text';
2104 - const escapedCode = escapeHtml(code.trim());
2105 - return `<div class="mxchat-code-block-container">
2106 - <div class="mxchat-code-header">
2107 - <span class="mxchat-code-language">${lang}</span>
2108 - <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
2109 - </div>
2110 - <pre class="mxchat-code-block"><code class="language-${lang}">${escapedCode}</code></pre>
2111 - </div>`;
2112 - });
2113 743
2114 - // Handle inline code with single backticks
2115 - text = text.replace(/`([^`\n]+)`/g, '<code class="mxchat-inline-code">$1</code>');
2116 744
2117 - // Handle raw PHP tags (legacy support)
2118 - text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
2119 - const escapedCode = escapeHtml(match);
2120 - return `<div class="mxchat-code-block-container">
2121 - <div class="mxchat-code-header">
2122 - <span class="mxchat-code-language">php</span>
2123 - <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
2124 - </div>
2125 - <pre class="mxchat-code-block"><code class="language-php">${escapedCode}</code></pre>
2126 - </div>`;
2127 - });
2128 -
2129 - return text;
2130 - }
2131 -
2132 - function formatMarkdownTables(text) {
2133 - var lines = text.split('\n');
2134 - var result = [];
2135 - var i = 0;
2136 -
2137 - while (i < lines.length) {
2138 - // Check for a table: current line has pipes AND next line is a separator row
2139 - if (i + 1 < lines.length &&
2140 - lines[i].indexOf('|') !== -1 &&
2141 - /^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
2142 -
2143 - var tableLines = [];
2144 - var headerLine = lines[i];
2145 - var separatorLine = lines[i + 1];
2146 - tableLines.push(headerLine);
2147 - tableLines.push(separatorLine);
2148 -
2149 - // Collect remaining table rows
2150 - var j = i + 2;
2151 - while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
2152 - tableLines.push(lines[j]);
2153 - j++;
2154 - }
2155 -
2156 - // Parse alignment from separator row
2157 - var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
2158 - var alignments = sepCells.map(function(cell) {
2159 - var trimmed = cell.trim();
2160 - if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
2161 - if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
2162 - return 'left';
2163 - });
2164 -
2165 - // Build HTML table
2166 - var html = '<div class="mxchat-table-wrapper"><table class="mxchat-table">';
2167 -
2168 - // Header row
2169 - var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
2170 - html += '<thead><tr>';
2171 - headerCells.forEach(function(cell, idx) {
2172 - var align = alignments[idx] || 'left';
2173 - html += '<th style="text-align:' + align + '">' + cell.trim() + '</th>';
2174 - });
2175 - html += '</tr></thead>';
2176 -
2177 - // Body rows
2178 - html += '<tbody>';
2179 - for (var r = 2; r < tableLines.length; r++) {
2180 - var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
2181 - html += '<tr>';
2182 - rowCells.forEach(function(cell, idx) {
2183 - var align = alignments[idx] || 'left';
2184 - html += '<td style="text-align:' + align + '">' + cell.trim() + '</td>';
2185 - });
2186 - html += '</tr>';
2187 - }
2188 - html += '</tbody></table></div>';
2189 -
2190 - result.push(html);
2191 - i = j;
2192 - } else {
2193 - result.push(lines[i]);
2194 - i++;
2195 - }
2196 - }
2197 -
2198 - return result.join('\n');
2199 - }
2200 -
2201 - function sanitizeUserInput(text) {
2202 - const div = document.createElement('div');
2203 - div.textContent = text;
2204 - return div.innerHTML;
2205 - }
2206 -
2207 - function escapeHtml(unsafe) {
2208 - // Skip escaping if it's already escaped or contains HTML code block markup
2209 - if (unsafe.includes('&lt;') || unsafe.includes('&gt;') ||
2210 - unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
2211 - return unsafe;
2212 - }
2213 -
2214 - return unsafe
2215 - .replace(/&/g, "&amp;")
2216 - .replace(/</g, "&lt;")
2217 - .replace(/>/g, "&gt;")
2218 - .replace(/"/g, "&quot;")
2219 - .replace(/'/g, "&#039;");
2220 - }
2221 -
2222 - function decodeHTMLEntities(text) {
2223 - var textArea = document.createElement('textarea');
2224 - textArea.innerHTML = text;
2225 - return textArea.value;
2226 - }
2227 -
2228 - // ====================================
2229 - // UI & SCROLLING CONTROLS
2230 - // ====================================
2231 -
2232 - function scrollToBottom(botIdOrInstant, instant) {
2233 - // Handle backward compatibility: scrollToBottom() or scrollToBottom(true/false)
2234 - var botId = 'default';
2235 - if (typeof botIdOrInstant === 'string') {
2236 - botId = botIdOrInstant;
2237 - instant = instant || false;
2238 - } else if (typeof botIdOrInstant === 'boolean') {
2239 - instant = botIdOrInstant;
2240 - } else {
2241 - instant = false;
2242 - }
2243 -
2244 - var chatBox = getElement(botId, 'chat-box');
2245 - if (instant) {
2246 - // Instantly set the scroll position to the bottom
2247 - chatBox.scrollTop(chatBox.prop("scrollHeight"));
2248 - } else {
2249 - // Use requestAnimationFrame for smoother scrolling if needed
2250 - let start = null;
2251 - const scrollHeight = chatBox.prop("scrollHeight");
2252 - const initialScroll = chatBox.scrollTop();
2253 - const distance = scrollHeight - initialScroll;
2254 - const duration = 500; // Duration in ms
2255 -
2256 - function smoothScroll(timestamp) {
2257 - if (!start) start = timestamp;
2258 - const progress = timestamp - start;
2259 - const currentScroll = initialScroll + (distance * (progress / duration));
2260 - chatBox.scrollTop(currentScroll);
2261 -
2262 - if (progress < duration) {
2263 - requestAnimationFrame(smoothScroll);
2264 - } else {
2265 - chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
2266 - }
2267 - }
2268 -
2269 - requestAnimationFrame(smoothScroll);
2270 - }
2271 - }
2272 -
2273 - function scrollElementToTop(element, botId, topOffset) {
2274 - botId = botId || 'default';
2275 - topOffset = (typeof topOffset === 'number') ? topOffset : 2;
2276 - var chatBox = getElement(botId, 'chat-box');
2277 - var elementTop = element.position().top + chatBox.scrollTop();
2278 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
2279 - }
2280 -
2281 - function showChatWidget(botId) {
2282 - botId = botId || 'default';
2283 - var $button = getElement(botId, 'floating-chatbot-button');
2284 - // First ensure display is set
2285 - $button.css('display', 'flex');
2286 - // Then handle the fade
2287 - $button.fadeTo(500, 1);
2288 - // Force visibility
2289 - $button.removeClass('hidden');
2290 - }
2291 -
2292 - function hideChatWidget(botId) {
2293 - botId = botId || 'default';
2294 - var $button = getElement(botId, 'floating-chatbot-button');
2295 - $button.css('display', 'none');
2296 - $button.addClass('hidden');
2297 - }
2298 -
2299 - function disableScroll() {
2300 - if (isMobile()) {
2301 - $('body').css('overflow', 'hidden');
2302 - }
2303 - }
2304 -
2305 - function enableScroll() {
2306 - if (isMobile()) {
2307 - $('body').css('overflow', '');
2308 - }
2309 - }
2310 -
2311 - function isMobile() {
2312 - // This can be a simple check, or more sophisticated detection of mobile devices
2313 - return window.innerWidth <= 768; // Example threshold for mobile devices
2314 - }
2315 -
2316 - function setFullHeight() {
2317 - var vh = $(window).innerHeight() * 0.01;
2318 - $(':root').css('--vh', vh + 'px');
2319 - }
2320 -
2321 -
2322 - // ====================================
2323 - // NOTIFICATION SYSTEM
2324 - // ====================================
2325 -
2326 - function createNotificationBadge() {
2327 - const chatButton = document.getElementById('floating-chatbot-button');
2328 -
2329 - if (!chatButton) return;
2330 -
2331 - // Remove any existing badge first
2332 - const existingBadge = chatButton.querySelector('.chat-notification-badge');
2333 - if (existingBadge) {
2334 - existingBadge.remove();
2335 - }
2336 -
2337 - notificationBadge = document.createElement('div');
2338 - notificationBadge.className = 'chat-notification-badge';
2339 - notificationBadge.style.cssText = `
2340 - display: none;
2341 - position: absolute;
2342 - top: -5px;
2343 - right: -5px;
2344 - background-color: red;
2345 - color: white;
2346 - border-radius: 50%;
2347 - padding: 4px 8px;
2348 - font-size: 12px;
2349 - font-weight: bold;
2350 - z-index: 10001;
2351 - `;
2352 - chatButton.style.position = 'relative';
2353 - chatButton.appendChild(notificationBadge);
2354 -
2355 - }
2356 -
2357 - function showNotification(botId) {
2358 - botId = botId || 'default';
2359 - const badge = getElementDOM(botId, 'chat-notification-badge');
2360 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2361 - if (badge && $floatingChatbot.hasClass('hidden')) {
2362 - badge.style.display = 'block';
2363 - badge.textContent = '1';
2364 - }
2365 - }
2366 -
2367 - function hideNotification(botId) {
2368 - botId = botId || 'default';
2369 - const badge = getElementDOM(botId, 'chat-notification-badge');
2370 - if (badge) {
2371 - badge.style.display = 'none';
2372 - }
2373 - }
2374 -
2375 - function startNotificationChecking(botId) {
2376 - botId = botId || 'default';
2377 - const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2378 - if (!chatPersistenceEnabled) return;
2379 -
2380 - createNotificationBadge(botId);
2381 - var instance = MxChatInstances.get(botId);
2382 - instance.notificationCheckInterval = setInterval(function() {
2383 - checkForNewMessages(botId);
2384 - }, 30000); // Check every 30 seconds
2385 - }
2386 -
2387 - function stopNotificationChecking(botId) {
2388 - botId = botId || 'default';
2389 - var instance = MxChatInstances.get(botId);
2390 - if (instance.notificationCheckInterval) {
2391 - clearInterval(instance.notificationCheckInterval);
2392 - }
2393 - }
2394 -
2395 - function checkForNewMessages() {
2396 - const sessionId = getChatSession();
2397 - const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2398 -
2399 - if (!chatPersistenceEnabled) return;
2400 -
2401 - $.ajax({
2402 - url: mxchatChat.ajax_url,
2403 - type: 'POST',
2404 - data: {
2405 - action: 'mxchat_check_new_messages',
2406 - session_id: sessionId,
2407 - last_seen_id: lastSeenMessageId,
2408 - nonce: mxchatChat.nonce
2409 - },
2410 - success: function(response) {
2411 - if (response.success && response.data.hasNewMessages) {
2412 - showNotification();
2413 - }
2414 - }
2415 - });
2416 - }
2417 -
2418 -
2419 -// ====================================
2420 -// LIVE AGENT FUNCTIONALITY
2421 -// ====================================
2422 -
2423 -function startPolling(botId) {
2424 - botId = botId || 'default';
2425 - var instance = MxChatInstances.get(botId);
2426 - // Clear any existing interval first
2427 - stopPolling(botId);
2428 - instance.pollingInterval = setInterval(function() {
2429 - checkForAgentMessages(botId);
2430 - }, 5000);
2431 -}
2432 -
2433 -function stopPolling(botId) {
2434 - botId = botId || 'default';
2435 - var instance = MxChatInstances.get(botId);
2436 - if (instance.pollingInterval) {
2437 - clearInterval(instance.pollingInterval);
2438 - instance.pollingInterval = null;
2439 - }
2440 -}
2441 -
2442 -function checkForAgentMessages(botId) {
2443 - botId = botId || 'default';
2444 - var instance = MxChatInstances.get(botId);
2445 - const sessionId = getChatSession(botId);
745 +// Update your checkForAgentMessages function
746 +function checkForAgentMessages() {
747 + const sessionId = getChatSession();
2446 748 $.ajax({
2447 749 url: mxchatChat.ajax_url,
2448 750 type: 'POST',
2449 751 dataType: 'json',
@@ -2449,73 +751,43 @@
2449 751 dataType: 'json',
2450 752 data: {
2451 753 action: 'mxchat_fetch_new_messages',
2452 754 session_id: sessionId,
2453 - last_seen_id: instance.lastSeenMessageId,
2454 - persistence_enabled: 'true',
755 + last_seen_id: lastSeenMessageId,
2455 756 nonce: mxchatChat.nonce
2456 757 },
2457 758 success: function (response) {
2458 759 if (response.success && response.data?.new_messages) {
2459 760 let hasNewMessage = false;
2460 -
761 +
2461 762 response.data.new_messages.forEach(function (message) {
2462 - if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) {
763 + if (message.role === "agent" && !processedMessageIds.has(message.id)) {
2463 764 hasNewMessage = true;
2464 - appendMessage("agent", message.content, '', [], false, botId);
2465 - instance.lastSeenMessageId = message.id;
2466 - instance.processedMessageIds.add(message.id);
765 + replaceLastMessage("agent", message.content);
766 + lastSeenMessageId = message.id;
767 + processedMessageIds.add(message.id);
2467 768 }
2468 769 });
2469 770
2470 - if (hasNewMessage) {
2471 - enableChatInput(botId);
771 + if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
772 + showNotification();
2472 773 }
2473 -
2474 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2475 - if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2476 - showNotification(botId);
2477 - }
2478 -
2479 - scrollToBottom(botId, true);
774 +
775 + scrollToBottom(true);
2480 776 }
2481 -
2482 - // Handle chat mode transitions (e.g. agent ended chat via !endchat)
2483 - if (response.success && response.data?.chat_mode) {
2484 - updateChatModeIndicator(response.data.chat_mode, botId);
2485 - }
2486 777 },
2487 778 error: function (xhr, status, error) {
2488 - // Polling error - silently continue
779 + console.error("Polling error:", xhr, status, error);
2489 780 }
2490 781 });
2491 782 }
2492 783
2493 - // ====================================
2494 - // CHAT HISTORY & PERSISTENCE
2495 - // ====================================
2496 784
2497 -function loadChatHistory(botId, onComplete) {
2498 - botId = botId || 'default';
2499 - var instance = MxChatInstances.get(botId);
2500 785
2501 - // Prevent duplicate loading
2502 - if (instance.chatHistoryLoaded) {
2503 - if (onComplete) onComplete();
2504 - return;
2505 - }
2506 -
2507 - // Use getChatSession which returns null if no session exists (does NOT create one)
2508 - var sessionId = getChatSession(botId);
786 +function loadChatHistory() {
787 + var sessionId = getChatSession();
2509 788 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2510 789
2511 - // No session yet — nothing to load. History will load after first message via ensureSession.
2512 - if (!sessionId) {
2513 - instance.chatHistoryLoaded = true;
2514 - if (onComplete) onComplete();
2515 - return;
2516 - }
2517 -
2518 790 if (chatPersistenceEnabled && sessionId) {
2519 791 $.ajax({
2520 792 url: mxchatChat.ajax_url,
2521 793 type: 'POST',
@@ -2524,1280 +796,889 @@
2524 796 action: 'mxchat_fetch_conversation_history',
2525 797 session_id: sessionId
2526 798 },
2527 799 success: function(response) {
2528 - // Handle session reset (IP changed while user was away)
2529 - if (response.success === false && response.data && response.data.action === 'reset_session') {
2530 - // Silent reset — new session but don't clear UI
2531 - MxChatInstances.silentResetSession(botId);
2532 - instance.chatHistoryLoaded = true; // Prevent retry loop
2533 - if (onComplete) onComplete();
2534 - return;
2535 - }
800 + if (response.success && response.data && Array.isArray(response.data.conversation)) {
2536 801
2537 - // Check if the response indicates success
2538 - if (response.success) {
2539 - // Handle case where conversation data exists and is an array
2540 - if (response.data && Array.isArray(response.data.conversation)) {
2541 - var $chatBox = getElement(botId, 'chat-box');
2542 - var $fragment = $(document.createDocumentFragment());
2543 - let highestMessageId = instance.lastSeenMessageId;
2544 802
2545 - // Update chat mode if provided
2546 - if (response.data.chat_mode) {
2547 - updateChatModeIndicator(response.data.chat_mode, botId);
2548 - }
803 + var $chatBox = $('#chat-box');
804 + var $fragment = $(document.createDocumentFragment());
805 + let highestMessageId = lastSeenMessageId;
2549 806
2550 - // Only process if there are actual messages
2551 - if (response.data.conversation.length > 0) {
2552 - // IMPORTANT: Clear existing messages before loading history
2553 - $chatBox.empty();
807 + if (response.data.chat_mode) {
808 + updateChatModeIndicator(response.data.chat_mode);
809 + }
2554 810
2555 - $.each(response.data.conversation, function(index, message) {
2556 - // Skip agent messages if persistence is off
2557 - if (!chatPersistenceEnabled && message.role === 'agent') {
2558 - return;
2559 - }
811 + $.each(response.data.conversation, function(index, message) {
812 + // Skip agent messages if persistence is off
813 + if (!chatPersistenceEnabled && message.role === 'agent') {
814 + return;
815 + }
2560 816
2561 - var messageClass, messageBgColor, messageFontColor;
817 + var messageClass, messageBgColor, messageFontColor;
2562 818
2563 - switch (message.role) {
2564 - case 'user':
2565 - messageClass = 'user-message';
2566 - messageBgColor = userMessageBgColor;
2567 - messageFontColor = userMessageFontColor;
2568 - break;
2569 - case 'agent':
2570 - messageClass = 'agent-message';
2571 - messageBgColor = liveAgentMessageBgColor;
2572 - messageFontColor = liveAgentMessageFontColor;
2573 - break;
2574 - default:
2575 - messageClass = 'bot-message';
2576 - messageBgColor = botMessageBgColor;
2577 - messageFontColor = botMessageFontColor;
2578 - break;
2579 - }
819 + switch (message.role) {
820 + case 'user':
821 + messageClass = 'user-message';
822 + messageBgColor = userMessageBgColor;
823 + messageFontColor = userMessageFontColor;
824 + break;
825 + case 'agent':
826 + messageClass = 'agent-message';
827 + messageBgColor = liveAgentMessageBgColor;
828 + messageFontColor = liveAgentMessageFontColor;
829 + break;
830 + default:
831 + messageClass = 'bot-message';
832 + messageBgColor = botMessageBgColor;
833 + messageFontColor = botMessageFontColor;
834 + break;
835 + }
2580 836
2581 - var messageElement = $('<div>').addClass(messageClass)
2582 - .css({
2583 - 'background': messageBgColor,
2584 - 'color': messageFontColor
2585 - });
837 + var messageElement = $('<div>').addClass(messageClass)
838 + .css({
839 + 'background': messageBgColor,
840 + 'color': messageFontColor
841 + });
2586 842
2587 - var content = message.content;
2588 - content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2589 - content = decodeHTMLEntities(content);
843 + var content = message.content;
844 + content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
845 + content = decodeHTMLEntities(content);
2590 846
2591 - // Skip linkify for messages containing structured HTML
2592 - // (forms, product cards, galleries, etc.) to avoid
2593 - // markdown formatting corrupting HTML attributes
2594 - // (e.g. underscores in name="field_name" becoming <em> tags)
2595 - if (content.includes("mxchat-product-card") ||
2596 - content.includes("mxchat-image-gallery") ||
2597 - content.includes("mxchat-featured-products") ||
2598 - content.includes("<form") ||
2599 - content.includes("<input") ||
2600 - content.includes("<select") ||
2601 - content.includes("<textarea")) {
2602 - messageElement.html(content);
2603 - } else {
2604 - var formattedContent = linkify(content);
2605 - messageElement.html(formattedContent);
2606 - }
847 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
848 + messageElement.html(content);
849 + } else {
850 + var formattedContent = linkify(
851 + formatBoldText(
852 + convertNewlinesToBreaks(formatCodeBlocks(content))
853 + )
854 + );
855 + messageElement.html(formattedContent);
856 + }
2607 857
2608 - $fragment.append(messageElement);
858 + $fragment.append(messageElement);
2609 859
2610 - // Track message IDs
2611 - if (message.id) {
2612 - highestMessageId = Math.max(highestMessageId, message.id);
2613 - instance.processedMessageIds.add(message.id);
2614 - }
2615 - });
860 + // In loadChatHistory, change this part:
861 + if (message.id) {
862 + highestMessageId = Math.max(highestMessageId, message.id);
863 + processedMessageIds.add(message.id); // Add all message IDs to processed set
864 + }
865 + });
2616 866
2617 - // Only append messages and scroll if we have content
2618 - $chatBox.append($fragment);
2619 - scrollToBottom(botId, true);
867 + $chatBox.append($fragment);
868 + scrollToBottom(true);
2620 869
2621 - // Collapse quick questions if we have conversation history
2622 - // BUT skip auto-collapse for embedded bots (they should stay expanded)
2623 - if (hasQuickQuestions(botId) && !isEmbeddedBot(botId)) {
2624 - collapseQuickQuestions(botId);
2625 - }
870 + if (response.data.conversation.length > 0) {
871 + $('#mxchat-popular-questions').hide();
872 + }
2626 873
2627 - // Update lastSeenMessageId after history loads
2628 - instance.lastSeenMessageId = highestMessageId;
874 + // Update lastSeenMessageId after history loads
875 + lastSeenMessageId = highestMessageId;
2629 876
2630 - // Only update chat mode if persistence is enabled and we have messages
2631 - if (chatPersistenceEnabled) {
2632 - var lastMessage = response.data.conversation[response.data.conversation.length - 1];
2633 - if (lastMessage.role === 'agent') {
2634 - updateChatModeIndicator('agent', botId);
2635 - }
2636 - }
2637 -
2638 - // Mark as loaded ONLY after successful load
2639 - instance.chatHistoryLoaded = true;
877 + // Only update chat mode if persistence is enabled
878 + if (chatPersistenceEnabled && response.data.conversation.length > 0) {
879 + var lastMessage = response.data.conversation[response.data.conversation.length - 1];
880 + if (lastMessage.role === 'agent') {
881 + updateChatModeIndicator('agent');
2640 882 }
2641 883 }
884 + } else {
885 + console.warn("No conversation history found.");
2642 886 }
2643 - if (onComplete) onComplete();
2644 887 },
2645 888 error: function(xhr, status, error) {
2646 - // Error loading chat history - silently continue
2647 - if (onComplete) onComplete();
889 + console.error("Error loading chat history:", status, error);
890 + appendMessage("bot", "Unable to load chat history.");
2648 891 }
2649 892 });
2650 893 } else {
2651 - if (onComplete) onComplete();
894 + console.warn("Chat persistence is disabled or no session ID found. Not loading history.");
2652 895 }
2653 896 }
2654 897
898 +// Function to decode HTML entities
899 +function decodeHTMLEntities(text) {
900 + var textArea = document.createElement('textarea');
901 + textArea.innerHTML = text;
902 + return textArea.value;
903 +}
2655 904
2656 - // ====================================
2657 - // FILE UPLOAD FUNCTIONALITY
2658 - // ====================================
2659 -
2660 - function addSafeEventListener(elementId, eventType, handler) {
2661 - const element = document.getElementById(elementId);
2662 - if (element) {
2663 - element.addEventListener(eventType, handler);
2664 - }
905 +
906 +// Update formatCodeBlocks function
907 +function formatCodeBlocks(text) {
908 + // First handle raw PHP tags
909 + text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
910 + return `<pre><code class="language-php">${escapeHtml(match)}</code></pre>`;
911 + });
912 +
913 + // Then handle code blocks with backticks
914 + text = text.replace(/```php5?\n([\s\S]+?)```/gi, (match, code) => {
915 + return `<pre><code class="language-php">${escapeHtml(code)}</code></pre>`;
916 + });
917 +
918 + return text;
919 +}
920 +
921 +// Update escapeHtml function to preserve existing code blocks
922 +function escapeHtml(unsafe) {
923 + // First check if it's already a code block
924 + if (unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
925 + return unsafe;
2665 926 }
2666 927
2667 - function showActivePdf(filename, botId) {
2668 - botId = botId || 'default';
2669 - const container = getElementDOM(botId, 'active-pdf-container');
2670 - const nameElement = getElementDOM(botId, 'active-pdf-name');
928 + return unsafe
929 + .replace(/&/g, "&amp;")
930 + .replace(/</g, "&lt;")
931 + .replace(/>/g, "&gt;")
932 + .replace(/"/g, "&quot;")
933 + .replace(/'/g, "&#039;");
934 +}
935 +// Utility function to escape HTML
936 +function escapeHtml(unsafe) {
937 + return unsafe
938 + .replace(/&/g, "&amp;")
939 + .replace(/</g, "&lt;")
940 + .replace(/>/g, "&gt;")
941 + .replace(/"/g, "&quot;")
942 + .replace(/'/g, "&#039;");
943 +}
2671 944
2672 - if (!container || !nameElement) {
2673 - return;
2674 - }
2675 945
2676 - nameElement.textContent = filename;
2677 - container.style.display = 'flex';
2678 - }
2679 946
2680 - function showActiveWord(filename, botId) {
2681 - botId = botId || 'default';
2682 - const container = getElementDOM(botId, 'active-word-container');
2683 - const nameElement = getElementDOM(botId, 'active-word-name');
2684 947
2685 - if (!container || !nameElement) {
2686 - return;
2687 - }
948 +// Function to convert newlines, skipping preformatted text
949 +function convertNewlinesToBreaks(text) {
950 + // Split while preserving code blocks
951 + return text.split(/(<pre\b[^>]*>[\s\S]*?<\/pre>)/g).map(part => {
952 + if (part.startsWith('<pre')) return part;
953 + return part.replace(/(^|[^>])\n/g, '$1<br>');
954 + }).join('');
955 +}
2688 956
2689 - nameElement.textContent = filename;
2690 - container.style.display = 'flex';
2691 - }
2692 957
2693 - function removeActivePdf(botId) {
2694 - botId = botId || 'default';
2695 - var instance = MxChatInstances.get(botId);
2696 - const container = getElementDOM(botId, 'active-pdf-container');
2697 - const nameElement = getElementDOM(botId, 'active-pdf-name');
2698 958
2699 - if (!container || !nameElement || !instance.activePdfFile) return;
2700 959
2701 - fetch(mxchatChat.ajax_url, {
2702 - method: 'POST',
2703 - headers: {
2704 - 'Content-Type': 'application/x-www-form-urlencoded',
2705 - },
2706 - body: new URLSearchParams({
2707 - 'action': 'mxchat_remove_pdf',
2708 - 'session_id': getChatSession(botId),
2709 - 'nonce': mxchatChat.nonce
2710 - })
2711 - })
2712 - .then(response => response.json())
2713 - .then(data => {
2714 - if (data.success) {
2715 - container.style.display = 'none';
2716 - nameElement.textContent = '';
2717 - activePdfFile = null;
2718 - appendMessage('bot', 'PDF removed.');
2719 - }
2720 - })
2721 - .catch(error => {
2722 - // Error removing PDF - silently continue
2723 - });
960 + // Helper function to check if a string is an image HTML
961 + function isImageHtml(str) {
962 + return str.startsWith('<img') && str.endsWith('>');
2724 963 }
2725 964
2726 - function removeActiveWord() {
2727 - const container = document.getElementById('active-word-container');
2728 - const nameElement = document.getElementById('active-word-name');
2729 -
2730 - if (!container || !nameElement || !activeWordFile) return;
2731 -
2732 - fetch(mxchatChat.ajax_url, {
2733 - method: 'POST',
2734 - headers: {
2735 - 'Content-Type': 'application/x-www-form-urlencoded',
2736 - },
2737 - body: new URLSearchParams({
2738 - 'action': 'mxchat_remove_word',
2739 - 'session_id': sessionId,
2740 - 'nonce': mxchatChat.nonce
2741 - })
2742 - })
2743 - .then(response => response.json())
2744 - .then(data => {
2745 - if (data.success) {
2746 - container.style.display = 'none';
2747 - nameElement.textContent = '';
2748 - activeWordFile = null;
2749 - appendMessage('bot', 'Word document removed.');
2750 - }
2751 - })
2752 - .catch(error => {
2753 - // Error removing Word document - silently continue
2754 - });
965 + // Function to remove thinking dots
966 + function removeThinkingDots() {
967 + $('.thinking-dots').closest('.temporary-message').remove();
2755 968 }
2756 969
2757 - // ====================================
2758 - // CONSENT & COMPLIANCE (GDPR)
2759 - // ====================================
970 + function isMobile() {
971 + // This can be a simple check, or more sophisticated detection of mobile devices
972 + return window.innerWidth <= 768; // Example threshold for mobile devices
973 + }
2760 974
2761 - function initializeChatVisibility(botId) {
2762 - botId = botId || 'default';
2763 - const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
2764 - mxchatChat.complianz_toggle === '1' ||
2765 - mxchatChat.complianz_toggle === 1;
2766 -
2767 - if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
2768 - // Initial check
2769 - checkConsentAndShowChat(botId);
2770 -
2771 - // Listen for consent changes
2772 - $(document).on('cmplz_status_change', function(event) {
2773 - checkConsentAndShowChat(botId);
2774 - });
2775 - } else {
2776 - // If Complianz is not enabled, always show
2777 - getElement(botId, 'floating-chatbot-button')
2778 - .css('display', 'flex')
2779 - .removeClass('hidden no-consent')
2780 - .fadeTo(500, 1);
2781 -
2782 - // Also check pre-chat message when Complianz is not enabled
2783 - checkPreChatDismissal(botId);
975 + function disableScroll() {
976 + if (isMobile()) {
977 + $('body').css('overflow', 'hidden');
2784 978 }
2785 979 }
2786 980
2787 -
2788 - function checkConsentAndShowChat(botId) {
2789 - botId = botId || 'default';
2790 - var consentStatus = cmplz_has_consent('marketing');
2791 - var consentType = complianz.consenttype;
2792 -
2793 - let $widget = getElement(botId, 'floating-chatbot-button');
2794 - let $chatbot = getElement(botId, 'floating-chatbot');
2795 - let $preChat = getElement(botId, 'pre-chat-message');
2796 -
2797 - if (consentStatus === true) {
2798 - $widget
2799 - .removeClass('no-consent')
2800 - .css('display', 'flex')
2801 - .removeClass('hidden')
2802 - .fadeTo(500, 1);
2803 - $chatbot.removeClass('no-consent');
2804 -
2805 - // Show pre-chat message if not dismissed
2806 - checkPreChatDismissal(botId);
2807 - } else {
2808 - $widget
2809 - .addClass('no-consent')
2810 - .fadeTo(500, 0, function() {
2811 - $(this)
2812 - .css('display', 'none')
2813 - .addClass('hidden');
2814 - });
2815 - $chatbot.addClass('no-consent');
2816 -
2817 - // Hide pre-chat message when no consent
2818 - $preChat.hide();
981 + function enableScroll() {
982 + if (isMobile()) {
983 + $('body').css('overflow', '');
2819 984 }
2820 985 }
2821 986
2822 -
2823 - // ====================================
2824 - // PRE-CHAT MESSAGE HANDLING
2825 - // ====================================
2826 -
2827 - function checkPreChatDismissal(botId) {
2828 - botId = botId || 'default';
2829 - try {
2830 - var dismissedAt = localStorage.getItem('mxchat_pre_chat_dismissed_' + botId);
2831 - if (dismissedAt) {
2832 - // Re-show after 24 hours
2833 - var elapsed = Date.now() - parseInt(dismissedAt, 10);
2834 - if (elapsed < 86400000) {
2835 - getElement(botId, 'pre-chat-message').hide();
2836 - return;
987 +// Pre-chat dismissal check function (wrapped in a function for reuse)
988 + function checkPreChatDismissal() {
989 + $.ajax({
990 + url: mxchatChat.ajax_url,
991 + type: 'POST',
992 + data: {
993 + action: 'mxchat_check_pre_chat_message_status',
994 + _ajax_nonce: mxchatChat.nonce
995 + },
996 + success: function(response) {
997 + if (response.success && !response.data.dismissed) {
998 + $('#pre-chat-message').fadeIn(250);
999 + } else {
1000 + $('#pre-chat-message').hide();
2837 1001 }
2838 - // Expired — clear and show again
2839 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
1002 + },
1003 + error: function() {
1004 + console.error('Failed to check pre-chat message dismissal status.');
2840 1005 }
2841 - getElement(botId, 'pre-chat-message').fadeIn(250);
2842 - } catch (e) {
2843 - // localStorage unavailable — show the message
2844 - getElement(botId, 'pre-chat-message').fadeIn(250);
2845 - }
1006 + });
2846 1007 }
1008 +
1009 + // Function to show the chatbot widget
1010 +function showChatWidget() {
1011 + // First ensure display is set
1012 + $('#floating-chatbot-button').css('display', 'flex');
1013 + // Then handle the fade
1014 + $('#floating-chatbot-button').fadeTo(500, 1);
1015 + // Force visibility
1016 + $('#floating-chatbot-button').removeClass('hidden');
1017 + //console.log('Showing widget');
1018 +}
2847 1019
2848 - function handlePreChatDismissal(botId) {
2849 - botId = botId || 'default';
2850 - getElement(botId, 'pre-chat-message').fadeOut(200);
2851 - try {
2852 - localStorage.setItem('mxchat_pre_chat_dismissed_' + botId, String(Date.now()));
2853 - } catch (e) {
2854 - // localStorage unavailable — dismissal won't persist
2855 - }
2856 - }
1020 +// Function to hide the chatbot widget
1021 +function hideChatWidget() {
1022 + $('#floating-chatbot-button').css('display', 'none');
1023 + $('#floating-chatbot-button').addClass('hidden');
1024 + //console.log('Hiding widget');
1025 +}
2857 1026
1027 +function initializeChatVisibility() {
1028 + //console.log('Initializing chat visibility');
1029 + const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
1030 + mxchatChat.complianz_toggle === '1' ||
1031 + mxchatChat.complianz_toggle === 1;
2858 1032
2859 - // ====================================
2860 - // UTILITY FUNCTIONS
2861 - // ====================================
2862 -
2863 - function copyToClipboard(text) {
2864 - var tempInput = $('<input>');
2865 - $('body').append(tempInput);
2866 - tempInput.val(text).select();
2867 - document.execCommand('copy');
2868 - tempInput.remove();
2869 - }
1033 + if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
1034 + // Initial check
1035 + checkConsentAndShowChat();
2870 1036
2871 -
2872 - function isImageHtml(str) {
2873 - return str.startsWith('<img') && str.endsWith('>');
1037 + // Listen for consent changes
1038 + $(document).on('cmplz_status_change', function(event) {
1039 + //console.log('Status change detected');
1040 + checkConsentAndShowChat();
1041 + });
1042 + } else {
1043 + // If Complianz is not enabled, always show
1044 + $('#floating-chatbot-button')
1045 + .css('display', 'flex')
1046 + .removeClass('hidden no-consent')
1047 + .fadeTo(500, 1);
1048 +
1049 + // Also check pre-chat message when Complianz is not enabled
1050 + checkPreChatDismissal();
2874 1051 }
1052 +}
2875 1053
2876 1054
2877 - // ====================================
2878 - // EVENT HANDLERS & INITIALIZATION
2879 - // ====================================
2880 1055
2881 -$(document).on('click', '.mxchat-popular-question', function () {
2882 - var question = $(this).text();
2883 - var botId = getBotIdFromElement(this);
1056 +function checkConsentAndShowChat() {
1057 + var consentStatus = cmplz_has_consent('marketing');
1058 + var consentType = complianz.consenttype;
1059 +
1060 + //console.log('Checking consent:', {status: consentStatus,type: consentType});
2884 1061
2885 - // Append the question as if the user typed it
2886 - appendMessage("user", question, '', [], false, botId);
1062 + let $widget = $('#floating-chatbot-button');
1063 + let $chatbot = $('#floating-chatbot');
1064 + let $preChat = $('#pre-chat-message');
1065 +
1066 + if (consentStatus === true) {
1067 + //console.log('Consent granted - showing widget');
1068 + $widget
1069 + .removeClass('no-consent')
1070 + .css('display', 'flex')
1071 + .removeClass('hidden')
1072 + .fadeTo(500, 1);
1073 + $chatbot.removeClass('no-consent');
1074 +
1075 + // Show pre-chat message if not dismissed
1076 + checkPreChatDismissal();
1077 + } else {
1078 + //console.log('No consent - hiding widget');
1079 + $widget
1080 + .addClass('no-consent')
1081 + .fadeTo(500, 0, function() {
1082 + $(this)
1083 + .css('display', 'none')
1084 + .addClass('hidden');
1085 + });
1086 + $chatbot.addClass('no-consent');
1087 +
1088 + // Hide pre-chat message when no consent
1089 + $preChat.hide();
1090 + }
1091 +}
2887 1092
2888 - // Only collapse if there are questions
2889 - if (hasQuickQuestions(botId)) {
2890 - collapseQuickQuestions(botId);
1093 + // Function to dismiss pre-chat message for 24 hours
1094 + function handlePreChatDismissal() {
1095 + $('#pre-chat-message').fadeOut(200);
1096 + $.ajax({
1097 + url: mxchatChat.ajax_url,
1098 + type: 'POST',
1099 + data: {
1100 + action: 'mxchat_dismiss_pre_chat_message',
1101 + _ajax_nonce: mxchatChat.nonce
1102 + },
1103 + success: function() {
1104 + $('#pre-chat-message').hide();
1105 + },
1106 + error: function() {
1107 + console.error('Failed to dismiss pre-chat message.');
1108 + }
1109 + });
2891 1110 }
2892 1111
2893 - // Send the question to the server
2894 - sendMessageToChatbot(question, botId);
2895 -});
1112 + // Handle pre-chat message dismissal on button click
1113 + $(document).on('click', '.close-pre-chat-message', function(e) {
1114 + e.stopPropagation();
1115 + handlePreChatDismissal();
1116 + });
2896 1117
2897 -$(document).on('click', '.questions-toggle-btn', function(e) {
2898 - e.preventDefault();
2899 - e.stopPropagation();
2900 - var botId = getBotIdFromElement(this);
2901 - expandQuickQuestions(botId);
2902 -});
2903 -
2904 -$(document).on('click', '.questions-collapse-btn', function(e) {
2905 - e.preventDefault();
2906 - e.stopPropagation();
2907 - var botId = getBotIdFromElement(this);
2908 - collapseQuickQuestions(botId);
2909 -});
2910 -
2911 - // Chatbot visibility toggle handlers - use class selector for multi-instance support
2912 - $(document).on('click', '.floating-chatbot-button', function() {
2913 - var botId = getBotIdFromElement(this);
2914 - var $chatbot = getElement(botId, 'floating-chatbot');
2915 - var $badge = getElement(botId, 'chat-notification-badge');
2916 - var $preChat = getElement(botId, 'pre-chat-message');
2917 -
2918 - if ($chatbot.hasClass('hidden')) {
2919 - $chatbot.removeClass('hidden').addClass('visible');
1118 + // Toggle chatbot visibility on floating button click
1119 + $(document).on('click', '#floating-chatbot-button', function() {
1120 + var chatbot = $('#floating-chatbot');
1121 + if (chatbot.hasClass('hidden')) {
1122 + chatbot.removeClass('hidden').addClass('visible');
2920 1123 $(this).addClass('hidden');
2921 - $badge.hide(); // Hide notification when opening chat
1124 + $('#chat-notification-badge').hide(); // Hide notification when opening chat
2922 1125 disableScroll();
2923 - $preChat.fadeOut(250);
2924 -
2925 - // Load chat history for returning visitors (persistence)
2926 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
2927 - if (chatPersistenceEnabled) {
2928 - MxChatInstances.ensureSession(botId);
2929 - }
2930 -
2931 - // Deferred email check — only on first widget open
2932 - var emailBlocker = getElementDOM(botId, 'email-blocker');
2933 - var instance = MxChatInstances.get(botId);
2934 - if (emailBlocker && !instance.emailCheckDone) {
2935 - instance.emailCheckDone = true;
2936 - resolveEmailState(botId);
2937 - } else if (!emailBlocker) {
2938 - // No email collection — still route through showChatContainerForBot
2939 - // so the loader is shown while chat history loads
2940 - showChatContainerForBot(botId);
2941 - }
1126 + $('#pre-chat-message').fadeOut(250);
2942 1127 } else {
2943 - $chatbot.removeClass('visible').addClass('hidden');
1128 + chatbot.removeClass('visible').addClass('hidden');
2944 1129 $(this).removeClass('hidden');
2945 1130 enableScroll();
2946 - checkPreChatDismissal(botId);
1131 + checkPreChatDismissal();
2947 1132 }
2948 1133 });
2949 1134
2950 - // Allow clicking anywhere on the title bar to close the chatbot
2951 - $(document).on('click', '.chatbot-top-bar', function() {
2952 - var botId = getBotIdFromElement(this);
2953 - getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2954 - getElement(botId, 'floating-chatbot-button').removeClass('hidden');
1135 + $(document).on('click', '#exit-chat-button', function() {
1136 + $('#floating-chatbot').addClass('hidden').removeClass('visible');
1137 + $('#floating-chatbot-button').removeClass('hidden');
2955 1138 enableScroll();
2956 1139 });
2957 1140
1141 + // Close pre-chat message on click
2958 1142 $(document).on('click', '.close-pre-chat-message', function(e) {
2959 1143 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2960 - var botId = getBotIdFromElement(this);
2961 - handlePreChatDismissal(botId);
1144 + $('#pre-chat-message').fadeOut(200, function() {
1145 + $(this).remove();
1146 + });
2962 1147 });
2963 1148
1149 + // Open chatbot when pre-chat message is clicked
1150 + $(document).on('click', '#pre-chat-message', function() {
1151 + var chatbot = $('#floating-chatbot');
1152 + if (chatbot.hasClass('hidden')) {
1153 + chatbot.removeClass('hidden').addClass('visible');
1154 + $('#floating-chatbot-button').addClass('hidden');
1155 + $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
1156 + disableScroll(); // Disable scroll when chatbot opens
1157 + }
1158 + });
2964 1159
2965 - // PDF upload button handlers - use class selector
2966 - $(document).on('click', '.pdf-upload-btn', function() {
2967 - var botId = getBotIdFromElement(this);
2968 - var pdfInput = getElementDOM(botId, 'pdf-upload');
2969 - if (pdfInput) pdfInput.click();
2970 - });
1160 + // If the chatbot is initially hidden, ensure the button is visible
1161 + if ($('#floating-chatbot').hasClass('hidden')) {
1162 + $('#floating-chatbot-button').removeClass('hidden');
1163 + }
2971 1164
2972 - // Word upload button handlers - use class selector
2973 - $(document).on('click', '.word-upload-btn', function() {
2974 - var botId = getBotIdFromElement(this);
2975 - var wordInput = getElementDOM(botId, 'word-upload');
2976 - if (wordInput) wordInput.click();
2977 - });
2978 -
2979 - // PDF file input change handler
2980 - addSafeEventListener('pdf-upload', 'change', async function(e) {
2981 - const file = e.target.files[0];
2982 -
2983 - if (!file || file.type !== 'application/pdf') {
2984 - alert('Please select a valid PDF file.');
2985 - return;
2986 - }
2987 -
2988 - if (!sessionId) {
2989 - alert('Error: No session ID found');
2990 - return;
2991 - }
1165 + function setFullHeight() {
1166 + var vh = $(window).innerHeight() * 0.01;
1167 + $(':root').css('--vh', vh + 'px');
1168 + }
2992 1169
2993 - if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
2994 - alert('Error: Ajax configuration missing');
2995 - return;
2996 - }
2997 -
2998 - // Disable buttons and show loading state
2999 - const uploadBtn = document.getElementById('pdf-upload-btn');
3000 - const sendBtn = document.getElementById('send-button');
3001 - const originalBtnContent = uploadBtn.innerHTML;
3002 -
3003 - try {
3004 - const formData = new FormData();
3005 - formData.append('action', 'mxchat_upload_pdf');
3006 - formData.append('pdf_file', file);
3007 - formData.append('session_id', sessionId);
3008 - formData.append('nonce', mxchatChat.nonce);
3009 -
3010 - uploadBtn.disabled = true;
3011 - sendBtn.disabled = true;
3012 - uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3013 - <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3014 - </svg>`;
3015 -
3016 - const response = await fetch(mxchatChat.ajax_url, {
3017 - method: 'POST',
3018 - body: formData
3019 - });
3020 -
3021 - const data = await response.json();
3022 -
3023 - if (data.success) {
3024 - // Hide popular questions if they exist
3025 - const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
3026 - if (hasQuickQuestions()) {
3027 - collapseQuickQuestions();
3028 - }
3029 -
3030 - // Show the active PDF name
3031 - showActivePdf(data.data.filename);
3032 -
3033 - appendMessage('bot', data.data.message);
3034 - scrollToBottom();
3035 - activePdfFile = data.data.filename;
3036 - } else {
3037 - alert('Failed to upload PDF. Please try again.');
3038 - }
3039 - } catch (error) {
3040 - alert('Error uploading file. Please try again.');
3041 - } finally {
3042 - uploadBtn.disabled = false;
3043 - sendBtn.disabled = false;
3044 - uploadBtn.innerHTML = originalBtnContent;
3045 - this.value = ''; // Reset file input
3046 - }
3047 - });
3048 -
3049 - // Word file input change handler
3050 - addSafeEventListener('word-upload', 'change', async function(e) {
3051 - const file = e.target.files[0];
3052 -
3053 - if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
3054 - alert('Please select a valid Word document (.docx).');
3055 - return;
3056 - }
3057 -
3058 - if (!sessionId) {
3059 - alert('Error: No session ID found');
3060 - return;
3061 - }
1170 + // Set the height when the page loads
3062 1171
3063 - // Disable buttons and show loading state
3064 - const uploadBtn = document.getElementById('word-upload-btn');
3065 - const sendBtn = document.getElementById('send-button');
3066 - const originalBtnContent = uploadBtn.innerHTML;
3067 -
3068 - try {
3069 - const formData = new FormData();
3070 - formData.append('action', 'mxchat_upload_word');
3071 - formData.append('word_file', file);
3072 - formData.append('session_id', sessionId);
3073 - formData.append('nonce', mxchatChat.nonce);
3074 -
3075 - uploadBtn.disabled = true;
3076 - sendBtn.disabled = true;
3077 - uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
3078 - <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
3079 - </svg>`;
3080 -
3081 - const response = await fetch(mxchatChat.ajax_url, {
3082 - method: 'POST',
3083 - body: formData
3084 - });
3085 -
3086 - const data = await response.json();
3087 -
3088 - if (data.success) {
3089 - // Hide popular questions if they exist
3090 - const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
3091 - if (hasQuickQuestions()) {
3092 - collapseQuickQuestions();
3093 - }
3094 -
3095 - // Show the active Word document name
3096 - showActiveWord(data.data.filename);
3097 -
3098 - appendMessage('bot', data.data.message);
3099 - scrollToBottom();
3100 - activeWordFile = data.data.filename;
3101 - } else {
3102 - alert('Failed to upload Word document. Please try again.');
3103 - }
3104 - } catch (error) {
3105 - alert('Error uploading file. Please try again.');
3106 - } finally {
3107 - uploadBtn.disabled = false;
3108 - sendBtn.disabled = false;
3109 - uploadBtn.innerHTML = originalBtnContent;
3110 - this.value = ''; // Reset file input
3111 - }
3112 - });
3113 -
3114 - // Remove button click handlers
3115 - document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
3116 - e.preventDefault();
3117 - e.stopPropagation();
3118 - removeActivePdf();
3119 - });
3120 -
3121 - document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
3122 - e.preventDefault();
3123 - e.stopPropagation();
3124 - removeActiveWord();
3125 - });
3126 -
3127 - // Window resize handlers
1172 +
1173 + // Set the height on resize and orientation change events
3128 1174 $(window).on('resize orientationchange', function() {
3129 1175 setFullHeight();
3130 1176 });
3131 1177
3132 1178
3133 - // ====================================
3134 - // TOOLBAR & STYLING SETUP
3135 - // ====================================
3136 -
3137 - // Apply toolbar settings
3138 - if (mxchatChat.chat_toolbar_toggle === 'on') {
3139 - $('.chat-toolbar').show();
3140 - } else {
3141 - $('.chat-toolbar').hide();
1179 + // Now handle the close button to dismiss the pre-chat message for 24 hours
1180 + var closeButton = document.querySelector('.close-pre-chat-message');
1181 + if (closeButton) {
1182 + closeButton.addEventListener('click', function() {
1183 + $('#pre-chat-message').fadeOut(200); // Hide the message
1184 +
1185 + // Send an AJAX request to set the transient flag for 24 hours
1186 + $.ajax({
1187 + url: mxchatChat.ajax_url,
1188 + type: 'POST',
1189 + data: {
1190 + action: 'mxchat_dismiss_pre_chat_message',
1191 + _ajax_nonce: mxchatChat.nonce
1192 + },
1193 + success: function() {
1194 + //console.log('Pre-chat message dismissed for 24 hours.');
1195 +
1196 + // Ensure the message is hidden after dismissal
1197 + $('#pre-chat-message').hide();
1198 + },
1199 + error: function() {
1200 + //console.error('Failed to dismiss pre-chat message.');
1201 + }
1202 + });
1203 + });
3142 1204 }
3143 -
3144 - // Apply toolbar icon colors
3145 - const toolbarElements = [
3146 - '#mxchat-chatbot .toolbar-btn svg',
3147 - '#mxchat-chatbot .active-pdf-name',
3148 - '#mxchat-chatbot .active-word-name',
3149 - '#mxchat-chatbot .remove-pdf-btn svg',
3150 - '#mxchat-chatbot .remove-word-btn svg',
3151 - '#mxchat-chatbot .toolbar-perplexity svg'
3152 - ];
3153 -
3154 - toolbarElements.forEach(selector => {
3155 - $(selector).css({
3156 - 'fill': toolbarIconColor,
3157 - 'stroke': toolbarIconColor,
3158 - 'color': toolbarIconColor
3159 - });
3160 - });
3161 1205
3162 1206
3163 -// ====================================
3164 -// INIT LOADER & CHAT CONTAINER HELPERS
3165 -// ====================================
3166 -// These must be outside the email collection block so they're always available
3167 -// (used by persistence loading even when email collection is off)
3168 1207
3169 -function showInitLoader(botId) {
3170 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3171 - if (loader) loader.style.display = 'flex';
3172 -}
3173 1208
3174 -function hideInitLoader(botId) {
3175 - var loader = getElementDOM(botId, 'mxchat-init-loader');
3176 - if (loader) loader.style.display = 'none';
1209 +// Event listener for Add to Cart button
1210 +$(document).on('click', '.mxchat-add-to-cart-button', function() {
1211 + var productId = $(this).data('product-id');
1212 + // Add a special prefix to indicate this is from button
1213 + appendMessage("user", "add to cart");
1214 + sendMessageToChatbot("!addtocart"); // Special command to indicate button click
1215 +});
1216 +
1217 +
1218 +if (document.getElementById('pdf-upload-btn')) {
1219 + document.getElementById('pdf-upload-btn').addEventListener('click', function() {
1220 + document.getElementById('pdf-upload').click();
1221 + });
3177 1222 }
3178 1223
3179 -function showEmailFormForBot(botId) {
3180 - hideInitLoader(botId);
3181 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3182 - var chatContainer = getElementDOM(botId, 'chat-container');
3183 - if (emailBlocker) emailBlocker.style.display = 'flex';
3184 - if (chatContainer) chatContainer.style.display = 'none';
1224 +if (document.getElementById('word-upload-btn')) {
1225 + document.getElementById('word-upload-btn').addEventListener('click', function() {
1226 + document.getElementById('word-upload').click();
1227 + });
3185 1228 }
3186 1229
3187 -function showChatContainerForBot(botId) {
3188 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3189 - var chatContainer = getElementDOM(botId, 'chat-container');
3190 - if (emailBlocker) emailBlocker.style.display = 'none';
3191 -
3192 - var instance = MxChatInstances.get(botId);
3193 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
3194 -
3195 - // If persistence is on and history hasn't loaded yet, show loader
3196 - // while history loads to prevent flash of empty chat
3197 - if (chatPersistenceEnabled && !instance.chatHistoryLoaded) {
3198 - if (chatContainer) chatContainer.style.display = 'none';
3199 - showInitLoader(botId);
3200 - loadChatHistory(botId, function() {
3201 - hideInitLoader(botId);
3202 - if (chatContainer) chatContainer.style.display = 'flex';
3203 - scrollToBottom(botId, true);
3204 - });
3205 - } else {
3206 - hideInitLoader(botId);
3207 - if (chatContainer) chatContainer.style.display = 'flex';
3208 - if (typeof loadChatHistory === 'function') {
3209 - loadChatHistory(botId);
3210 - }
1230 +function addSafeEventListener(elementId, eventType, handler) {
1231 + const element = document.getElementById(elementId);
1232 + if (element) {
1233 + element.addEventListener(eventType, handler);
3211 1234 }
3212 1235 }
3213 1236
3214 -// ====================================
3215 -// EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
3216 -// ====================================
3217 -// Only run email collection setup if it's enabled
3218 -if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
3219 1237
3220 - // Track submitting state per bot
3221 - const emailSubmittingState = {};
1238 +// PDF file input change handler
1239 +addSafeEventListener('pdf-upload', 'change', async function(e) {
1240 + const file = e.target.files[0];
3222 1241
3223 - // Add CSS animations for email form (once globally)
3224 - if (!document.getElementById('email-error-styles')) {
3225 - const style = document.createElement('style');
3226 - style.id = 'email-error-styles';
3227 - style.textContent = `
3228 - @keyframes fadeInError {
3229 - from { opacity: 0; transform: translateY(-5px); }
3230 - to { opacity: 1; transform: translateY(0); }
3231 - }
3232 - .email-input-shake {
3233 - animation: shake 0.5s ease-in-out;
3234 - }
3235 - @keyframes shake {
3236 - 0%, 100% { transform: translateX(0); }
3237 - 25% { transform: translateX(-5px); }
3238 - 75% { transform: translateX(5px); }
3239 - }
3240 - @keyframes spin {
3241 - from { transform: rotate(0deg); }
3242 - to { transform: rotate(360deg); }
3243 - }
3244 - .email-spinner {
3245 - display: inline-block;
3246 - vertical-align: middle;
3247 - }
3248 - `;
3249 - document.head.appendChild(style);
1242 + if (!file || file.type !== 'application/pdf') {
1243 + alert('Please select a valid PDF file.');
1244 + return;
3250 1245 }
3251 1246
3252 - function isValidEmailAddress(email) {
3253 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3254 - return emailRegex.test(email.trim()) && email.length <= 254;
1247 + if (!sessionId) {
1248 + console.error('No session ID found');
1249 + alert('Error: No session ID found');
1250 + return;
3255 1251 }
3256 1252
3257 - function isValidNameInput(name) {
3258 - return name && name.trim().length >= 2 && name.trim().length <= 100;
1253 + if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
1254 + console.error('mxchatChat not properly configured:', mxchatChat);
1255 + alert('Error: Ajax configuration missing');
1256 + return;
3259 1257 }
3260 1258
3261 - /**
3262 - * Replace {visitor_name} placeholder in intro message with actual visitor name
3263 - * @param {string} botId - The bot instance ID
3264 - * @param {string} visitorName - The visitor's name to insert
3265 - */
3266 - function replaceVisitorNamePlaceholder(botId, visitorName) {
3267 - var chatBox = getElementDOM(botId, 'chat-box');
3268 - if (!chatBox) return;
1259 + // Disable buttons and show loading state
1260 + const uploadBtn = document.getElementById('pdf-upload-btn');
1261 + const sendBtn = document.getElementById('send-button');
1262 + const originalBtnContent = uploadBtn.innerHTML;
3269 1263
3270 - // Find the first bot message (intro message)
3271 - var introMessage = chatBox.querySelector('.bot-message');
3272 - if (!introMessage) return;
1264 + try {
1265 + const formData = new FormData();
1266 + formData.append('action', 'mxchat_upload_pdf');
1267 + formData.append('pdf_file', file);
1268 + formData.append('session_id', sessionId);
1269 + formData.append('nonce', mxchatChat.nonce);
3273 1270
3274 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3275 - if (!messageContent) return;
1271 + uploadBtn.disabled = true;
1272 + sendBtn.disabled = true;
1273 + uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
1274 + <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
1275 + </svg>`;
3276 1276
3277 - var html = messageContent.innerHTML;
1277 + const response = await fetch(mxchatChat.ajax_url, {
1278 + method: 'POST',
1279 + body: formData
1280 + });
3278 1281
3279 - // Replace {visitor_name} placeholder (case-insensitive)
3280 - if (visitorName && visitorName.trim()) {
3281 - // Escape HTML to prevent XSS
3282 - var safeName = $('<div>').text(visitorName.trim()).html();
3283 - html = html.replace(/\{visitor_name\}/gi, safeName);
1282 + const data = await response.json();
1283 +
1284 + if (data.success) {
1285 + // Hide popular questions if they exist
1286 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
1287 + if (popularQuestionsContainer) {
1288 + popularQuestionsContainer.style.display = 'none';
1289 + }
1290 +
1291 + // Show the active PDF name
1292 + showActivePdf(data.data.filename);
1293 +
1294 + appendMessage('bot', data.data.message);
1295 + scrollToBottom();
1296 + activePdfFile = data.data.filename;
3284 1297 } else {
3285 - // Remove placeholder and clean up spacing if no name provided
3286 - html = html.replace(/\{visitor_name\}/gi, '');
3287 - // Clean up any double spaces that might result
3288 - html = html.replace(/\s{2,}/g, ' ').trim();
1298 + console.error('Upload failed:', data.data);
1299 + alert('Failed to upload PDF. Please try again.');
3289 1300 }
3290 -
3291 - messageContent.innerHTML = html;
1301 + } catch (error) {
1302 + console.error('Upload error:', error);
1303 + alert('Error uploading file. Please try again.');
1304 + } finally {
1305 + uploadBtn.disabled = false;
1306 + sendBtn.disabled = false;
1307 + uploadBtn.innerHTML = originalBtnContent;
1308 + this.value = ''; // Reset file input
3292 1309 }
1310 +});
3293 1311
3294 - function setEmailSubmissionState(botId, loading) {
3295 - var submitButton = getElementDOM(botId, 'email-submit-button');
3296 - var emailInput = getElementDOM(botId, 'user-email');
3297 - var nameInput = getElementDOM(botId, 'user-name');
1312 +// Word file input change handler
1313 +addSafeEventListener('word-upload', 'change', async function(e) {
1314 + const file = e.target.files[0];
3298 1315
3299 - if (loading) {
3300 - emailSubmittingState[botId] = true;
3301 - if (submitButton) submitButton.disabled = true;
3302 - if (emailInput) emailInput.disabled = true;
3303 - if (nameInput) nameInput.disabled = true;
1316 + if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
1317 + alert('Please select a valid Word document (.docx).');
1318 + return;
1319 + }
3304 1320
3305 - if (submitButton && !submitButton.getAttribute('data-original-html')) {
3306 - submitButton.setAttribute('data-original-html', submitButton.innerHTML);
3307 - const originalText = submitButton.textContent;
3308 - submitButton.innerHTML = `
3309 - <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
3310 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
3311 - <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
3312 - <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
3313 - </circle>
3314 - </svg>
3315 - ${originalText}
3316 - `;
3317 - submitButton.style.opacity = '0.8';
3318 - }
3319 - } else {
3320 - emailSubmittingState[botId] = false;
3321 - if (submitButton) submitButton.disabled = false;
3322 - if (emailInput) emailInput.disabled = false;
3323 - if (nameInput) nameInput.disabled = false;
3324 -
3325 - if (submitButton) {
3326 - const originalHtml = submitButton.getAttribute('data-original-html');
3327 - if (originalHtml) {
3328 - submitButton.innerHTML = originalHtml;
3329 - }
3330 - submitButton.style.opacity = '1';
3331 - }
3332 - }
1321 + if (!sessionId) {
1322 + console.error('No session ID found');
1323 + alert('Error: No session ID found');
1324 + return;
3333 1325 }
3334 1326
3335 - function showEmailError(botId, message) {
3336 - clearEmailError(botId);
1327 + // Disable buttons and show loading state
1328 + const uploadBtn = document.getElementById('word-upload-btn');
1329 + const sendBtn = document.getElementById('send-button');
1330 + const originalBtnContent = uploadBtn.innerHTML;
3337 1331
3338 - var emailForm = getElementDOM(botId, 'email-collection-form');
3339 - if (!emailForm) return;
1332 + try {
1333 + const formData = new FormData();
1334 + formData.append('action', 'mxchat_upload_word');
1335 + formData.append('word_file', file);
1336 + formData.append('session_id', sessionId);
1337 + formData.append('nonce', mxchatChat.nonce);
3340 1338
3341 - const errorDiv = document.createElement('div');
3342 - errorDiv.className = 'email-error';
3343 - errorDiv.style.cssText = `
3344 - color: #e74c3c;
3345 - font-size: 12px;
3346 - margin-top: 8px;
3347 - padding: 4px 0;
3348 - animation: fadeInError 0.3s ease;
3349 - `;
3350 - errorDiv.textContent = message;
3351 - emailForm.appendChild(errorDiv);
1339 + uploadBtn.disabled = true;
1340 + sendBtn.disabled = true;
1341 + uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
1342 + <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
1343 + </svg>`;
3352 1344
3353 - // Add shake animation to inputs
3354 - var emailInput = getElementDOM(botId, 'user-email');
3355 - var nameInput = getElementDOM(botId, 'user-name');
1345 + const response = await fetch(mxchatChat.ajax_url, {
1346 + method: 'POST',
1347 + body: formData
1348 + });
3356 1349
3357 - if (emailInput) {
3358 - emailInput.classList.add('email-input-shake');
3359 - setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
3360 - }
3361 - if (nameInput) {
3362 - nameInput.classList.add('email-input-shake');
3363 - setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
3364 - }
3365 - }
1350 + const data = await response.json();
3366 1351
3367 - function clearEmailError(botId) {
3368 - var emailForm = getElementDOM(botId, 'email-collection-form');
3369 - if (emailForm) {
3370 - const existingErrors = emailForm.querySelectorAll('.email-error');
3371 - existingErrors.forEach(error => error.remove());
3372 - }
3373 - }
1352 + if (data.success) {
1353 + // Hide popular questions if they exist
1354 + const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
1355 + if (popularQuestionsContainer) {
1356 + popularQuestionsContainer.style.display = 'none';
1357 + }
3374 1358
3375 - // Resolve email state using server-side data when available, AJAX fallback otherwise
3376 - function resolveEmailState(botId) {
3377 - if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
3378 - if (mxchatChat.initial_email_state.show_email_form) {
3379 - showEmailFormForBot(botId);
3380 - } else {
3381 - showChatContainerForBot(botId);
3382 - }
1359 + // Show the active Word document name
1360 + showActiveWord(data.data.filename);
1361 +
1362 + appendMessage('bot', data.data.message);
1363 + scrollToBottom();
1364 + activeWordFile = data.data.filename;
3383 1365 } else {
3384 - checkSessionAndEmailForBot(botId);
1366 + console.error('Upload failed:', data.data);
1367 + alert('Failed to upload Word document. Please try again.');
3385 1368 }
1369 + } catch (error) {
1370 + console.error('Upload error:', error);
1371 + alert('Error uploading file. Please try again.');
1372 + } finally {
1373 + uploadBtn.disabled = false;
1374 + sendBtn.disabled = false;
1375 + uploadBtn.innerHTML = originalBtnContent;
1376 + this.value = ''; // Reset file input
3386 1377 }
1378 +});
3387 1379
3388 - function checkSessionAndEmailForBot(botId) {
3389 - const sessionId = MxChatInstances.ensureSession(botId);
1380 +// Function to show active PDF name in toolbar
1381 +function showActivePdf(filename) {
1382 + const container = document.getElementById('active-pdf-container');
1383 + const nameElement = document.getElementById('active-pdf-name');
1384 +
1385 + if (!container || !nameElement) {
1386 + console.error('PDF container elements not found');
1387 + return;
1388 + }
3390 1389
3391 - // Hide both panels while we check — show loader instead
3392 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3393 - var chatContainer = getElementDOM(botId, 'chat-container');
3394 - if (emailBlocker) emailBlocker.style.display = 'none';
3395 - if (chatContainer) chatContainer.style.display = 'none';
3396 - showInitLoader(botId);
1390 + nameElement.textContent = filename;
1391 + container.style.display = 'flex';
1392 +}
3397 1393
3398 - fetch(mxchatChat.ajax_url, {
3399 - method: 'POST',
3400 - headers: {
3401 - 'Content-Type': 'application/x-www-form-urlencoded',
3402 - },
3403 - body: new URLSearchParams({
3404 - action: 'mxchat_check_email_provided',
3405 - session_id: sessionId,
3406 - nonce: mxchatChat.nonce,
3407 - })
3408 - })
3409 - .then((response) => {
3410 - if (!response.ok) {
3411 - throw new Error(`HTTP error! status: ${response.status}`);
3412 - }
3413 - return response.json();
3414 - })
3415 - .then((data) => {
3416 - if (data.success) {
3417 - if (data.data.logged_in || data.data.email) {
3418 - showChatContainerForBot(botId);
3419 - } else {
3420 - showEmailFormForBot(botId);
3421 - }
3422 - } else {
3423 - showEmailFormForBot(botId);
3424 - }
3425 - })
3426 - .catch((error) => {
3427 - showEmailFormForBot(botId);
3428 - });
1394 +// Function to show active Word document name in toolbar
1395 +function showActiveWord(filename) {
1396 + const container = document.getElementById('active-word-container');
1397 + const nameElement = document.getElementById('active-word-name');
1398 +
1399 + if (!container || !nameElement) {
1400 + console.error('Word document container elements not found');
1401 + return;
3429 1402 }
3430 1403
3431 - // Event delegation for email form submission
3432 - $(document).on('submit', '.email-collection-form', function(e) {
3433 - e.preventDefault();
3434 - e.stopPropagation();
1404 + nameElement.textContent = filename;
1405 + container.style.display = 'flex';
1406 +}
3435 1407
3436 - var botId = getBotIdFromElement(this);
1408 +// Function to remove active PDF
1409 +function removeActivePdf() {
1410 + const container = document.getElementById('active-pdf-container');
1411 + const nameElement = document.getElementById('active-pdf-name');
1412 +
1413 + if (!container || !nameElement || !activePdfFile) return;
3437 1414
3438 - // Prevent double submission
3439 - if (emailSubmittingState[botId]) {
3440 - return false;
1415 + fetch(mxchatChat.ajax_url, {
1416 + method: 'POST',
1417 + headers: {
1418 + 'Content-Type': 'application/x-www-form-urlencoded',
1419 + },
1420 + body: new URLSearchParams({
1421 + 'action': 'mxchat_remove_pdf',
1422 + 'session_id': sessionId,
1423 + 'nonce': mxchatChat.nonce
1424 + })
1425 + })
1426 + .then(response => response.json())
1427 + .then(data => {
1428 + if (data.success) {
1429 + container.style.display = 'none';
1430 + nameElement.textContent = '';
1431 + activePdfFile = null;
1432 + appendMessage('bot', 'PDF removed.');
3441 1433 }
1434 + })
1435 + .catch(error => {
1436 + console.error('Error removing PDF:', error);
1437 + });
1438 +}
3442 1439
3443 - var emailInput = getElementDOM(botId, 'user-email');
3444 - var nameInput = getElementDOM(botId, 'user-name');
3445 - var userEmail = emailInput ? emailInput.value.trim() : '';
3446 - var userName = nameInput ? nameInput.value.trim() : '';
3447 - var sessionId = MxChatInstances.ensureSession(botId);
1440 +// Function to remove active Word document
1441 +function removeActiveWord() {
1442 + const container = document.getElementById('active-word-container');
1443 + const nameElement = document.getElementById('active-word-name');
1444 +
1445 + if (!container || !nameElement || !activeWordFile) return;
3448 1446
3449 - // Validate email
3450 - if (!userEmail) {
3451 - showEmailError(botId, 'Please enter your email address.');
3452 - return false;
1447 + fetch(mxchatChat.ajax_url, {
1448 + method: 'POST',
1449 + headers: {
1450 + 'Content-Type': 'application/x-www-form-urlencoded',
1451 + },
1452 + body: new URLSearchParams({
1453 + 'action': 'mxchat_remove_word',
1454 + 'session_id': sessionId,
1455 + 'nonce': mxchatChat.nonce
1456 + })
1457 + })
1458 + .then(response => response.json())
1459 + .then(data => {
1460 + if (data.success) {
1461 + container.style.display = 'none';
1462 + nameElement.textContent = '';
1463 + activeWordFile = null;
1464 + appendMessage('bot', 'Word document removed.');
3453 1465 }
1466 + })
1467 + .catch(error => {
1468 + console.error('Error removing Word document:', error);
1469 + });
1470 +}
3454 1471
3455 - if (!isValidEmailAddress(userEmail)) {
3456 - showEmailError(botId, 'Please enter a valid email address.');
3457 - return false;
1472 +// Add remove button click handlers
1473 +document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
1474 + e.preventDefault();
1475 + e.stopPropagation();
1476 + removeActivePdf();
1477 +});
1478 +
1479 +document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
1480 + e.preventDefault();
1481 + e.stopPropagation();
1482 + removeActiveWord();
1483 +});
1484 +
1485 +// Check initial document status
1486 +function checkInitialDocumentStatus() {
1487 + if (!sessionId) return;
1488 +
1489 + // Check PDF status
1490 + fetch(mxchatChat.ajax_url, {
1491 + method: 'POST',
1492 + headers: {
1493 + 'Content-Type': 'application/x-www-form-urlencoded',
1494 + },
1495 + body: new URLSearchParams({
1496 + 'action': 'mxchat_check_pdf_status',
1497 + 'session_id': sessionId,
1498 + 'nonce': mxchatChat.nonce
1499 + })
1500 + })
1501 + .then(response => response.json())
1502 + .then(data => {
1503 + if (data.success && data.data.filename) {
1504 + showActivePdf(data.data.filename);
1505 + activePdfFile = data.data.filename;
3458 1506 }
1507 + })
1508 + .catch(error => {
1509 + console.error('Error checking PDF status:', error);
1510 + });
3459 1511
3460 - // Validate name if field exists and has content
3461 - if (nameInput && userName && !isValidNameInput(userName)) {
3462 - showEmailError(botId, 'Please enter a valid name (2-100 characters).');
3463 - return false;
1512 + // Check Word document status
1513 + fetch(mxchatChat.ajax_url, {
1514 + method: 'POST',
1515 + headers: {
1516 + 'Content-Type': 'application/x-www-form-urlencoded',
1517 + },
1518 + body: new URLSearchParams({
1519 + 'action': 'mxchat_check_word_status',
1520 + 'session_id': sessionId,
1521 + 'nonce': mxchatChat.nonce
1522 + })
1523 + })
1524 + .then(response => response.json())
1525 + .then(data => {
1526 + if (data.success && data.data.filename) {
1527 + showActiveWord(data.data.filename);
1528 + activeWordFile = data.data.filename;
3464 1529 }
1530 + })
1531 + .catch(error => {
1532 + console.error('Error checking Word document status:', error);
1533 + });
1534 +}
3465 1535
3466 - clearEmailError(botId);
3467 - setEmailSubmissionState(botId, true);
1536 +// Apply toolbar settings
1537 +if (mxchatChat.chat_toolbar_toggle === 'on') {
1538 + $('.chat-toolbar').show();
1539 +} else {
1540 + $('.chat-toolbar').hide();
1541 +}
3468 1542
3469 - // Prepare form data
3470 - const formData = new URLSearchParams({
3471 - action: 'mxchat_handle_save_email_and_response',
3472 - email: userEmail,
3473 - session_id: sessionId,
3474 - nonce: mxchatChat.nonce,
3475 - });
1543 +// Initialize on page load
1544 +document.addEventListener('DOMContentLoaded', function() {
1545 + checkInitialDocumentStatus();
1546 +});
3476 1547
3477 - if (userName) {
3478 - formData.append('name', userName);
3479 - }
1548 +const toolbarElements = [
1549 + '#mxchat-chatbot .toolbar-btn svg',
1550 + '#mxchat-chatbot .active-pdf-name',
1551 + '#mxchat-chatbot .active-word-name',
1552 + '#mxchat-chatbot .remove-pdf-btn svg',
1553 + '#mxchat-chatbot .remove-word-btn svg',
1554 + '#mxchat-chatbot .toolbar-perplexity svg'
1555 +];
3480 1556
1557 +toolbarElements.forEach(selector => {
1558 + $(selector).css({
1559 + 'fill': toolbarIconColor,
1560 + 'stroke': toolbarIconColor,
1561 + 'color': toolbarIconColor
1562 + });
1563 +});
1564 +
1565 +
1566 +// Ensure essential elements are defined
1567 +const emailForm = document.getElementById('email-collection-form');
1568 +const emailBlocker = document.getElementById('email-blocker');
1569 +const chatbotWrapper = document.getElementById('chat-container');
1570 +
1571 +// Only show error if email collection is enabled but elements are missing
1572 +if (emailForm && emailBlocker && chatbotWrapper) {
1573 + // Check if email exists for the current session
1574 + function checkSessionAndEmail() {
1575 + const sessionId = getChatSession();
1576 + //console.log("[DEBUG JS] checkSessionAndEmail -> sessionId:", sessionId);
1577 +
3481 1578 fetch(mxchatChat.ajax_url, {
3482 1579 method: 'POST',
3483 1580 headers: {
3484 1581 'Content-Type': 'application/x-www-form-urlencoded',
3485 1582 },
3486 - body: formData
1583 + body: new URLSearchParams({
1584 + action: 'mxchat_check_email_provided',
1585 + session_id: sessionId,
1586 + nonce: mxchatChat.nonce,
1587 + }),
3487 1588 })
3488 - .then((response) => {
3489 - if (!response.ok) {
3490 - throw new Error(`HTTP error! status: ${response.status}`);
3491 - }
3492 - return response.json();
3493 - })
1589 + .then((response) => response.json())
3494 1590 .then((data) => {
3495 - setEmailSubmissionState(botId, false);
3496 -
1591 + //console.log("[DEBUG JS] mxchat_check_email_provided response:", data);
1592 +
3497 1593 if (data.success) {
3498 - showChatContainerForBot(botId);
3499 -
3500 - // Replace {visitor_name} placeholder in intro message with actual name
3501 - if (userName) {
3502 - replaceVisitorNamePlaceholder(botId, userName);
1594 + if (data.data.logged_in) {
1595 + //console.log("[DEBUG JS] User is logged in. Hiding email form.");
1596 + emailBlocker.style.display = 'none';
1597 + chatbotWrapper.style.display = 'flex';
1598 + } else if (data.data.email) {
1599 + //console.log("[DEBUG JS] Email found for session. Hiding email form.");
1600 + emailBlocker.style.display = 'none';
1601 + chatbotWrapper.style.display = 'flex';
3503 1602 } else {
3504 - // Remove placeholder if no name provided
3505 - replaceVisitorNamePlaceholder(botId, '');
1603 + //console.log("[DEBUG JS] No email provided. Showing email form.");
1604 + emailBlocker.style.display = 'flex';
1605 + chatbotWrapper.style.display = 'none';
3506 1606 }
3507 -
3508 - if (data.message && typeof appendMessage === 'function') {
3509 - setTimeout(() => {
3510 - appendMessage('bot', data.message, '', [], false, botId);
3511 - if (typeof scrollToBottom === 'function') {
3512 - scrollToBottom(botId);
3513 - }
3514 - }, 100);
3515 - }
3516 1607 } else {
3517 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
1608 + //console.log("[DEBUG JS] Error or no data received. Showing email form.");
1609 + emailBlocker.style.display = 'flex';
1610 + chatbotWrapper.style.display = 'none';
3518 1611 }
3519 1612 })
3520 1613 .catch((error) => {
3521 - setEmailSubmissionState(botId, false);
3522 - showEmailError(botId, 'An error occurred. Please try again.');
1614 + // console.error("[DEBUG JS] Fetch error -> forcing email form visible:", error);
1615 + emailBlocker.style.display = 'flex';
1616 + chatbotWrapper.style.display = 'none';
3523 1617 });
3524 -
3525 - return false;
3526 - });
3527 -
3528 - // Real-time email validation using event delegation
3529 - $(document).on('input', '.mxchat-email-input', function() {
3530 - var botId = getBotIdFromElement(this);
3531 - var $input = $(this);
3532 -
3533 - // Clear previous timeout
3534 - clearTimeout($input.data('validationTimeout'));
3535 -
3536 - // Debounce validation
3537 - var timeout = setTimeout(() => {
3538 - var email = this.value.trim();
3539 - clearEmailError(botId);
3540 -
3541 - if (email && !isValidEmailAddress(email)) {
3542 - showEmailError(botId, 'Please enter a valid email address.');
3543 - }
3544 - }, 500);
3545 -
3546 - $input.data('validationTimeout', timeout);
3547 - });
3548 -
3549 - // Handle Enter key in email input
3550 - $(document).on('keypress', '.mxchat-email-input', function(e) {
3551 - if (e.key === 'Enter') {
3552 - e.preventDefault();
3553 - var botId = getBotIdFromElement(this);
3554 - if (!emailSubmittingState[botId]) {
3555 - $(this).closest('.email-collection-form').submit();
3556 - }
3557 - }
3558 - });
3559 -
3560 - // Handle Enter key in name input
3561 - $(document).on('keypress', '.mxchat-name-input', function(e) {
3562 - if (e.key === 'Enter') {
3563 - e.preventDefault();
3564 - var botId = getBotIdFromElement(this);
3565 - if (!emailSubmittingState[botId]) {
3566 - $(this).closest('.email-collection-form').submit();
3567 - }
3568 - }
3569 - });
3570 -
3571 - // Initialize email check for all bot instances
3572 - // For floating bots: defer until widget is opened (zero passive AJAX)
3573 - // For embedded bots: check immediately since the form is visible
3574 - $('.mxchat-chatbot-wrapper').each(function() {
3575 - var botId = $(this).data('bot-id') || 'default';
3576 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3577 -
3578 - if (emailBlocker) {
3579 - if (isEmbeddedBot(botId)) {
3580 - // Embedded bots are always visible — check now
3581 - resolveEmailState(botId);
3582 - }
3583 - // Floating bots: handled in the widget open handler
3584 - } else if (isEmbeddedBot(botId)) {
3585 - // Embedded bot, no email collection — load history with loader
3586 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3587 - if (chatPersistenceEnabled) {
3588 - MxChatInstances.ensureSession(botId);
3589 - showChatContainerForBot(botId);
3590 - }
3591 - }
3592 - });
3593 -}
3594 -
3595 - // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
3596 - $(document).on('click', '.pre-chat-message', function() {
3597 - var botId = getBotIdFromElement(this);
3598 - var $chatbot = getElement(botId, 'floating-chatbot');
3599 - if ($chatbot.hasClass('hidden')) {
3600 - $chatbot.removeClass('hidden').addClass('visible');
3601 - getElement(botId, 'floating-chatbot-button').addClass('hidden');
3602 - handlePreChatDismissal(botId);
3603 - disableScroll(); // Disable scroll when chatbot opens
3604 -
3605 - // Load chat history for returning visitors (persistence)
3606 - var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
3607 - if (chatPersistenceEnabled) {
3608 - MxChatInstances.ensureSession(botId);
3609 - }
3610 -
3611 - // Deferred email check — only on first widget open
3612 - var emailBlocker = getElementDOM(botId, 'email-blocker');
3613 - var instance = MxChatInstances.get(botId);
3614 - if (emailBlocker && !instance.emailCheckDone) {
3615 - instance.emailCheckDone = true;
3616 - resolveEmailState(botId);
3617 - } else if (!emailBlocker) {
3618 - showChatContainerForBot(botId);
3619 - }
3620 - }
3621 - });
3622 -
3623 - // Legacy duplicate close handler removed — handled by single event delegation above
3624 -
3625 -
3626 -function hasQuickQuestions(botId) {
3627 - botId = botId || 'default';
3628 - var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3629 - if (!questionsContainer) return false;
3630 - const questionButtons = questionsContainer.querySelectorAll('.mxchat-popular-question');
3631 - return questionButtons.length > 0;
3632 -}
3633 -
3634 -/**
3635 - * Check if a bot is embedded (not floating)
3636 - * Embedded bots don't have a .floating-chatbot wrapper
3637 - */
3638 -function isEmbeddedBot(botId) {
3639 - botId = botId || 'default';
3640 - var floatingWrapper = document.getElementById('floating-chatbot-' + botId);
3641 - return !floatingWrapper;
3642 -}
3643 -
3644 -function collapseQuickQuestions(botId) {
3645 - botId = botId || 'default';
3646 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3647 - if (questionsContainer && hasQuickQuestions(botId)) {
3648 - questionsContainer.classList.add('collapsed');
3649 - questionsContainer.classList.add('has-been-collapsed');
3650 - try {
3651 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'true');
3652 - sessionStorage.setItem('mxchat_questions_has_been_collapsed_' + botId, 'true');
3653 - } catch (e) {
3654 - // Ignore if sessionStorage is not available
3655 - }
3656 1618 }
3657 -}
3658 1619
3659 -function expandQuickQuestions(botId) {
3660 - botId = botId || 'default';
3661 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3662 - if (questionsContainer && hasQuickQuestions(botId)) {
3663 - questionsContainer.classList.remove('collapsed');
3664 - try {
3665 - sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'false');
3666 - } catch (e) {
3667 - // Ignore if sessionStorage is not available
3668 - }
3669 - }
3670 -}
1620 + // Handle email form submission
1621 + emailForm.addEventListener('submit', function (event) {
1622 + event.preventDefault();
1623 + const userEmail = document.getElementById('user-email').value;
1624 + const sessionId = getChatSession();
3671 1625
3672 -function checkQuickQuestionsState(botId) {
3673 - botId = botId || 'default';
3674 - if (!hasQuickQuestions(botId)) {
3675 - return; // Don't do anything if no questions exist
3676 - }
1626 + if (userEmail) {
1627 + fetch(mxchatChat.ajax_url, {
1628 + method: 'POST',
1629 + headers: {
1630 + 'Content-Type': 'application/x-www-form-urlencoded',
1631 + },
1632 + body: new URLSearchParams({
1633 + action: 'mxchat_handle_save_email_and_response',
1634 + email: userEmail,
1635 + session_id: sessionId,
1636 + nonce: mxchatChat.nonce,
1637 + }),
1638 + })
1639 + .then((response) => response.json())
1640 + .then((data) => {
1641 + //console.log('Backend response:', data);
1642 + if (data.success) {
1643 + //console.log('Email saved successfully:', userEmail);
1644 + emailBlocker.style.display = 'none';
1645 + chatbotWrapper.style.display = 'flex';
3677 1646
3678 - // Skip restoring collapsed state for embedded bots - they should always start expanded
3679 - if (isEmbeddedBot(botId)) {
3680 - return;
3681 - }
3682 -
3683 - try {
3684 - const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed_' + botId);
3685 - const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed_' + botId);
3686 -
3687 - const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3688 - if (questionsContainer) {
3689 - if (hasBeenCollapsed === 'true') {
3690 - questionsContainer.classList.add('has-been-collapsed');
3691 - }
3692 - if (isCollapsed === 'true') {
3693 - questionsContainer.classList.add('collapsed');
3694 - }
3695 - }
3696 - } catch (e) {
3697 - // Ignore if sessionStorage is not available
3698 - }
3699 -}
3700 -
3701 -// Global delegation for dynamically added links as fallback
3702 -// Use class selector for multi-instance support
3703 -$(document).on('click', '.chat-box a[href]:not([data-tracked])', function(e) {
3704 - const $link = $(this);
3705 - const messageDiv = $link.closest('.bot-message, .agent-message');
3706 -
3707 - // Only process bot/agent message links
3708 - if (messageDiv.length > 0) {
3709 - const originalHref = $link.attr('href');
3710 -
3711 - if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
3712 - e.preventDefault();
3713 - e.stopPropagation();
3714 -
3715 - // Mark as tracked
3716 - $link.attr('data-tracked', 'true');
3717 -
3718 - // Get bot ID from the chat box context
3719 - var botId = getBotIdFromElement(this);
3720 -
3721 - // Get message context from the message div
3722 - const messageText = messageDiv.text().substring(0, 200);
3723 -
3724 - $.ajax({
3725 - url: mxchatChat.ajax_url,
3726 - type: 'POST',
3727 - data: {
3728 - action: 'mxchat_track_url_click',
3729 - session_id: getChatSession(botId),
3730 - url: originalHref,
3731 - message_context: messageText,
3732 - nonce: mxchatChat.nonce
3733 - },
3734 - complete: function() {
3735 - if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
3736 - window.open(originalHref, '_blank');
3737 - } else {
3738 - window.location.href = originalHref;
1647 + // Optionally handle bot response
1648 + if (data.message) {
1649 + appendMessage('bot', data.message);
1650 + scrollToBottom();
3739 1651 }
1652 + } else {
1653 + console.error('Error saving email:', data.message || 'Unknown error');
3740 1654 }
1655 + })
1656 + .catch((error) => {
1657 + console.error('AJAX error:', error);
3741 1658 });
3742 -
3743 - return false;
3744 1659 }
3745 - }
3746 -});
3747 -
3748 - // ====================================
3749 - // MAIN INITIALIZATION
3750 - // ====================================
3751 -
3752 - // Initialize all chatbot instances on the page
3753 - initializeAllInstances();
3754 -
3755 - // Legacy initialization for single bot compatibility
3756 - $('.floating-chatbot.hidden').each(function() {
3757 - var botId = getBotIdFromElement(this);
3758 - getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3759 1660 });
3760 1661
3761 - // Initialize when document is ready
3762 - setFullHeight();
1662 + // Check session and email status on page load
1663 + checkSessionAndEmail();
1664 +} else if (mxchatChat.email_collection_enabled) {
1665 + // Only show error if email collection is enabled but elements are missing
1666 + console.error('Essential elements for email handling are missing.');
1667 +}
3763 1668
3764 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3765 - // until the user's first interaction via MxChatInstances.ensureSession()
3766 1669
3767 - // Initialize chat visibility for all instances
3768 - $('.mxchat-chatbot-wrapper').each(function() {
3769 - var botId = $(this).data('bot-id') || 'default';
3770 - initializeChatVisibility(botId);
3771 - });
1670 +// Initialize when document is ready
1671 +$(document).ready(function() {
1672 + setFullHeight();
1673 + initializeChatVisibility();
1674 + loadChatHistory();
3772 1675
3773 - // Make functions globally available for add-ons
3774 - window.hasQuickQuestions = hasQuickQuestions;
3775 - window.collapseQuickQuestions = collapseQuickQuestions;
3776 - window.appendMessage = appendMessage;
3777 - window.appendThinkingMessage = appendThinkingMessage;
3778 - window.scrollToBottom = scrollToBottom;
3779 - window.scrollElementToTop = scrollElementToTop;
3780 - window.replaceLastMessage = replaceLastMessage;
3781 - window.callMxChat = callMxChat;
3782 - window.callMxChatStream = callMxChatStream;
3783 - window.shouldUseStreaming = shouldUseStreaming;
3784 - window.getChatSession = getChatSession;
3785 - window.getPageContext = getPageContext;
3786 - window.updateStreamingMessage = updateStreamingMessage;
3787 - window.MxChatInstances = MxChatInstances;
3788 - window.getElement = getElement;
3789 - window.getElementDOM = getElementDOM;
3790 - window.getBotIdFromElement = getBotIdFromElement;
1676 +});
3791 1677
3792 -}); // End of jQuery ready
1678 +});
3793 1679
3794 -
3795 -// ====================================
3796 -// GLOBAL EVENT LISTENERS (Outside jQuery)
3797 -// ====================================
3798 -
3799 -// Event listener for copy button (code blocks)
1680 +// Event listener for copy button
3800 1681 document.addEventListener("click", (e) => {
3801 1682 if (e.target.classList.contains("mxchat-copy-button")) {
3802 1683 const copyButton = e.target;
3803 1684 const codeBlock = copyButton
@@ -3817,5 +1698,7 @@
3817 1698 });
3818 1699 }
3819 1700 }
3820 1701 });
1702 +
1703 +
3821 1704