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