PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.2.4
MxChat – AI Chatbot & Content Generation for WordPress v2.2.4
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 +959 -2853 3.2.42.2.4 View file →
@@ -1,228 +1,12 @@
1 1 jQuery(document).ready(function($) {
2 -
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 - }
19 -
2 +
20 3 // ====================================
21 - // MULTI-INSTANCE MANAGEMENT SYSTEM
22 - // ====================================
23 -
24 - // Instance registry - tracks all chatbot instances on the page
25 - const MxChatInstances = {
26 - instances: {},
27 -
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';
34 -
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 -
53 - // Get instance by botId
54 - get: function(botId) {
55 - return this.instances[botId] || this.init(botId);
56 - },
57 -
58 - // Get all active bot IDs
59 - getAllBotIds: function() {
60 - return Object.keys(this.instances);
61 - },
62 -
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);
70 -
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 - }
75 -
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;
82 - }
83 -
84 - // Guard against stored sentinel values that indicate earlier broken writes.
85 - if (sessionId === 'null' || sessionId === 'undefined') {
86 - sessionId = null;
87 - }
88 -
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 - }
93 -
94 - return sessionId || null;
95 - },
96 -
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);
101 -
102 - if (instance.sessionId) {
103 - return instance.sessionId;
104 - }
105 -
106 - // Check for existing session from cookie or localStorage
107 - var existingSession = this.getChatSession(botId);
108 -
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 -
118 - // Now that we have a session, do the deferred work
119 - refreshNonceIfNeeded();
120 - trackOriginatingPage();
121 -
122 - // Note: loadChatHistory is handled by showChatContainerForBot with loader UI,
123 - // so we do NOT call it here to avoid a race condition.
124 -
125 - return instance.sessionId;
126 - },
127 -
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 - },
137 -
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 - },
152 -
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 - };
165 -
166 - // ====================================
167 - // ELEMENT SELECTOR HELPERS
168 - // ====================================
169 -
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 - }
180 -
181 - // Get element by ID with bot suffix - returns jQuery object
182 - function getElement(botId, elementName) {
183 - return $('#' + elementName + '-' + botId);
184 - }
185 -
186 - // Get element by ID with bot suffix - returns DOM element
187 - function getElementDOM(botId, elementName) {
188 - return document.getElementById(elementName + '-' + botId);
189 - }
190 -
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 - }
213 -
214 - // Get wrapper element for a bot
215 - function getWrapper(botId) {
216 - return getElement(botId, 'mxchat-chatbot-wrapper');
217 - }
218 -
219 - // ====================================
220 4 // GLOBAL VARIABLES & CONFIGURATION
221 5 // ====================================
222 6 const toolbarIconColor = mxchatChat.toolbar_icon_color || '#212121';
223 -
224 - // Initialize color settings (these are global as they come from PHP)
7 +
8 + // Initialize color settings
225 9 var userMessageBgColor = mxchatChat.user_message_bg_color;
226 10 var userMessageFontColor = mxchatChat.user_message_font_color;
227 11 var botMessageBgColor = mxchatChat.bot_message_bg_color;
228 12 var botMessageFontColor = mxchatChat.bot_message_font_color;
@@ -227,454 +11,203 @@
227 11 var botMessageBgColor = mxchatChat.bot_message_bg_color;
228 12 var botMessageFontColor = mxchatChat.bot_message_font_color;
229 13 var liveAgentMessageBgColor = mxchatChat.live_agent_message_bg_color;
230 14 var liveAgentMessageFontColor = mxchatChat.live_agent_message_font_color;
15 +
16 + var linkTarget = mxchatChat.link_target_toggle === 'on' ? '_blank' : '_self';
17 + let lastSeenMessageId = '';
18 + let notificationCheckInterval;
19 + let notificationBadge;
20 + var sessionId = getChatSession();
21 + let pollingInterval;
22 + let processedMessageIds = new Set();
23 + let activePdfFile = null;
24 + let activeWordFile = null;
231 25
232 - var linkTarget = mxchatChat.link_target_toggle === 'on' ? '_blank' : '_self';
233 26
234 27 // ====================================
235 - // SESSION MANAGEMENT (Legacy compatibility)
28 + // SESSION MANAGEMENT
236 29 // ====================================
237 -
30 +
31 + function getChatSession() {
32 + var sessionId = getCookie('mxchat_session_id');
33 + //console.log("Session ID retrieved from cookie: ", sessionId);
34 +
35 + if (!sessionId) {
36 + sessionId = generateSessionId();
37 + //console.log("Generated new session ID: ", sessionId);
38 + setChatSession(sessionId);
39 + }
40 +
41 + //console.log("Final session ID: ", sessionId);
42 + return sessionId;
43 + }
44 +
45 + function setChatSession(sessionId) {
46 + // Set the cookie with a 24-hour expiration (86400 seconds)
47 + document.cookie = "mxchat_session_id=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
48 + }
49 +
238 50 function getCookie(name) {
239 51 let value = "; " + document.cookie;
240 52 let parts = value.split("; " + name + "=");
241 53 if (parts.length == 2) return parts.pop().split(";").shift();
242 54 }
243 -
55 +
244 56 function generateSessionId() {
245 57 return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
246 58 }
247 59
248 - // Legacy function - now delegates to instance manager
249 - function getChatSession(botId) {
250 - botId = botId || 'default';
251 - return MxChatInstances.getChatSession(botId);
252 - }
253 60
254 - function setChatSession(sessionId, botId) {
255 - botId = botId || 'default';
256 - MxChatInstances.setChatSession(botId, sessionId);
257 - }
258 -
259 - function resetChatSession(botId) {
260 - botId = botId || 'default';
261 - MxChatInstances.resetChatSession(botId);
262 - }
263 -
264 61 // ====================================
265 - // INITIALIZE ALL CHATBOT INSTANCES
62 + // CORE CHAT FUNCTIONALITY
266 63 // ====================================
267 -
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 - }
276 -
277 - function initializeBotInstance(botId) {
278 - var instance = MxChatInstances.get(botId);
279 -
280 - // Initialize quick questions state for this bot
281 - checkQuickQuestionsState(botId);
282 -
283 - // Note: Event handlers use event delegation with class selectors,
284 - // so they work automatically for all instances without per-bot setup
285 - }
286 -
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 64
297 - // Get page URL
298 - const pageUrl = window.location.href;
65 + function sendMessage() {
66 + var message = $('#chat-input').val(); // Get value from textarea
67 + if (message) {
68 + appendMessage("user", message); // Append user's message
69 + $('#chat-input').val(''); // Clear the textarea
70 + $('#chat-input').css('height', 'auto'); // Reset height after clearing content
299 71
300 - // Get page title
301 - const pageTitle = document.title || '';
72 + // Hide the popular questions section
73 + $('#mxchat-popular-questions').hide();
302 74
303 - // Get main content from the page
304 - let pageContent = '';
75 + // Show typing indicator
76 + appendThinkingMessage();
77 + scrollToBottom();
305 78
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;
79 + callMxChat(message, function(response) {
80 + // Replace typing indicator with actual response
81 + replaceLastMessage("bot", response);
82 + });
325 83 }
326 84 }
327 85
328 - // If no specific content area found, use body but exclude header, footer, nav, sidebar
329 - if (!contentElement) {
330 - contentElement = document.body;
331 - }
86 + function sendMessageToChatbot(message) {
87 + var sessionId = getChatSession(); // Reuse the session ID logic
332 88
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);
370 - }
371 - });
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 - }
89 + // Hide the popular questions section
90 + $('#mxchat-popular-questions').hide();
387 91
388 - // Only return context if we have meaningful content
389 - if (!pageContent || pageContent.length < 50) {
390 - return null;
391 - }
92 + // Show thinking indicator (no need to append the user's message again)
93 + appendThinkingMessage();
94 + scrollToBottom();
392 95
393 - return {
394 - url: pageUrl,
395 - title: pageTitle,
396 - content: pageContent
397 - };
398 -}
399 -
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';
96 + //console.log("Sending message to chatbot:", message); // Log the message
97 + //console.log("Session ID:", sessionId); // Log the session ID
405 98
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 - }
427 - });
428 -}
429 -
430 -// ====================================
431 -// CORE CHAT FUNCTIONALITY
432 -// ====================================
433 -
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 -}
449 -
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 -}
465 -
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 -
473 - // ADD PROMPT HOOK HERE
474 - if (typeof customMxChatFilter === 'function') {
475 - message = customMxChatFilter(message, "prompt");
476 - }
477 -
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 - }
485 -
486 - appendMessage("user", message, '', [], false, botId);
487 - $chatInput.val('');
488 - $chatInput.css('height', 'auto');
489 -
490 - if (hasQuickQuestions(botId)) {
491 - collapseQuickQuestions(botId);
492 - }
493 - appendThinkingMessage(botId);
494 - scrollToBottom(botId);
495 -
496 - const currentModel = mxchatChat.model || 'gpt-5.1-chat-latest';
497 -
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 - }
509 -}
510 -
511 -// Update your existing sendMessageToChatbot function
512 -function sendMessageToChatbot(message, botId) {
513 - botId = botId || 'default';
514 - MxChatInstances.ensureSession(botId);
515 -
516 - // ADD PROMPT HOOK HERE
517 - if (typeof customMxChatFilter === 'function') {
518 - message = customMxChatFilter(message, "prompt");
519 - }
520 -
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 - }
527 -
528 - var sessionId = getChatSession(botId);
529 -
530 - if (hasQuickQuestions(botId)) {
531 - collapseQuickQuestions(botId);
532 - }
533 - appendThinkingMessage(botId);
534 - scrollToBottom(botId);
535 -
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 {
99 + // Call the chatbot using the same call logic as sendMessage
544 100 callMxChat(message, function(response) {
545 - getElement(botId, 'chat-box').find('.temporary-message').remove();
546 - replaceLastMessage("bot", response, '', [], botId);
547 - }, botId);
548 - }
549 -}
550 -
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';
101 + // ** Ensure temporary thinking message is removed before adding new response **
102 + $('.temporary-message').remove();
555 103
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 -
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
104 + // Replace thinking indicator with actual response
105 + replaceLastMessage("bot", response);
106 + });
570 107 }
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 - }
576 108
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 - }
590 -}
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);
598 - }
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 -
604 -function callMxChat(message, callback, botId) {
605 - botId = botId || getMxChatBotId();
606 -
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);
624 - }
625 -
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 -
655 - $.ajax({
656 - url: mxchatChat.ajax_url,
657 - type: 'POST',
658 - dataType: 'json',
659 - data: ajaxData,
660 - 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)) {
109 + function callMxChat(message, callback) {
110 + $.ajax({
111 + url: mxchatChat.ajax_url,
112 + type: 'POST',
113 + dataType: 'json',
114 + data: {
115 + action: 'mxchat_handle_chat_request',
116 + message: message,
117 + session_id: getChatSession(),
118 + nonce: mxchatChat.nonce
119 + },
120 + success: function(response) {
121 + // Log the full response for debugging
122 + //console.log("API Response:", response);
123 +
124 + // First check if this is a successful response by looking for text, html, or message fields
125 + // This preserves compatibility with your server response format
126 + if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
127 + (response.success === true && response.data && response.data.status === 'waiting_for_agent')) {
128 +
129 + // Handle successful response - this is your original success handling code
130 +
131 + // Existing chat mode check
132 + if (response.chat_mode) {
133 + updateChatModeIndicator(response.chat_mode);
134 + }
135 + else if (response.fallbackResponse && response.fallbackResponse.chat_mode) {
136 + updateChatModeIndicator(response.fallbackResponse.chat_mode);
137 + }
138 +
139 + // Add PDF filename handling
140 + if (response.data && response.data.filename) {
141 + showActivePdf(response.data.filename);
142 + activePdfFile = response.data.filename;
143 + }
144 +
145 + // Add redirect check here
146 + if (response.redirect_url) {
147 + let responseText = response.text || '';
148 + if (responseText) {
149 + replaceLastMessage("bot", responseText);
150 + }
151 + setTimeout(() => {
152 + window.location.href = response.redirect_url;
153 + }, 1500);
154 + return;
155 + }
156 +
157 + // Check for live agent response
158 + if (response.success && response.data && response.data.status === 'waiting_for_agent') {
159 + updateChatModeIndicator('agent');
160 + return;
161 + }
162 +
163 + // Handle other responses
164 + let responseText = response.text || '';
165 + let responseHtml = response.html || '';
166 + let responseMessage = response.message || '';
167 +
168 + if (responseText === 'You are now chatting with the AI chatbot.') {
169 + updateChatModeIndicator('ai');
170 + }
171 +
172 + // Handle the message and show notification if chat is hidden
173 + if (responseText || responseHtml || responseMessage) {
174 + // Update the messages as before
175 + if (responseText && responseHtml) {
176 + replaceLastMessage("bot", responseText, responseHtml);
177 + } else if (responseText) {
178 + replaceLastMessage("bot", responseText);
179 + } else if (responseHtml) {
180 + replaceLastMessage("bot", "", responseHtml);
181 + } else if (responseMessage) {
182 + replaceLastMessage("bot", responseMessage);
183 + }
184 +
185 + // Check if chat is hidden and show notification
186 + if ($('#floating-chatbot').hasClass('hidden')) {
187 + const badge = $('#chat-notification-badge');
188 + if (badge.length) {
189 + badge.show();
190 + }
191 + }
192 + } else {
193 + //console.error("Unexpected response format:", response);
194 + replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.");
195 + }
196 +
197 + if (response.message_id) {
198 + lastSeenMessageId = response.message_id;
199 + }
200 +
201 + return;
202 + }
203 +
204 + // If we got here, it's likely an error response
205 + // Now we can check for error conditions with our robust error handling
206 +
674 207 let errorMessage = "";
675 208 let errorCode = "";
676 -
209 +
677 210 // Check various possible error locations in the response
678 211 if (response.data && response.data.error_message) {
679 212 errorMessage = response.data.error_message;
680 213 errorCode = response.data.error_code || "";
@@ -684,944 +217,248 @@
684 217 } else if (response.message) {
685 218 errorMessage = response.message;
686 219 } else if (typeof response.data === 'string') {
687 220 errorMessage = response.data;
221 + } else if (!response.success) {
222 + // Explicit check for success: false without other error info
223 + errorMessage = "An error occurred. Please try again or contact support.";
688 224 } else {
689 225 // Fallback for any other unexpected response format
690 - errorMessage = "An error occurred. Please try again or contact support.";
226 + errorMessage = "Unexpected response received. Please try again or contact support.";
691 227 }
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 -
228 +
229 + // Log the error with code for debugging
230 + //console.log("Response data:", response.data);
231 + //console.error("API Error:", errorMessage, "Code:", errorCode);
232 +
715 233 // Format user-friendly error message
716 234 let displayMessage = errorMessage;
717 -
235 +
718 236 // Customize message for admin users
719 237 if (mxchatChat.is_admin) {
720 238 // For admin users, show more technical details including error code
721 239 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
722 240 }
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
729 - // This preserves compatibility with your server response format
730 - if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
731 - (response.success === true && response.data && response.data.status === 'waiting_for_agent')) {
732 -
733 - // 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 -
740 - // Add PDF filename handling
741 - if (response.data && response.data.filename) {
742 - showActivePdf(response.data.filename, botId);
743 - var instance = MxChatInstances.get(botId);
744 - instance.activePdfFile = response.data.filename;
745 - }
746 -
747 - // Add redirect check here
748 - if (response.redirect_url) {
749 - if (responseText) {
750 - replaceLastMessage("bot", responseText, '', [], botId);
241 +
242 + replaceLastMessage("bot", displayMessage);
243 + },
244 + error: function(xhr, status, error) {
245 + console.error("AJAX Error:", status, error);
246 + //console.log("Response Text:", xhr.responseText);
247 +
248 + let errorMessage = "An unexpected error occurred.";
249 +
250 + // Try to parse the response if it's JSON
251 + try {
252 + const responseJson = JSON.parse(xhr.responseText);
253 + //console.log("Parsed error response:", responseJson);
254 +
255 + if (responseJson.data && responseJson.data.error_message) {
256 + errorMessage = responseJson.data.error_message;
257 + } else if (responseJson.message) {
258 + errorMessage = responseJson.message;
751 259 }
752 - setTimeout(() => {
753 - window.location.href = response.redirect_url;
754 - }, 1500);
755 - return;
756 - }
757 -
758 - // Check for live agent response
759 - if (response.success && response.data && response.data.status === 'waiting_for_agent') {
760 - removeThinkingDots(botId);
761 - updateChatModeIndicator('agent', botId);
762 - enableChatInput(botId);
763 - return;
764 - }
765 -
766 - // Handle the message and show notification if chat is hidden
767 - if (responseText || responseHtml || responseMessage) {
768 -
769 - // ADD RESPONSE HOOKS HERE - BEFORE DISPLAYING
770 - if (responseText && typeof customMxChatFilter === 'function') {
771 - responseText = customMxChatFilter(responseText, "response");
260 + } catch (e) {
261 + // Not JSON or parsing failed, use HTTP status based messages
262 + if (xhr.status === 0) {
263 + errorMessage = "Network error: Please check your internet connection.";
264 + } else if (xhr.status === 403) {
265 + errorMessage = "Access denied: Your session may have expired. Please refresh the page.";
266 + } else if (xhr.status === 404) {
267 + errorMessage = "API endpoint not found. Please contact support.";
268 + } else if (xhr.status === 429) {
269 + errorMessage = "Too many requests. Please try again in a moment.";
270 + } else if (xhr.status >= 500) {
271 + errorMessage = "Server error: The server encountered an issue. Please try again later.";
772 272 }
773 - if (responseMessage && typeof customMxChatFilter === 'function') {
774 - responseMessage = customMxChatFilter(responseMessage, "response");
775 - }
776 -
777 - // Update the messages as before
778 - if (responseText && responseHtml) {
779 - replaceLastMessage("bot", responseText, responseHtml, [], botId);
780 - } else if (responseText) {
781 - replaceLastMessage("bot", responseText, '', [], botId);
782 - } else if (responseHtml) {
783 - replaceLastMessage("bot", "", responseHtml, [], botId);
784 - } else if (responseMessage) {
785 - replaceLastMessage("bot", responseMessage, '', [], botId);
786 - }
787 -
788 - // 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();
794 - }
795 - }
796 - } 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);
802 273 }
803 -
804 - if (response.message_id) {
805 - var instance = MxChatInstances.get(botId);
806 - instance.lastSeenMessageId = response.message_id;
807 - }
808 -
809 - return;
274 +
275 + replaceLastMessage("bot", errorMessage);
810 276 }
811 -
812 - // Fallback for truly unexpected response formats
813 - replaceLastMessage("bot", "Unexpected response format. Please try again or contact support.", '', [], botId);
814 - },
815 - error: function(xhr, status, error) {
816 - let errorMessage = "An unexpected error occurred.";
817 -
818 - // Try to parse the response if it's JSON
819 - try {
820 - const responseJson = JSON.parse(xhr.responseText);
821 -
822 - if (responseJson.data && responseJson.data.error_message) {
823 - errorMessage = responseJson.data.error_message;
824 - } else if (responseJson.message) {
825 - errorMessage = responseJson.message;
826 - }
827 - } catch (e) {
828 - // Not JSON or parsing failed, use HTTP status based messages
829 - if (xhr.status === 0) {
830 - errorMessage = "Network error: Please check your internet connection.";
831 - } else if (xhr.status === 403) {
832 - errorMessage = "Access denied: Your session may have expired. Please refresh the page.";
833 - } else if (xhr.status === 404) {
834 - errorMessage = "API endpoint not found. Please contact support.";
835 - } else if (xhr.status === 429) {
836 - errorMessage = "Too many requests. Please try again in a moment.";
837 - } else if (xhr.status >= 500) {
838 - errorMessage = "Server error: The server encountered an issue. Please try again later.";
839 - }
277 + });
278 + }
279 +
280 + function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
281 + try {
282 + // Determine styles based on sender type
283 + let messageClass, bgColor, fontColor;
284 +
285 + if (sender === "user") {
286 + messageClass = "user-message";
287 + bgColor = userMessageBgColor;
288 + fontColor = userMessageFontColor;
289 + // Only sanitize user input
290 + messageText = sanitizeUserInput(messageText);
291 + } else if (sender === "agent") {
292 + messageClass = "agent-message";
293 + bgColor = liveAgentMessageBgColor;
294 + fontColor = liveAgentMessageFontColor;
295 + } else {
296 + messageClass = "bot-message";
297 + bgColor = botMessageBgColor;
298 + fontColor = botMessageFontColor;
840 299 }
841 -
842 - replaceLastMessage("bot", errorMessage, '', [], botId);
843 - }
844 - });
845 -}
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);
300 +
301 + const messageDiv = $('<div>')
302 + .addClass(messageClass)
303 + .attr('dir', 'auto') // Add dir="auto" for automatic text direction
304 + .css({
305 + 'background': bgColor,
306 + 'color': fontColor,
307 + 'margin-bottom': '1em'
308 + });
309 +
310 + // Process the message content based on sender
311 + let fullMessage;
312 + if (sender === "user") {
313 + // For user messages, apply linkify after sanitization
314 + fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(messageText))));
315 + } else {
316 + // For bot/agent messages, preserve HTML
317 + fullMessage = messageText;
931 318 }
932 -
933 - // Check for testing panel
934 - if (window.mxchatTestPanelInstance && data.testing_data) {
935 - window.mxchatTestPanelInstance.handleTestingData(data.testing_data);
319 +
320 + // Add images if provided
321 + if (images && images.length > 0) {
322 + fullMessage += '<div class="image-gallery" dir="auto">'; // Add dir="auto" to image gallery
323 + images.forEach(img => {
324 + // Ensure image URLs and titles are properly escaped
325 + const safeTitle = sanitizeUserInput(img.title);
326 + const safeUrl = encodeURI(img.image_url);
327 + const safeThumbnail = encodeURI(img.thumbnail_url);
328 +
329 + fullMessage += `
330 + <div style="margin-bottom: 10px;">
331 + <strong>${safeTitle}</strong><br>
332 + <a href="${safeUrl}" target="_blank">
333 + <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
334 + </a>
335 + </div>`;
336 + });
337 + fullMessage += '</div>';
936 338 }
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;
339 +
340 + // Append HTML content if provided
341 + if (messageHtml && sender !== "user") {
342 + fullMessage += '<br><br>' + messageHtml;
343 + }
344 +
345 + messageDiv.html(fullMessage);
346 +
347 + if (isTemporary) {
348 + messageDiv.addClass('temporary-message');
349 + }
350 +
351 + messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
352 + if (sender === "bot") {
353 + const lastUserMessage = $('#chat-box').find('.user-message').last();
354 + if (lastUserMessage.length) {
355 + scrollElementToTop(lastUserMessage);
975 356 }
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 357 }
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 358 });
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);
359 +
360 + if (messageText.id) {
361 + lastSeenMessageId = messageText.id;
362 + hideNotification();
1098 363 }
1099 - });
1100 -}
1101 -
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.";
364 + } catch (error) {
365 + console.error("Error rendering message:", error);
1137 366 }
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 367 }
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 368
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 -
369 + function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
370 + var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
371 + var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1330 372
1331 -function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1332 - try {
1333 - // Determine styles based on sender type
1334 - let messageClass, bgColor, fontColor;
1335 -
373 + // Determine styles
374 + let bgColor, fontColor;
1336 375 if (sender === "user") {
1337 - messageClass = "user-message";
1338 376 bgColor = userMessageBgColor;
1339 377 fontColor = userMessageFontColor;
1340 - // Only sanitize user input
1341 - messageText = sanitizeUserInput(messageText);
1342 378 } else if (sender === "agent") {
1343 - messageClass = "agent-message";
1344 379 bgColor = liveAgentMessageBgColor;
1345 380 fontColor = liveAgentMessageFontColor;
1346 381 } else {
1347 - messageClass = "bot-message";
1348 382 bgColor = botMessageBgColor;
1349 383 fontColor = botMessageFontColor;
1350 384 }
1351 -
1352 - const messageDiv = $('<div>')
1353 - .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({
1364 - 'background': bgColor,
1365 - 'color': fontColor,
1366 - 'margin-bottom': '1em'
1367 - });
385 +
386 + var fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(responseText))));
387 + if (responseHtml) {
388 + fullMessage += '<br><br>' + responseHtml;
1368 389 }
1369 -
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 - // Add images if provided
1376 - if (images && images.length > 0) {
1377 - fullMessage += '<div class="image-gallery" dir="auto">';
390 +
391 + if (images.length > 0) {
392 + fullMessage += '<div class="image-gallery" dir="auto">'; // Add dir="auto" to image gallery
1378 393 images.forEach(img => {
1379 - const safeTitle = sanitizeUserInput(img.title);
1380 - const safeUrl = encodeURI(img.image_url);
1381 - const safeThumbnail = encodeURI(img.thumbnail_url);
1382 -
1383 394 fullMessage += `
1384 395 <div style="margin-bottom: 10px;">
1385 - <strong>${safeTitle}</strong><br>
1386 - <a href="${safeUrl}" target="_blank">
1387 - <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
396 + <strong>${img.title}</strong><br>
397 + <a href="${img.image_url}" target="_blank">
398 + <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
1388 399 </a>
1389 400 </div>`;
1390 401 });
1391 402 fullMessage += '</div>';
1392 403 }
1393 -
1394 - // Append HTML content if provided
1395 - 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 - }
1402 - }
1403 -
1404 - messageDiv.html(fullMessage);
1405 -
1406 - if (isTemporary) {
1407 - messageDiv.addClass('temporary-message');
1408 - }
1409 -
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 -
1418 - if (sender === "bot") {
1419 - const lastUserMessage = $chatBox.find('.user-message').last();
1420 - if (lastUserMessage.length) {
1421 - scrollElementToTop(lastUserMessage, botId);
1422 - }
1423 - }
1424 - });
1425 -
1426 - if (messageText.id) {
1427 - var instance = MxChatInstances.get(botId);
1428 - instance.lastSeenMessageId = messageText.id;
1429 - hideNotification(botId);
1430 - }
1431 - } catch (error) {
1432 - // Error rendering message - silently continue
1433 - }
1434 -}
1435 -
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;
404 +
405 + if (lastMessageDiv.length) {
406 + lastMessageDiv.fadeOut(200, function() {
407 + $(this)
408 + .html(fullMessage)
409 + .removeClass('bot-message user-message')
410 + .addClass(messageClass)
411 + .attr('dir', 'auto') // Add dir="auto" for automatic text direction
412 + .css({
413 + 'background-color': bgColor,
414 + 'color': fontColor,
415 + })
416 + .removeClass('temporary-message')
417 + .fadeIn(200, function() {
418 + if (sender === "bot" || sender === "agent") {
419 + const lastUserMessage = $('#chat-box').find('.user-message').last();
420 + if (lastUserMessage.length) {
421 + scrollElementToTop(lastUserMessage);
1481 422 }
423 + // Show notification if chat is hidden
424 + if ($('#floating-chatbot').hasClass('hidden')) {
425 + showNotification();
426 + }
1482 427 }
1483 428 });
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') {
1493 - 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();
1496 -
1497 - // Determine styles
1498 - let bgColor, fontColor;
1499 - if (sender === "user") {
1500 - bgColor = userMessageBgColor;
1501 - fontColor = userMessageFontColor;
1502 - } else if (sender === "agent") {
1503 - bgColor = liveAgentMessageBgColor;
1504 - fontColor = liveAgentMessageFontColor;
1505 - } else {
1506 - bgColor = botMessageBgColor;
1507 - fontColor = botMessageFontColor;
1508 - }
1509 -
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 -
1515 - 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;
429 + });
1519 430 } else {
1520 - fullMessage = responseHtml;
431 + appendMessage(sender, responseText, responseHtml, images);
1521 432 }
1522 433 }
434 +
435 + function appendThinkingMessage() {
436 + // Remove any existing thinking dots first
437 + $('.thinking-dots').remove();
1523 438
1524 - if (images.length > 0) {
1525 - fullMessage += '<div class="image-gallery" dir="auto">';
1526 - images.forEach(img => {
1527 - fullMessage += `
1528 - <div style="margin-bottom: 10px;">
1529 - <strong>${img.title}</strong><br>
1530 - <a href="${img.image_url}" target="_blank">
1531 - <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
1532 - </a>
1533 - </div>`;
1534 - });
1535 - fullMessage += '</div>';
1536 - }
1537 -
1538 - 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);
1572 - } else {
1573 - appendMessage(sender, responseText, responseHtml, images, false, botId);
1574 - // Re-enable chat input after response is displayed
1575 - enableChatInput(botId);
1576 - }
1577 -}
1578 -
1579 -
1580 - function appendThinkingMessage(botId) {
1581 - botId = botId || 'default';
1582 -
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 439 // Retrieve the bot message font color and background color
1598 440 var botMessageFontColor = mxchatChat.bot_message_font_color;
1599 441 var botMessageBgColor = mxchatChat.bot_message_bg_color;
1600 442
1601 - // Build thinking dots HTML - skip inline colors if AI theme is active
1602 - var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
443 +
1603 444 var thinkingHtml = '<div class="thinking-dots-container">' +
1604 445 '<div class="thinking-dots">' +
1605 - '<span class="dot"' + dotStyle + '></span>' +
1606 - '<span class="dot"' + dotStyle + '></span>' +
1607 - '<span class="dot"' + dotStyle + '></span>' +
446 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
447 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
448 + '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1608 449 '</div>' +
1609 450 '</div>';
1610 451
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);
452 + // Append the thinking dots to the chat container (or within the temporary message div)
453 + $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
454 + scrollToBottom();
1615 455 }
456 +
457 + function removeThinkingDots() {
458 + $('.thinking-dots').closest('.temporary-message').remove();
459 + }
1616 460
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 461
1625 462 // ====================================
1626 463 // TEXT FORMATTING & PROCESSING
1627 464 // ====================================
@@ -1626,332 +463,82 @@
1626 463 // TEXT FORMATTING & PROCESSING
1627 464 // ====================================
1628 465
1629 466 function linkify(inputText) {
1630 - if (!inputText) {
1631 - return '';
1632 - }
467 + if (!inputText) return '';
468 +
469 + // Process markdown headers
470 + let processedText = formatMarkdownHeaders(inputText);
471 +
472 + // Process markdown links
473 + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s]+)\)/g;
474 + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
475 + const safeUrl = encodeURI(url);
476 + const safeText = sanitizeUserInput(text);
477 + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
478 + });
1633 479
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 - }
480 + // Process phone numbers (tel:)
481 + const phonePattern = /\[([^\]]+)\]\((tel:[\d+]+)\)/g;
482 + processedText = processedText.replace(phonePattern, (match, text, phone) => {
483 + const safePhone = encodeURI(phone);
484 + const safeText = sanitizeUserInput(text);
485 + return `<a href="${safePhone}">${safeText}</a>`;
486 + });
1639 487
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 - }
488 + // Process standalone URLs
489 + const urlPattern = /(^|[^">])(https?:\/\/[^\s<]+)/gim;
490 + processedText = processedText.replace(urlPattern, (match, prefix, url) => {
491 + const safeUrl = encodeURI(url);
492 + return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
493 + });
1649 494
1650 - // Process markdown headers FIRST
1651 - let processedText = formatMarkdownHeaders(inputText);
495 + // Process www. URLs
496 + const wwwPattern = /(^|[^">])(www\.[\S]+(\b|$))(?![^<]*<\/a>)/gim;
497 + processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
498 + const safeUrl = encodeURI(`http://${url}`);
499 + return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
500 + });
1652 501
1653 - // Process text styling (bold, italic, strikethrough)
1654 - processedText = formatTextStyling(processedText);
502 + return processedText;
503 + }
1655 504
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;
1777 -}
1778 -
1779 505 function formatMarkdownHeaders(text) {
1780 506 // Handle h1 to h6 headers
1781 - return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
507 + return text.replace(/^(#{1,6})\s(.+)$/gm, function(match, hashes, content) {
1782 508 const level = hashes.length;
1783 - return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`;
509 + return `<h${level} class="chat-heading">${content}</h${level}>`;
1784 510 });
1785 511 }
1786 512
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 -
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 513 function formatBoldText(text) {
1838 - // This function is kept for compatibility but now uses formatTextStyling
1839 - return formatTextStyling(text);
514 + return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1840 515 }
1841 516
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);
517 + function convertNewlinesToBreaks(text) {
518 + // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
519 + const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
520 +
521 + // Wrap each paragraph in <p> tags
522 + return paragraphs
523 + .map(para => `<p>${para.trim()}</p>`)
524 + .join('');
525 + }
1845 526
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 -}
1853 527 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>`;
528 + // First handle raw PHP tags
529 + text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
530 + return `<pre><code class="language-php">${escapeHtml(match)}</code></pre>`;
1865 531 });
1866 -
1867 - // Handle inline code with single backticks
1868 - text = text.replace(/`([^`\n]+)`/g, '<code class="mxchat-inline-code">$1</code>');
1869 -
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>`;
532 +
533 + // Then handle code blocks with backticks
534 + text = text.replace(/```php5?\n([\s\S]+?)```/gi, (match, code) => {
535 + return `<pre><code class="language-php">${escapeHtml(code)}</code></pre>`;
1880 536 });
1881 -
537 +
1882 538 return text;
1883 539 }
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 -
540 +
1954 541 function sanitizeUserInput(text) {
1955 542 const div = document.createElement('div');
1956 543 div.textContent = text;
1957 544 return div.innerHTML;
@@ -1956,12 +543,12 @@
1956 543 div.textContent = text;
1957 544 return div.innerHTML;
1958 545 }
1959 546
547 +
1960 548 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>')) {
549 + // First check if it's already a code block
550 + if (unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
1964 551 return unsafe;
1965 552 }
1966 553
1967 554 return unsafe
@@ -1977,25 +564,15 @@
1977 564 textArea.innerHTML = text;
1978 565 return textArea.value;
1979 566 }
1980 567
568 +
1981 569 // ====================================
1982 570 // UI & SCROLLING CONTROLS
1983 571 // ====================================
1984 572
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');
573 + function scrollToBottom(instant = false) {
574 + var chatBox = $('#chat-box');
1998 575 if (instant) {
1999 576 // Instantly set the scroll position to the bottom
2000 577 chatBox.scrollTop(chatBox.prop("scrollHeight"));
2001 578 } else {
@@ -2004,15 +581,15 @@
2004 581 const scrollHeight = chatBox.prop("scrollHeight");
2005 582 const initialScroll = chatBox.scrollTop();
2006 583 const distance = scrollHeight - initialScroll;
2007 584 const duration = 500; // Duration in ms
2008 -
585 +
2009 586 function smoothScroll(timestamp) {
2010 587 if (!start) start = timestamp;
2011 588 const progress = timestamp - start;
2012 589 const currentScroll = initialScroll + (distance * (progress / duration));
2013 590 chatBox.scrollTop(currentScroll);
2014 -
591 +
2015 592 if (progress < duration) {
2016 593 requestAnimationFrame(smoothScroll);
2017 594 } else {
2018 595 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
@@ -2017,37 +594,33 @@
2017 594 } else {
2018 595 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
2019 596 }
2020 597 }
2021 -
598 +
2022 599 requestAnimationFrame(smoothScroll);
2023 600 }
2024 601 }
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');
602 +
603 + function scrollElementToTop(element) {
604 + var chatBox = $('#chat-box');
2030 605 var elementTop = element.position().top + chatBox.scrollTop();
2031 - chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
606 + chatBox.animate({ scrollTop: elementTop }, 500);
2032 607 }
2033 -
2034 - function showChatWidget(botId) {
2035 - botId = botId || 'default';
2036 - var $button = getElement(botId, 'floating-chatbot-button');
608 +
609 + function showChatWidget() {
2037 610 // First ensure display is set
2038 - $button.css('display', 'flex');
611 + $('#floating-chatbot-button').css('display', 'flex');
2039 612 // Then handle the fade
2040 - $button.fadeTo(500, 1);
613 + $('#floating-chatbot-button').fadeTo(500, 1);
2041 614 // Force visibility
2042 - $button.removeClass('hidden');
615 + $('#floating-chatbot-button').removeClass('hidden');
616 + //console.log('Showing widget');
2043 617 }
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');
618 +
619 + function hideChatWidget() {
620 + $('#floating-chatbot-button').css('display', 'none');
621 + $('#floating-chatbot-button').addClass('hidden');
622 + //console.log('Hiding widget');
2050 623 }
2051 624
2052 625 function disableScroll() {
2053 626 if (isMobile()) {
@@ -2076,15 +649,18 @@
2076 649 // NOTIFICATION SYSTEM
2077 650 // ====================================
2078 651
2079 652 function createNotificationBadge() {
653 + //console.log("Creating notification badge...");
2080 654 const chatButton = document.getElementById('floating-chatbot-button');
2081 -
655 + //console.log("Chat button found:", !!chatButton);
656 +
2082 657 if (!chatButton) return;
2083 -
658 +
2084 659 // Remove any existing badge first
2085 660 const existingBadge = chatButton.querySelector('.chat-notification-badge');
2086 661 if (existingBadge) {
662 + //console.log("Removing existing badge");
2087 663 existingBadge.remove();
2088 664 }
2089 665
2090 666 notificationBadge = document.createElement('div');
@@ -2106,43 +682,34 @@
2106 682 chatButton.appendChild(notificationBadge);
2107 683
2108 684 }
2109 685
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')) {
686 + function showNotification() {
687 + const badge = document.getElementById('chat-notification-badge');
688 + if (badge && $('#floating-chatbot').hasClass('hidden')) {
2115 689 badge.style.display = 'block';
2116 690 badge.textContent = '1';
2117 691 }
2118 692 }
2119 -
2120 - function hideNotification(botId) {
2121 - botId = botId || 'default';
2122 - const badge = getElementDOM(botId, 'chat-notification-badge');
693 +
694 + function hideNotification() {
695 + const badge = document.getElementById('chat-notification-badge');
2123 696 if (badge) {
2124 697 badge.style.display = 'none';
2125 698 }
2126 699 }
2127 -
2128 - function startNotificationChecking(botId) {
2129 - botId = botId || 'default';
700 +
701 + function startNotificationChecking() {
2130 702 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2131 703 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
704 +
705 + createNotificationBadge();
706 + notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
2138 707 }
2139 -
2140 - function stopNotificationChecking(botId) {
2141 - botId = botId || 'default';
2142 - var instance = MxChatInstances.get(botId);
2143 - if (instance.notificationCheckInterval) {
2144 - clearInterval(instance.notificationCheckInterval);
708 +
709 + function stopNotificationChecking() {
710 + if (notificationCheckInterval) {
711 + clearInterval(notificationCheckInterval);
2145 712 }
2146 713 }
2147 714
2148 715 function checkForNewMessages() {
@@ -2168,243 +735,202 @@
2168 735 });
2169 736 }
2170 737
2171 738
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);
2199 - $.ajax({
2200 - url: mxchatChat.ajax_url,
2201 - type: 'POST',
2202 - dataType: 'json',
2203 - data: {
2204 - action: 'mxchat_fetch_new_messages',
2205 - session_id: sessionId,
2206 - last_seen_id: instance.lastSeenMessageId,
2207 - persistence_enabled: 'true',
2208 - nonce: mxchatChat.nonce
2209 - },
2210 - success: function (response) {
2211 - if (response.success && response.data?.new_messages) {
2212 - let hasNewMessage = false;
2213 -
2214 - response.data.new_messages.forEach(function (message) {
2215 - if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) {
2216 - hasNewMessage = true;
2217 - appendMessage("agent", message.content, '', [], false, botId);
2218 - instance.lastSeenMessageId = message.id;
2219 - instance.processedMessageIds.add(message.id);
2220 - }
2221 - });
2222 -
2223 - if (hasNewMessage) {
2224 - enableChatInput(botId);
2225 - }
2226 -
2227 - var $floatingChatbot = getElement(botId, 'floating-chatbot');
2228 - if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
2229 - showNotification(botId);
2230 - }
2231 -
2232 - scrollToBottom(botId, true);
739 + // ====================================
740 + // LIVE AGENT FUNCTIONALITY
741 + // ====================================
742 +
743 + function updateChatModeIndicator(mode) {
744 + const indicator = document.getElementById('chat-mode-indicator');
745 + if (indicator) {
746 + // For Live Agent, keep as is; for AI mode, use the customized text
747 + if (mode === 'agent') {
748 + indicator.textContent = 'Live Agent';
749 + } else {
750 + // Get the custom AI agent text from a data attribute we'll add to the element
751 + const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
752 + indicator.textContent = customAiText;
2233 753 }
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 - },
2240 - error: function (xhr, status, error) {
2241 - // Polling error - silently continue
2242 754 }
2243 - });
2244 -}
2245 -
2246 - // ====================================
2247 - // CHAT HISTORY & PERSISTENCE
2248 - // ====================================
2249 -
2250 -function loadChatHistory(botId, onComplete) {
2251 - botId = botId || 'default';
2252 - var instance = MxChatInstances.get(botId);
2253 -
2254 - // Prevent duplicate loading
2255 - if (instance.chatHistoryLoaded) {
2256 - if (onComplete) onComplete();
2257 - return;
755 + // Start or stop polling based on mode
756 + if (mode === 'agent') {
757 + startPolling();
758 + } else {
759 + stopPolling();
760 + }
2258 761 }
2259 -
2260 - // Use getChatSession which returns null if no session exists (does NOT create one)
2261 - var sessionId = getChatSession(botId);
2262 - var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2263 -
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;
762 +
763 + function startPolling() {
764 + // Clear any existing interval first
765 + stopPolling();
766 + // Start new polling interval
767 + pollingInterval = setInterval(checkForAgentMessages, 5000);
768 + //console.log("Started agent message polling");
2269 769 }
2270 -
2271 - if (chatPersistenceEnabled && sessionId) {
770 +
771 + function stopPolling() {
772 + if (pollingInterval) {
773 + clearInterval(pollingInterval);
774 + pollingInterval = null;
775 + //console.log("Stopped agent message polling");
776 + }
777 + }
778 +
779 + function checkForAgentMessages() {
780 + const sessionId = getChatSession();
2272 781 $.ajax({
2273 782 url: mxchatChat.ajax_url,
2274 783 type: 'POST',
2275 784 dataType: 'json',
2276 785 data: {
2277 - action: 'mxchat_fetch_conversation_history',
2278 - session_id: sessionId
786 + action: 'mxchat_fetch_new_messages',
787 + session_id: sessionId,
788 + last_seen_id: lastSeenMessageId,
789 + nonce: mxchatChat.nonce
2279 790 },
2280 - 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;
791 + success: function (response) {
792 + if (response.success && response.data?.new_messages) {
793 + let hasNewMessage = false;
794 +
795 + response.data.new_messages.forEach(function (message) {
796 + if (message.role === "agent" && !processedMessageIds.has(message.id)) {
797 + hasNewMessage = true;
798 + replaceLastMessage("agent", message.content);
799 + lastSeenMessageId = message.id;
800 + processedMessageIds.add(message.id);
801 + }
802 + });
803 +
804 + if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
805 + showNotification();
806 + }
807 +
808 + scrollToBottom(true);
2288 809 }
810 + },
811 + error: function (xhr, status, error) {
812 + console.error("Polling error:", xhr, status, error);
813 + }
814 + });
815 + }
2289 816
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');
817 +
818 + // ====================================
819 + // CHAT HISTORY & PERSISTENCE
820 + // ====================================
821 +
822 + function loadChatHistory() {
823 + var sessionId = getChatSession();
824 + var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
825 +
826 + if (chatPersistenceEnabled && sessionId) {
827 + $.ajax({
828 + url: mxchatChat.ajax_url,
829 + type: 'POST',
830 + dataType: 'json',
831 + data: {
832 + action: 'mxchat_fetch_conversation_history',
833 + session_id: sessionId
834 + },
835 + success: function(response) {
836 + if (response.success && response.data && Array.isArray(response.data.conversation)) {
837 +
838 +
839 + var $chatBox = $('#chat-box');
2295 840 var $fragment = $(document.createDocumentFragment());
2296 - let highestMessageId = instance.lastSeenMessageId;
2297 -
2298 - // Update chat mode if provided
841 + let highestMessageId = lastSeenMessageId;
842 +
2299 843 if (response.data.chat_mode) {
2300 - updateChatModeIndicator(response.data.chat_mode, botId);
844 + updateChatModeIndicator(response.data.chat_mode);
2301 845 }
2302 -
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();
2307 -
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 - }
2313 -
2314 - var messageClass, messageBgColor, messageFontColor;
2315 -
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 - }
2333 -
2334 - var messageElement = $('<div>').addClass(messageClass)
2335 - .css({
2336 - 'background': messageBgColor,
2337 - 'color': messageFontColor
2338 - });
2339 -
2340 - var content = message.content;
2341 - content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2342 - content = decodeHTMLEntities(content);
2343 -
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 - }
2360 -
2361 - $fragment.append(messageElement);
2362 -
2363 - // Track message IDs
846 +
847 + $.each(response.data.conversation, function(index, message) {
848 + // Skip agent messages if persistence is off
849 + if (!chatPersistenceEnabled && message.role === 'agent') {
850 + return;
851 + }
852 +
853 + var messageClass, messageBgColor, messageFontColor;
854 +
855 + switch (message.role) {
856 + case 'user':
857 + messageClass = 'user-message';
858 + messageBgColor = userMessageBgColor;
859 + messageFontColor = userMessageFontColor;
860 + break;
861 + case 'agent':
862 + messageClass = 'agent-message';
863 + messageBgColor = liveAgentMessageBgColor;
864 + messageFontColor = liveAgentMessageFontColor;
865 + break;
866 + default:
867 + messageClass = 'bot-message';
868 + messageBgColor = botMessageBgColor;
869 + messageFontColor = botMessageFontColor;
870 + break;
871 + }
872 +
873 + var messageElement = $('<div>').addClass(messageClass)
874 + .css({
875 + 'background': messageBgColor,
876 + 'color': messageFontColor
877 + });
878 +
879 + var content = message.content;
880 + content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
881 + content = decodeHTMLEntities(content);
882 +
883 + if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
884 + messageElement.html(content);
885 + } else {
886 + var formattedContent = linkify(
887 + formatBoldText(
888 + convertNewlinesToBreaks(formatCodeBlocks(content))
889 + )
890 + );
891 + messageElement.html(formattedContent);
892 + }
893 +
894 + $fragment.append(messageElement);
895 +
896 + // In loadChatHistory, change this part:
2364 897 if (message.id) {
2365 898 highestMessageId = Math.max(highestMessageId, message.id);
2366 - instance.processedMessageIds.add(message.id);
899 + processedMessageIds.add(message.id); // Add all message IDs to processed set
2367 900 }
2368 - });
2369 -
2370 - // Only append messages and scroll if we have content
2371 - $chatBox.append($fragment);
2372 - scrollToBottom(botId, true);
2373 -
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);
901 + });
902 +
903 + $chatBox.append($fragment);
904 + scrollToBottom(true);
905 +
906 + if (response.data.conversation.length > 0) {
907 + $('#mxchat-popular-questions').hide();
908 + }
909 +
910 + // Update lastSeenMessageId after history loads
911 + lastSeenMessageId = highestMessageId;
912 +
913 + // Only update chat mode if persistence is enabled
914 + if (chatPersistenceEnabled && response.data.conversation.length > 0) {
915 + var lastMessage = response.data.conversation[response.data.conversation.length - 1];
916 + if (lastMessage.role === 'agent') {
917 + updateChatModeIndicator('agent');
2378 918 }
2379 -
2380 - // Update lastSeenMessageId after history loads
2381 - instance.lastSeenMessageId = highestMessageId;
2382 -
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;
2393 919 }
920 + } else {
921 + console.warn("No conversation history found.");
2394 922 }
923 + },
924 + error: function(xhr, status, error) {
925 + console.error("Error loading chat history:", status, error);
926 + appendMessage("bot", "Unable to load chat history.");
2395 927 }
2396 - if (onComplete) onComplete();
2397 - },
2398 - error: function(xhr, status, error) {
2399 - // Error loading chat history - silently continue
2400 - if (onComplete) onComplete();
2401 - }
2402 - });
2403 - } else {
2404 - if (onComplete) onComplete();
928 + });
929 + } else {
930 + console.warn("Chat persistence is disabled or no session ID found. Not loading history.");
931 + }
2405 932 }
2406 -}
2407 933
2408 934
2409 935 // ====================================
2410 936 // FILE UPLOAD FUNCTIONALITY
@@ -2416,42 +942,40 @@
2416 942 element.addEventListener(eventType, handler);
2417 943 }
2418 944 }
2419 945
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');
2424 -
946 + function showActivePdf(filename) {
947 + const container = document.getElementById('active-pdf-container');
948 + const nameElement = document.getElementById('active-pdf-name');
949 +
2425 950 if (!container || !nameElement) {
951 + console.error('PDF container elements not found');
2426 952 return;
2427 953 }
2428 -
954 +
2429 955 nameElement.textContent = filename;
2430 956 container.style.display = 'flex';
2431 957 }
2432 -
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 -
958 +
959 + function showActiveWord(filename) {
960 + const container = document.getElementById('active-word-container');
961 + const nameElement = document.getElementById('active-word-name');
962 +
2438 963 if (!container || !nameElement) {
964 + console.error('Word document container elements not found');
2439 965 return;
2440 966 }
2441 -
967 +
2442 968 nameElement.textContent = filename;
2443 969 container.style.display = 'flex';
2444 970 }
2445 -
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 -
2452 - if (!container || !nameElement || !instance.activePdfFile) return;
2453 -
971 +
972 + function removeActivePdf() {
973 + const container = document.getElementById('active-pdf-container');
974 + const nameElement = document.getElementById('active-pdf-name');
975 +
976 + if (!container || !nameElement || !activePdfFile) return;
977 +
2454 978 fetch(mxchatChat.ajax_url, {
2455 979 method: 'POST',
2456 980 headers: {
2457 981 'Content-Type': 'application/x-www-form-urlencoded',
@@ -2457,9 +981,9 @@
2457 981 'Content-Type': 'application/x-www-form-urlencoded',
2458 982 },
2459 983 body: new URLSearchParams({
2460 984 'action': 'mxchat_remove_pdf',
2461 - 'session_id': getChatSession(botId),
985 + 'session_id': sessionId,
2462 986 'nonce': mxchatChat.nonce
2463 987 })
2464 988 })
2465 989 .then(response => response.json())
@@ -2471,12 +995,12 @@
2471 995 appendMessage('bot', 'PDF removed.');
2472 996 }
2473 997 })
2474 998 .catch(error => {
2475 - // Error removing PDF - silently continue
999 + console.error('Error removing PDF:', error);
2476 1000 });
2477 1001 }
2478 -
1002 +
2479 1003 function removeActiveWord() {
2480 1004 const container = document.getElementById('active-word-container');
2481 1005 const nameElement = document.getElementById('active-word-name');
2482 1006
@@ -2502,53 +1026,107 @@
2502 1026 appendMessage('bot', 'Word document removed.');
2503 1027 }
2504 1028 })
2505 1029 .catch(error => {
2506 - // Error removing Word document - silently continue
1030 + console.error('Error removing Word document:', error);
2507 1031 });
2508 1032 }
1033 +
1034 + function checkInitialDocumentStatus() {
1035 + if (!sessionId) return;
1036 +
1037 + // Check PDF status
1038 + fetch(mxchatChat.ajax_url, {
1039 + method: 'POST',
1040 + headers: {
1041 + 'Content-Type': 'application/x-www-form-urlencoded',
1042 + },
1043 + body: new URLSearchParams({
1044 + 'action': 'mxchat_check_pdf_status',
1045 + 'session_id': sessionId,
1046 + 'nonce': mxchatChat.nonce
1047 + })
1048 + })
1049 + .then(response => response.json())
1050 + .then(data => {
1051 + if (data.success && data.data.filename) {
1052 + showActivePdf(data.data.filename);
1053 + activePdfFile = data.data.filename;
1054 + }
1055 + })
1056 + .catch(error => {
1057 + console.error('Error checking PDF status:', error);
1058 + });
1059 +
1060 + // Check Word document status
1061 + fetch(mxchatChat.ajax_url, {
1062 + method: 'POST',
1063 + headers: {
1064 + 'Content-Type': 'application/x-www-form-urlencoded',
1065 + },
1066 + body: new URLSearchParams({
1067 + 'action': 'mxchat_check_word_status',
1068 + 'session_id': sessionId,
1069 + 'nonce': mxchatChat.nonce
1070 + })
1071 + })
1072 + .then(response => response.json())
1073 + .then(data => {
1074 + if (data.success && data.data.filename) {
1075 + showActiveWord(data.data.filename);
1076 + activeWordFile = data.data.filename;
1077 + }
1078 + })
1079 + .catch(error => {
1080 + console.error('Error checking Word document status:', error);
1081 + });
1082 + }
2509 1083
1084 +
2510 1085 // ====================================
2511 1086 // CONSENT & COMPLIANCE (GDPR)
2512 1087 // ====================================
2513 -
2514 - function initializeChatVisibility(botId) {
2515 - botId = botId || 'default';
2516 - const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
2517 - mxchatChat.complianz_toggle === '1' ||
1088 +
1089 + function initializeChatVisibility() {
1090 + //console.log('Initializing chat visibility');
1091 + const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
1092 + mxchatChat.complianz_toggle === '1' ||
2518 1093 mxchatChat.complianz_toggle === 1;
2519 -
1094 +
2520 1095 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
2521 1096 // Initial check
2522 - checkConsentAndShowChat(botId);
2523 -
1097 + checkConsentAndShowChat();
1098 +
2524 1099 // Listen for consent changes
2525 1100 $(document).on('cmplz_status_change', function(event) {
2526 - checkConsentAndShowChat(botId);
1101 + //console.log('Status change detected');
1102 + checkConsentAndShowChat();
2527 1103 });
2528 1104 } else {
2529 1105 // If Complianz is not enabled, always show
2530 - getElement(botId, 'floating-chatbot-button')
1106 + $('#floating-chatbot-button')
2531 1107 .css('display', 'flex')
2532 1108 .removeClass('hidden no-consent')
2533 1109 .fadeTo(500, 1);
2534 -
1110 +
2535 1111 // Also check pre-chat message when Complianz is not enabled
2536 - checkPreChatDismissal(botId);
1112 + checkPreChatDismissal();
2537 1113 }
2538 1114 }
2539 1115
2540 -
2541 - function checkConsentAndShowChat(botId) {
2542 - botId = botId || 'default';
1116 +
1117 + function checkConsentAndShowChat() {
2543 1118 var consentStatus = cmplz_has_consent('marketing');
2544 1119 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 -
1120 +
1121 + //console.log('Checking consent:', {status: consentStatus,type: consentType});
1122 +
1123 + let $widget = $('#floating-chatbot-button');
1124 + let $chatbot = $('#floating-chatbot');
1125 + let $preChat = $('#pre-chat-message');
1126 +
2550 1127 if (consentStatus === true) {
1128 + //console.log('Consent granted - showing widget');
2551 1129 $widget
2552 1130 .removeClass('no-consent')
2553 1131 .css('display', 'flex')
2554 1132 .removeClass('hidden')
@@ -2553,12 +1131,13 @@
2553 1131 .css('display', 'flex')
2554 1132 .removeClass('hidden')
2555 1133 .fadeTo(500, 1);
2556 1134 $chatbot.removeClass('no-consent');
2557 -
1135 +
2558 1136 // Show pre-chat message if not dismissed
2559 - checkPreChatDismissal(botId);
1137 + checkPreChatDismissal();
2560 1138 } else {
1139 + //console.log('No consent - hiding widget');
2561 1140 $widget
2562 1141 .addClass('no-consent')
2563 1142 .fadeTo(500, 0, function() {
2564 1143 $(this)
@@ -2565,9 +1144,9 @@
2565 1144 .css('display', 'none')
2566 1145 .addClass('hidden');
2567 1146 });
2568 1147 $chatbot.addClass('no-consent');
2569 -
1148 +
2570 1149 // Hide pre-chat message when no consent
2571 1150 $preChat.hide();
2572 1151 }
2573 1152 }
@@ -2575,38 +1154,46 @@
2575 1154
2576 1155 // ====================================
2577 1156 // PRE-CHAT MESSAGE HANDLING
2578 1157 // ====================================
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;
1158 +
1159 + function checkPreChatDismissal() {
1160 + $.ajax({
1161 + url: mxchatChat.ajax_url,
1162 + type: 'POST',
1163 + data: {
1164 + action: 'mxchat_check_pre_chat_message_status',
1165 + _ajax_nonce: mxchatChat.nonce
1166 + },
1167 + success: function(response) {
1168 + if (response.success && !response.data.dismissed) {
1169 + $('#pre-chat-message').fadeIn(250);
1170 + } else {
1171 + $('#pre-chat-message').hide();
2590 1172 }
2591 - // Expired — clear and show again
2592 - localStorage.removeItem('mxchat_pre_chat_dismissed_' + botId);
1173 + },
1174 + error: function() {
1175 + console.error('Failed to check pre-chat message dismissal status.');
2593 1176 }
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 - }
1177 + });
2599 1178 }
2600 -
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 - }
1179 +
1180 + function handlePreChatDismissal() {
1181 + $('#pre-chat-message').fadeOut(200);
1182 + $.ajax({
1183 + url: mxchatChat.ajax_url,
1184 + type: 'POST',
1185 + data: {
1186 + action: 'mxchat_dismiss_pre_chat_message',
1187 + _ajax_nonce: mxchatChat.nonce
1188 + },
1189 + success: function() {
1190 + $('#pre-chat-message').hide();
1191 + },
1192 + error: function() {
1193 + console.error('Failed to dismiss pre-chat message.');
1194 + }
1195 + });
2609 1196 }
2610 1197
2611 1198
2612 1199 // ====================================
@@ -2630,106 +1217,90 @@
2630 1217 // ====================================
2631 1218 // EVENT HANDLERS & INITIALIZATION
2632 1219 // ====================================
2633 1220
2634 -$(document).on('click', '.mxchat-popular-question', function () {
2635 - var question = $(this).text();
2636 - var botId = getBotIdFromElement(this);
1221 + // Enter key handler for chat input
1222 + $('#chat-input').keypress(function(e) {
1223 + if (e.which == 13 && !e.shiftKey) { // Check if "Enter" is pressed without Shift
1224 + e.preventDefault(); // Prevent default "Enter" behavior
1225 + $('#send-button').click(); // Trigger send button click
1226 + }
1227 + });
1228 +
1229 + // Send button click handler
1230 + $('#send-button').click(function() {
1231 + sendMessage();
1232 + });
1233 +
1234 + // Popular questions click handler
1235 + $('.mxchat-popular-question').on('click', function () {
1236 + var question = $(this).text(); // Get the text of the clicked question
2637 1237
2638 - // Append the question as if the user typed it
2639 - appendMessage("user", question, '', [], false, botId);
1238 + // Append the question as if the user typed it
1239 + appendMessage("user", question);
2640 1240
2641 - // Only collapse if there are questions
2642 - if (hasQuickQuestions(botId)) {
2643 - collapseQuickQuestions(botId);
2644 - }
2645 -
2646 - // Send the question to the server
2647 - sendMessageToChatbot(question, botId);
2648 -});
2649 -
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 -});
1241 + // Send the question to the server (backend)
1242 + sendMessageToChatbot(question);
1243 + });
2663 1244
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');
1245 + // Pre-chat message dismissal handlers
1246 + $(document).on('click', '.close-pre-chat-message', function(e) {
1247 + e.stopPropagation();
1248 + handlePreChatDismissal();
1249 + });
1250 +
1251 + // Chatbot visibility toggle handlers
1252 + $(document).on('click', '#floating-chatbot-button', function() {
1253 + var chatbot = $('#floating-chatbot');
1254 + if (chatbot.hasClass('hidden')) {
1255 + chatbot.removeClass('hidden').addClass('visible');
2673 1256 $(this).addClass('hidden');
2674 - $badge.hide(); // Hide notification when opening chat
1257 + $('#chat-notification-badge').hide(); // Hide notification when opening chat
2675 1258 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 - }
1259 + $('#pre-chat-message').fadeOut(250);
2695 1260 } else {
2696 - $chatbot.removeClass('visible').addClass('hidden');
1261 + chatbot.removeClass('visible').addClass('hidden');
2697 1262 $(this).removeClass('hidden');
2698 1263 enableScroll();
2699 - checkPreChatDismissal(botId);
1264 + checkPreChatDismissal();
2700 1265 }
2701 1266 });
2702 -
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');
1267 +
1268 + $(document).on('click', '#exit-chat-button', function() {
1269 + $('#floating-chatbot').addClass('hidden').removeClass('visible');
1270 + $('#floating-chatbot-button').removeClass('hidden');
2708 1271 enableScroll();
2709 1272 });
2710 -
1273 +
2711 1274 $(document).on('click', '.close-pre-chat-message', function(e) {
2712 1275 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2713 - var botId = getBotIdFromElement(this);
2714 - handlePreChatDismissal(botId);
1276 + $('#pre-chat-message').fadeOut(200, function() {
1277 + $(this).remove();
1278 + });
2715 1279 });
2716 -
2717 -
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();
1280 +
1281 + // Add to Cart button handler
1282 + $(document).on('click', '.mxchat-add-to-cart-button', function() {
1283 + var productId = $(this).data('product-id');
1284 + // Add a special prefix to indicate this is from button
1285 + appendMessage("user", "add to cart");
1286 + sendMessageToChatbot("!addtocart"); // Special command to indicate button click
2723 1287 });
2724 -
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 1288
1289 + // PDF upload button handlers
1290 + if (document.getElementById('pdf-upload-btn')) {
1291 + document.getElementById('pdf-upload-btn').addEventListener('click', function() {
1292 + document.getElementById('pdf-upload').click();
1293 + });
1294 + }
1295 +
1296 + // Word upload button handlers
1297 + if (document.getElementById('word-upload-btn')) {
1298 + document.getElementById('word-upload-btn').addEventListener('click', function() {
1299 + document.getElementById('word-upload').click();
1300 + });
1301 + }
1302 +
2732 1303 // PDF file input change handler
2733 1304 addSafeEventListener('pdf-upload', 'change', async function(e) {
2734 1305 const file = e.target.files[0];
2735 1306
@@ -2738,13 +1309,15 @@
2738 1309 return;
2739 1310 }
2740 1311
2741 1312 if (!sessionId) {
1313 + console.error('No session ID found');
2742 1314 alert('Error: No session ID found');
2743 1315 return;
2744 1316 }
2745 -
1317 +
2746 1318 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
1319 + console.error('mxchatChat not properly configured:', mxchatChat);
2747 1320 alert('Error: Ajax configuration missing');
2748 1321 return;
2749 1322 }
2750 1323
@@ -2775,12 +1348,12 @@
2775 1348
2776 1349 if (data.success) {
2777 1350 // Hide popular questions if they exist
2778 1351 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2779 - if (hasQuickQuestions()) {
2780 - collapseQuickQuestions();
1352 + if (popularQuestionsContainer) {
1353 + popularQuestionsContainer.style.display = 'none';
2781 1354 }
2782 -
1355 +
2783 1356 // Show the active PDF name
2784 1357 showActivePdf(data.data.filename);
2785 1358
2786 1359 appendMessage('bot', data.data.message);
@@ -2786,11 +1359,13 @@
2786 1359 appendMessage('bot', data.data.message);
2787 1360 scrollToBottom();
2788 1361 activePdfFile = data.data.filename;
2789 1362 } else {
1363 + console.error('Upload failed:', data.data);
2790 1364 alert('Failed to upload PDF. Please try again.');
2791 1365 }
2792 1366 } catch (error) {
1367 + console.error('Upload error:', error);
2793 1368 alert('Error uploading file. Please try again.');
2794 1369 } finally {
2795 1370 uploadBtn.disabled = false;
2796 1371 sendBtn.disabled = false;
@@ -2808,12 +1383,13 @@
2808 1383 return;
2809 1384 }
2810 1385
2811 1386 if (!sessionId) {
1387 + console.error('No session ID found');
2812 1388 alert('Error: No session ID found');
2813 1389 return;
2814 1390 }
2815 -
1391 +
2816 1392 // Disable buttons and show loading state
2817 1393 const uploadBtn = document.getElementById('word-upload-btn');
2818 1394 const sendBtn = document.getElementById('send-button');
2819 1395 const originalBtnContent = uploadBtn.innerHTML;
@@ -2840,10 +1416,10 @@
2840 1416
2841 1417 if (data.success) {
2842 1418 // Hide popular questions if they exist
2843 1419 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2844 - if (hasQuickQuestions()) {
2845 - collapseQuickQuestions();
1420 + if (popularQuestionsContainer) {
1421 + popularQuestionsContainer.style.display = 'none';
2846 1422 }
2847 1423
2848 1424 // Show the active Word document name
2849 1425 showActiveWord(data.data.filename);
@@ -2851,11 +1427,13 @@
2851 1427 appendMessage('bot', data.data.message);
2852 1428 scrollToBottom();
2853 1429 activeWordFile = data.data.filename;
2854 1430 } else {
1431 + console.error('Upload failed:', data.data);
2855 1432 alert('Failed to upload Word document. Please try again.');
2856 1433 }
2857 1434 } catch (error) {
1435 + console.error('Upload error:', error);
2858 1436 alert('Error uploading file. Please try again.');
2859 1437 } finally {
2860 1438 uploadBtn.disabled = false;
2861 1439 sendBtn.disabled = false;
@@ -2912,640 +1490,164 @@
2912 1490 });
2913 1491 });
2914 1492
2915 1493
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 -
2922 -function showInitLoader(botId) {
2923 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2924 - if (loader) loader.style.display = 'flex';
2925 -}
2926 -
2927 -function hideInitLoader(botId) {
2928 - var loader = getElementDOM(botId, 'mxchat-init-loader');
2929 - if (loader) loader.style.display = 'none';
2930 -}
2931 -
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';
2938 -}
2939 -
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 - }
2964 - }
2965 -}
2966 -
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 -
2973 - // Track submitting state per bot
2974 - const emailSubmittingState = {};
2975 -
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);
3003 - }
3004 -
3005 - function isValidEmailAddress(email) {
3006 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3007 - return emailRegex.test(email.trim()) && email.length <= 254;
3008 - }
3009 -
3010 - function isValidNameInput(name) {
3011 - return name && name.trim().length >= 2 && name.trim().length <= 100;
3012 - }
3013 -
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;
3022 -
3023 - // Find the first bot message (intro message)
3024 - var introMessage = chatBox.querySelector('.bot-message');
3025 - if (!introMessage) return;
3026 -
3027 - var messageContent = introMessage.querySelector('div[dir="auto"]');
3028 - if (!messageContent) return;
3029 -
3030 - var html = messageContent.innerHTML;
3031 -
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);
3037 - } 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();
3042 - }
3043 -
3044 - messageContent.innerHTML = html;
3045 - }
3046 -
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');
3051 -
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;
3057 -
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 - }
3086 - }
3087 -
3088 - function showEmailError(botId, message) {
3089 - clearEmailError(botId);
3090 -
3091 - var emailForm = getElementDOM(botId, 'email-collection-form');
3092 - if (!emailForm) return;
3093 -
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);
3105 -
3106 - // Add shake animation to inputs
3107 - var emailInput = getElementDOM(botId, 'user-email');
3108 - var nameInput = getElementDOM(botId, 'user-name');
3109 -
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 - }
3119 -
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 - }
3127 -
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 - }
3136 - } else {
3137 - checkSessionAndEmailForBot(botId);
3138 - }
3139 - }
3140 -
3141 - function checkSessionAndEmailForBot(botId) {
3142 - const sessionId = MxChatInstances.ensureSession(botId);
3143 -
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);
3150 -
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,
1494 + // ====================================
1495 + // EMAIL COLLECTION SETUP
1496 + // ====================================
1497 +
1498 + // Email collection form setup and handlers
1499 + const emailForm = document.getElementById('email-collection-form');
1500 + const emailBlocker = document.getElementById('email-blocker');
1501 + const chatbotWrapper = document.getElementById('chat-container');
1502 +
1503 + if (emailForm && emailBlocker && chatbotWrapper) {
1504 + // Check if email exists for the current session
1505 + function checkSessionAndEmail() {
1506 + const sessionId = getChatSession();
1507 + //console.log("[DEBUG JS] checkSessionAndEmail -> sessionId:", sessionId);
1508 +
1509 + fetch(mxchatChat.ajax_url, {
1510 + method: 'POST',
1511 + headers: {
1512 + 'Content-Type': 'application/x-www-form-urlencoded',
1513 + },
1514 + body: new URLSearchParams({
1515 + action: 'mxchat_check_email_provided',
1516 + session_id: sessionId,
1517 + nonce: mxchatChat.nonce,
1518 + }),
3160 1519 })
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);
1520 + .then((response) => response.json())
1521 + .then((data) => {
1522 + //console.log("[DEBUG JS] mxchat_check_email_provided response:", data);
1523 +
1524 + if (data.success) {
1525 + if (data.data.logged_in) {
1526 + //console.log("[DEBUG JS] User is logged in. Hiding email form.");
1527 + emailBlocker.style.display = 'none';
1528 + chatbotWrapper.style.display = 'flex';
1529 + } else if (data.data.email) {
1530 + //console.log("[DEBUG JS] Email found for session. Hiding email form.");
1531 + emailBlocker.style.display = 'none';
1532 + chatbotWrapper.style.display = 'flex';
1533 + } else {
1534 + //console.log("[DEBUG JS] No email provided. Showing email form.");
1535 + emailBlocker.style.display = 'flex';
1536 + chatbotWrapper.style.display = 'none';
1537 + }
3172 1538 } else {
3173 - showEmailFormForBot(botId);
1539 + //console.log("[DEBUG JS] Error or no data received. Showing email form.");
1540 + emailBlocker.style.display = 'flex';
1541 + chatbotWrapper.style.display = 'none';
3174 1542 }
3175 - } else {
3176 - showEmailFormForBot(botId);
3177 - }
3178 - })
3179 - .catch((error) => {
3180 - showEmailFormForBot(botId);
3181 - });
3182 - }
3183 -
3184 - // Event delegation for email form submission
3185 - $(document).on('submit', '.email-collection-form', function(e) {
3186 - e.preventDefault();
3187 - e.stopPropagation();
3188 -
3189 - var botId = getBotIdFromElement(this);
3190 -
3191 - // Prevent double submission
3192 - if (emailSubmittingState[botId]) {
3193 - return false;
1543 + })
1544 + .catch((error) => {
1545 + // console.error("[DEBUG JS] Fetch error -> forcing email form visible:", error);
1546 + emailBlocker.style.display = 'flex';
1547 + chatbotWrapper.style.display = 'none';
1548 + });
3194 1549 }
3195 -
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);
3201 -
3202 - // Validate email
3203 - if (!userEmail) {
3204 - showEmailError(botId, 'Please enter your email address.');
3205 - return false;
3206 - }
3207 -
3208 - if (!isValidEmailAddress(userEmail)) {
3209 - showEmailError(botId, 'Please enter a valid email address.');
3210 - return false;
3211 - }
3212 -
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;
3217 - }
3218 -
3219 - clearEmailError(botId);
3220 - setEmailSubmissionState(botId, true);
3221 -
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 - });
3229 -
3230 - if (userName) {
3231 - formData.append('name', userName);
3232 - }
3233 -
3234 - fetch(mxchatChat.ajax_url, {
3235 - method: 'POST',
3236 - headers: {
3237 - 'Content-Type': 'application/x-www-form-urlencoded',
3238 - },
3239 - body: formData
3240 - })
3241 - .then((response) => {
3242 - if (!response.ok) {
3243 - throw new Error(`HTTP error! status: ${response.status}`);
3244 - }
3245 - return response.json();
3246 - })
3247 - .then((data) => {
3248 - setEmailSubmissionState(botId, false);
3249 -
3250 - 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);
3256 - } else {
3257 - // Remove placeholder if no name provided
3258 - replaceVisitorNamePlaceholder(botId, '');
3259 - }
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);
1550 +
1551 + // Handle email form submission
1552 + emailForm.addEventListener('submit', function (event) {
1553 + event.preventDefault();
1554 + const userEmail = document.getElementById('user-email').value;
1555 + const sessionId = getChatSession();
1556 +
1557 + if (userEmail) {
1558 + fetch(mxchatChat.ajax_url, {
1559 + method: 'POST',
1560 + headers: {
1561 + 'Content-Type': 'application/x-www-form-urlencoded',
1562 + },
1563 + body: new URLSearchParams({
1564 + action: 'mxchat_handle_save_email_and_response',
1565 + email: userEmail,
1566 + session_id: sessionId,
1567 + nonce: mxchatChat.nonce,
1568 + }),
1569 + })
1570 + .then((response) => response.json())
1571 + .then((data) => {
1572 + //console.log('Backend response:', data);
1573 + if (data.success) {
1574 + //console.log('Email saved successfully:', userEmail);
1575 + emailBlocker.style.display = 'none';
1576 + chatbotWrapper.style.display = 'flex';
1577 +
1578 + // Optionally handle bot response
1579 + if (data.message) {
1580 + appendMessage('bot', data.message);
1581 + scrollToBottom();
3266 1582 }
3267 - }, 100);
3268 - }
3269 - } else {
3270 - showEmailError(botId, data.message || 'Failed to save email. Please try again.');
1583 + } else {
1584 + console.error('Error saving email:', data.message || 'Unknown error');
1585 + }
1586 + })
1587 + .catch((error) => {
1588 + console.error('AJAX error:', error);
1589 + });
3271 1590 }
3272 - })
3273 - .catch((error) => {
3274 - setEmailSubmissionState(botId, false);
3275 - showEmailError(botId, 'An error occurred. Please try again.');
3276 1591 });
1592 +
1593 + // Check session and email status on page load
1594 + checkSessionAndEmail();
1595 + } else if (mxchatChat.email_collection_enabled) {
1596 + // Only show error if email collection is enabled but elements are missing
1597 + console.error('Essential elements for email handling are missing.');
1598 + }
3277 1599
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);
1600 + // Open chatbot when pre-chat message is clicked
1601 + $(document).on('click', '#pre-chat-message', function() {
1602 + var chatbot = $('#floating-chatbot');
1603 + if (chatbot.hasClass('hidden')) {
1604 + chatbot.removeClass('hidden').addClass('visible');
1605 + $('#floating-chatbot-button').addClass('hidden');
1606 + $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
3356 1607 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 1608 }
3374 1609 });
3375 1610
3376 - // Legacy duplicate close handler removed — handled by single event delegation above
1611 + var closeButton = document.querySelector('.close-pre-chat-message');
1612 + if (closeButton) {
1613 + closeButton.addEventListener('click', function() {
1614 + $('#pre-chat-message').fadeOut(200); // Hide the message
3377 1615
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 - }
3410 -}
3411 -
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 -}
3424 -
3425 -function checkQuickQuestionsState(botId) {
3426 - botId = botId || 'default';
3427 - if (!hasQuickQuestions(botId)) {
3428 - return; // Don't do anything if no questions exist
3429 - }
3430 -
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 -
1616 + // Send an AJAX request to set the transient flag for 24 hours
3477 1617 $.ajax({
3478 1618 url: mxchatChat.ajax_url,
3479 1619 type: 'POST',
3480 1620 data: {
3481 - action: 'mxchat_track_url_click',
3482 - session_id: getChatSession(botId),
3483 - url: originalHref,
3484 - message_context: messageText,
3485 - nonce: mxchatChat.nonce
1621 + action: 'mxchat_dismiss_pre_chat_message',
1622 + _ajax_nonce: mxchatChat.nonce
3486 1623 },
3487 - complete: function() {
3488 - if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
3489 - window.open(originalHref, '_blank');
3490 - } else {
3491 - window.location.href = originalHref;
3492 - }
1624 + success: function() {
1625 + //console.log('Pre-chat message dismissed for 24 hours.');
1626 +
1627 + // Ensure the message is hidden after dismissal
1628 + $('#pre-chat-message').hide();
1629 + },
1630 + error: function() {
1631 + //console.error('Failed to dismiss pre-chat message.');
3493 1632 }
3494 1633 });
3495 -
3496 - return false;
3497 - }
1634 + });
3498 1635 }
3499 -});
3500 -
3501 1636 // ====================================
3502 1637 // MAIN INITIALIZATION
3503 1638 // ====================================
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 - });
3513 -
1639 +
1640 + if ($('#floating-chatbot').hasClass('hidden')) {
1641 + $('#floating-chatbot-button').removeClass('hidden');
1642 + }
3514 1643 // Initialize when document is ready
3515 1644 setFullHeight();
1645 + initializeChatVisibility();
1646 + loadChatHistory();
3516 1647
3517 - // Note: trackOriginatingPage() and loadChatHistory() are now deferred
3518 - // until the user's first interaction via MxChatInstances.ensureSession()
3519 -
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 - });
3525 -
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;
3544 -
3545 1648 }); // End of jQuery ready
3546 1649
3547 -
3548 1650 // ====================================
3549 1651 // GLOBAL EVENT LISTENERS (Outside jQuery)
3550 1652 // ====================================
3551 1653
@@ -3571,4 +1673,8 @@
3571 1673 }
3572 1674 }
3573 1675 });
3574 1676
1677 +// Initialize on page load
1678 +document.addEventListener('DOMContentLoaded', function() {
1679 + checkInitialDocumentStatus();
1680 +});