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

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

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