PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.0
MxChat – AI Chatbot & Content Generation for WordPress v3.1.0
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.0, at js/chat-script.js

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