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

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