PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.3
MxChat – AI Chatbot & Content Generation for WordPress v3.2.3
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / js / chat-script.js

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

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