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

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