PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.3
MxChat – AI Chatbot & Content Generation for WordPress v3.1.3
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
mxchat-basic / js / chat-script.js

chat-script.js in MxChat – AI Chatbot & Content Generation for WordPress 3.1.3, at js/chat-script.js

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