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

3,216 lines 122.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 jQuery(document).ready(function($) {
2
3 // ====================================
4 // MULTI-INSTANCE MANAGEMENT SYSTEM
5 // ====================================
6
7 // Instance registry - tracks all chatbot instances on the page
8 const MxChatInstances = {
9 instances: {},
10
11 // Initialize an instance for a bot
12 init: function(botId) {
13 if (!this.instances[botId]) {
14 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 // Re-enable chat input when stream ends with content
848 enableChatInput(botId);
849
850 if (callback) {
851 callback(accumulatedContent);
852 }
853 return;
854 }
855
856 buffer += decoder.decode(value, { stream: true });
857 const lines = buffer.split('\n');
858 buffer = lines.pop() || '';
859
860 for (const line of lines) {
861 if (line.startsWith('data: ')) {
862 const data = line.substring(6);
863
864 if (data === '[DONE]') {
865 if (!accumulatedContent) {
866 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
867 callMxChat(message, callback, botId);
868 return;
869 }
870
871 // Re-enable chat input after streaming completes
872 enableChatInput(botId);
873
874 if (callback) {
875 callback(accumulatedContent);
876 }
877 return;
878 }
879
880 try {
881 const json = JSON.parse(data);
882
883 // IMMEDIATE CHAT MODE UPDATE FOR STREAMING
884 if (json.chat_mode) {
885 updateChatModeIndicator(json.chat_mode, botId);
886 }
887
888 // Handle testing data
889 if (json.testing_data && !testingDataReceived) {
890 if (window.mxchatTestPanelInstance) {
891 window.mxchatTestPanelInstance.handleTestingData(json.testing_data);
892 testingDataReceived = true;
893 }
894 }
895 // Handle content streaming
896 else if (json.content) {
897 streamingStarted = true;
898 accumulatedContent += json.content;
899 updateStreamingMessage(accumulatedContent, botId);
900 }
901 // Handle complete response in stream (fallback response)
902 else if (json.text || json.message || json.html) {
903 handleNonStreamResponse(json, callback, botId);
904 return;
905 }
906 // Handle errors
907 else if (json.error) {
908
909 // Get error message from various possible fields
910 let errorMessage = json.error_message || json.message || json.text ||
911 (typeof json.error === 'string' ? json.error : 'An error occurred. Please try again.');
912
913 // Re-enable chat input on error
914 enableChatInput(botId);
915
916 // Display the error directly in the chat
917 replaceLastMessage("bot", errorMessage, '', [], botId);
918
919 if (callback) {
920 callback(errorMessage);
921 }
922 return;
923 }
924 } catch (e) {
925 // SSE data parsing error - silently continue
926 }
927 }
928 }
929
930 processStream();
931 }).catch(streamError => {
932 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
933 callMxChat(message, callback, botId);
934 });
935 }
936
937 processStream();
938 })
939 .catch(error => {
940 // Check if we have server error data with chat mode
941 if (error && error.isServerError && error.data) {
942 // Check for chat mode in error data
943 if (error.data.chat_mode) {
944 updateChatModeIndicator(error.data.chat_mode, botId);
945 }
946
947 handleNonStreamResponse(error.data, callback, botId);
948 } else {
949 // Only fall back to regular call if we don't have any response data
950 getElement(botId, 'chat-box').find('.bot-message.temporary-message').remove();
951 callMxChat(message, callback, botId);
952 }
953 });
954 }
955
956 // Helper function to handle non-streaming responses
957 function handleNonStreamResponse(data, callback, botId) {
958 botId = botId || 'default';
959
960 // IMMEDIATE CHAT MODE UPDATE FOR NON-STREAMING RESPONSES
961 if (data.chat_mode) {
962 updateChatModeIndicator(data.chat_mode, botId);
963 }
964
965 // Also check in data property if response is wrapped
966 if (data.data && data.data.chat_mode) {
967 updateChatModeIndicator(data.data.chat_mode, botId);
968 }
969
970 // NOTE: Don't remove temporary message here - let replaceLastMessage handle it
971 // This prevents a visual gap between thinking dots disappearing and content appearing
972
973 // SECURITY FIX: Check for errors FIRST
974 if (data.success === false || (data.data && data.data.error_message)) {
975 let errorMessage = "";
976 let errorCode = "";
977
978 // Check various possible error locations
979 if (data.data && data.data.error_message) {
980 errorMessage = data.data.error_message;
981 errorCode = data.data.error_code || "";
982 } else if (data.error_message) {
983 errorMessage = data.error_message;
984 errorCode = data.error_code || "";
985 } else if (data.message) {
986 errorMessage = data.message;
987 } else if (typeof data.data === 'string') {
988 errorMessage = data.data;
989 } else {
990 errorMessage = "An error occurred. Please try again or contact support.";
991 }
992
993 // Handle session reset action (IP changed, session expired, etc.)
994 if (data.data && data.data.action === 'reset_session') {
995 // Clear the old session and generate a new one
996 resetChatSession(botId);
997 // Re-send the original message with the new session
998 var originalMessage = getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message');
999 if (originalMessage) {
1000 getElement(botId, 'mxchat-chatbot-wrapper').find('.mxchat-input-holder textarea').data('pending-message', null);
1001 // Re-add the user message and thinking indicator
1002 appendMessage("user", originalMessage, '', [], false, botId);
1003 appendThinkingMessage(botId);
1004 scrollToBottom(botId);
1005 // Determine whether to use streaming
1006 const currentModel = mxchatChat.model || 'gpt-4o';
1007 if (shouldUseStreaming(currentModel)) {
1008 callMxChatStream(originalMessage, callback, botId);
1009 } else {
1010 callMxChat(originalMessage, callback, botId);
1011 }
1012 }
1013 return;
1014 }
1015
1016 // Format user-friendly error message
1017 let displayMessage = errorMessage;
1018 if (mxchatChat.is_admin) {
1019 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
1020 }
1021
1022 replaceLastMessage("bot", displayMessage, '', [], botId);
1023
1024 if (callback) {
1025 callback('');
1026 }
1027 return; // Exit early for errors
1028 }
1029
1030 // Handle different response formats
1031 if (data.text || data.html || data.message) {
1032
1033 // Apply response hooks
1034 if (data.text && typeof customMxChatFilter === 'function') {
1035 data.text = customMxChatFilter(data.text, "response");
1036 }
1037 if (data.message && typeof customMxChatFilter === 'function') {
1038 data.message = customMxChatFilter(data.message, "response");
1039 }
1040
1041 // Display the response
1042 if (data.text && data.html) {
1043 replaceLastMessage("bot", data.text, data.html, [], botId);
1044 } else if (data.text) {
1045 replaceLastMessage("bot", data.text, '', [], botId);
1046 } else if (data.html) {
1047 replaceLastMessage("bot", "", data.html, [], botId);
1048 } else if (data.message) {
1049 replaceLastMessage("bot", data.message, '', [], botId);
1050 }
1051 }
1052
1053 // Handle other response properties
1054 if (data.data && data.data.filename) {
1055 showActivePdf(data.data.filename, botId);
1056 var instance = MxChatInstances.get(botId);
1057 instance.activePdfFile = data.data.filename;
1058 }
1059
1060 if (data.redirect_url) {
1061 setTimeout(() => {
1062 window.location.href = data.redirect_url;
1063 }, 1500);
1064 }
1065
1066 // Ensure chat input is re-enabled (safety net for edge cases)
1067 enableChatInput(botId);
1068
1069 if (callback) {
1070 callback(data.text || data.message || '');
1071 }
1072 }
1073
1074 // Enhanced updateChatModeIndicator function for immediate DOM updates
1075 function updateChatModeIndicator(mode, botId) {
1076 console.log('[MxChat] updateChatModeIndicator called with mode:', mode, 'botId:', botId);
1077 botId = botId || 'default';
1078 const indicator = getElementDOM(botId, 'chat-mode-indicator');
1079 console.log('[MxChat] chat-mode-indicator element found:', !!indicator);
1080 if (indicator) {
1081 const oldText = indicator.textContent;
1082 console.log('[MxChat] Current indicator text:', oldText, '-> changing to mode:', mode);
1083
1084 if (mode === 'agent') {
1085 indicator.textContent = 'Live Agent';
1086 console.log('[MxChat] Mode is agent, calling startPolling...');
1087 startPolling(botId);
1088 } else {
1089 // Everything else is AI mode
1090 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1091 indicator.textContent = customAiText;
1092 stopPolling(botId);
1093 }
1094
1095 // Force immediate DOM update and reflow
1096 if (oldText !== indicator.textContent) {
1097 // Force a reflow to ensure the change is visible immediately
1098 indicator.style.display = 'none';
1099 indicator.offsetHeight; // Trigger reflow
1100 indicator.style.display = '';
1101
1102 // Double-check after a brief moment to ensure the change stuck
1103 setTimeout(() => {
1104 if (mode === 'agent' && indicator.textContent !== 'Live Agent') {
1105 indicator.textContent = 'Live Agent';
1106 } else if (mode !== 'agent' && indicator.textContent === 'Live Agent') {
1107 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1108 indicator.textContent = customAiText;
1109 }
1110 }, 50);
1111 }
1112 }
1113 }
1114
1115 // Function to update message during streaming
1116 function updateStreamingMessage(content, botId) {
1117 botId = botId || 'default';
1118
1119 // ADD RESPONSE HOOK FOR REAL-TIME STREAMING
1120 if (typeof customMxChatFilter === 'function') {
1121 content = customMxChatFilter(content, "response");
1122 }
1123
1124 const formattedContent = linkify(content);
1125
1126 // Find the temporary message in this bot's chat box
1127 var $chatBox = getElement(botId, 'chat-box');
1128 const tempMessage = $chatBox.find('.bot-message.temporary-message').last();
1129
1130 if (tempMessage.length) {
1131 // Update existing message
1132 tempMessage.html(formattedContent);
1133 } else {
1134 // Create new temporary message if it doesn't exist
1135 appendMessage("bot", content, '', [], true, botId);
1136 }
1137 }
1138
1139 function isStreamingSupported(model) {
1140 if (!model) return false;
1141
1142 const modelPrefix = model.split('-')[0].toLowerCase();
1143
1144 // Support streaming for OpenAI, Claude, Grok, DeepSeek, and OpenRouter models
1145 const isSupported = modelPrefix === 'gpt' ||
1146 modelPrefix === 'o1' ||
1147 modelPrefix === 'claude' ||
1148 modelPrefix === 'grok' ||
1149 modelPrefix === 'deepseek' ||
1150 model === 'openrouter'; // Add this line - check full model name for OpenRouter
1151
1152 return isSupported;
1153 }
1154
1155 // Update the event handlers to use the correct function names (using event delegation)
1156 // Use class-based selectors for multi-instance support
1157 $(document).on('click', '.send-button', function() {
1158 var botId = getBotIdFromElement(this);
1159 disableChatInput(botId);
1160 sendMessage(botId);
1161 });
1162
1163 // Override enter key handler (using event delegation)
1164 $(document).on('keypress', '.chat-input', function(e) {
1165 if (e.which == 13 && !e.shiftKey) {
1166 e.preventDefault();
1167 var botId = getBotIdFromElement(this);
1168 disableChatInput(botId);
1169 sendMessage(botId);
1170 }
1171 });
1172
1173
1174 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
1175 try {
1176 // Determine styles based on sender type
1177 let messageClass, bgColor, fontColor;
1178
1179 if (sender === "user") {
1180 messageClass = "user-message";
1181 bgColor = userMessageBgColor;
1182 fontColor = userMessageFontColor;
1183 // Only sanitize user input
1184 messageText = sanitizeUserInput(messageText);
1185 } else if (sender === "agent") {
1186 messageClass = "agent-message";
1187 bgColor = liveAgentMessageBgColor;
1188 fontColor = liveAgentMessageFontColor;
1189 } else {
1190 messageClass = "bot-message";
1191 bgColor = botMessageBgColor;
1192 fontColor = botMessageFontColor;
1193 }
1194
1195 const messageDiv = $('<div>')
1196 .addClass(messageClass)
1197 .attr('dir', 'auto');
1198
1199 // Only apply inline colors if AI theme is not active (let CSS handle it)
1200 var skipColors = shouldSkipInlineColors(botId);
1201 if (skipColors) {
1202 messageDiv.css({
1203 'margin-bottom': '1em'
1204 });
1205 } else {
1206 messageDiv.css({
1207 'background': bgColor,
1208 'color': fontColor,
1209 'margin-bottom': '1em'
1210 });
1211 }
1212
1213 // Process the message content based on sender
1214 let fullMessage;
1215 if (sender === "user") {
1216 // For user messages, apply linkify after sanitization
1217 fullMessage = linkify(messageText);
1218 } else {
1219 // For bot/agent messages, preserve HTML
1220 fullMessage = messageText;
1221 }
1222
1223 // Add images if provided
1224 if (images && images.length > 0) {
1225 fullMessage += '<div class="image-gallery" dir="auto">';
1226 images.forEach(img => {
1227 const safeTitle = sanitizeUserInput(img.title);
1228 const safeUrl = encodeURI(img.image_url);
1229 const safeThumbnail = encodeURI(img.thumbnail_url);
1230
1231 fullMessage += `
1232 <div style="margin-bottom: 10px;">
1233 <strong>${safeTitle}</strong><br>
1234 <a href="${safeUrl}" target="_blank">
1235 <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
1236 </a>
1237 </div>`;
1238 });
1239 fullMessage += '</div>';
1240 }
1241
1242 // Append HTML content if provided
1243 if (messageHtml && sender !== "user") {
1244 // Only add line breaks if there's actual text content before the HTML
1245 if (fullMessage && fullMessage.trim()) {
1246 fullMessage += '<br><br>' + messageHtml;
1247 } else {
1248 fullMessage = messageHtml;
1249 }
1250 }
1251
1252 messageDiv.html(fullMessage);
1253
1254 if (isTemporary) {
1255 messageDiv.addClass('temporary-message');
1256 }
1257
1258 // Append to the correct chatbot instance's chat-box
1259 var $chatBox = getElement(botId, 'chat-box');
1260 messageDiv.hide().appendTo($chatBox).fadeIn(300, function() {
1261 // FIXED: Use event delegation for link tracking
1262 if (sender === "bot" || sender === "agent") {
1263 attachLinkTracking(messageDiv, messageText, botId);
1264 }
1265
1266 if (sender === "bot") {
1267 const lastUserMessage = $chatBox.find('.user-message').last();
1268 if (lastUserMessage.length) {
1269 scrollElementToTop(lastUserMessage, botId);
1270 }
1271 }
1272 });
1273
1274 if (messageText.id) {
1275 var instance = MxChatInstances.get(botId);
1276 instance.lastSeenMessageId = messageText.id;
1277 hideNotification(botId);
1278 }
1279 } catch (error) {
1280 // Error rendering message - silently continue
1281 }
1282 }
1283
1284 // Helper function to attach link tracking with proper event handling
1285 function attachLinkTracking(messageDiv, messageText, botId) {
1286 botId = botId || 'default';
1287 // Use a slight delay to ensure DOM is ready
1288 setTimeout(function() {
1289 const links = messageDiv.find('a[href]').not('[data-tracked]');
1290
1291 links.each(function() {
1292 const $link = $(this);
1293 const originalHref = $link.attr('href');
1294
1295 // Mark as tracked to avoid duplicate handlers
1296 $link.attr('data-tracked', 'true');
1297
1298 // Only track external URLs
1299 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
1300 // Remove any existing click handlers first
1301 $link.off('click.tracking');
1302
1303 // Add new click handler with namespace
1304 $link.on('click.tracking', function(e) {
1305 e.preventDefault();
1306 e.stopPropagation();
1307
1308 const messageContext = typeof messageText === 'string'
1309 ? messageText.substring(0, 200)
1310 : '';
1311
1312 // Track the click
1313 $.ajax({
1314 url: mxchatChat.ajax_url,
1315 type: 'POST',
1316 data: {
1317 action: 'mxchat_track_url_click',
1318 session_id: getChatSession(botId),
1319 url: originalHref,
1320 message_context: messageContext,
1321 nonce: mxchatChat.nonce
1322 },
1323 complete: function() {
1324 // Always redirect, even if tracking fails
1325 if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
1326 window.open(originalHref, '_blank');
1327 } else {
1328 window.location.href = originalHref;
1329 }
1330 }
1331 });
1332
1333 return false; // Extra insurance to prevent default
1334 });
1335 }
1336 });
1337 }, 100); // Small delay to ensure DOM is ready
1338 }
1339
1340 function replaceLastMessage(sender, responseText, responseHtml = '', images = [], botId = 'default') {
1341 var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
1342 var $chatBox = getElement(botId, 'chat-box');
1343 var lastMessageDiv = $chatBox.find('.bot-message.temporary-message, .agent-message.temporary-message').last();
1344
1345 // Determine styles
1346 let bgColor, fontColor;
1347 if (sender === "user") {
1348 bgColor = userMessageBgColor;
1349 fontColor = userMessageFontColor;
1350 } else if (sender === "agent") {
1351 bgColor = liveAgentMessageBgColor;
1352 fontColor = liveAgentMessageFontColor;
1353 } else {
1354 bgColor = botMessageBgColor;
1355 fontColor = botMessageFontColor;
1356 }
1357
1358 // FIXED: Only linkify if response doesn't already contain HTML links or tags
1359 // This prevents double-processing of URLs that are already formatted as HTML
1360 var fullMessage;
1361 if (sender === "user") {
1362 // Always linkify user messages (they're plain text)
1363 fullMessage = linkify(responseText);
1364 } else {
1365 // For bot/agent messages, check if HTML already exists
1366 if (responseText.includes('<a href=') || responseText.includes('</a>') ||
1367 responseText.includes('<img') || responseText.includes('<div') ||
1368 responseText.includes('<p>') || responseText.includes('<br>')) {
1369 // Response already has HTML, don't process it
1370 fullMessage = responseText;
1371 } else {
1372 // Plain text response, apply linkify
1373 fullMessage = linkify(responseText);
1374 }
1375 }
1376
1377 if (responseHtml) {
1378 // Only add line breaks if there's actual text content before the HTML
1379 if (fullMessage && fullMessage.trim()) {
1380 fullMessage += '<br><br>' + responseHtml;
1381 } else {
1382 fullMessage = responseHtml;
1383 }
1384 }
1385
1386 if (images.length > 0) {
1387 fullMessage += '<div class="image-gallery" dir="auto">';
1388 images.forEach(img => {
1389 fullMessage += `
1390 <div style="margin-bottom: 10px;">
1391 <strong>${img.title}</strong><br>
1392 <a href="${img.image_url}" target="_blank">
1393 <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
1394 </a>
1395 </div>`;
1396 });
1397 fullMessage += '</div>';
1398 }
1399
1400 if (lastMessageDiv.length) {
1401 // Replace content immediately to prevent visual gap between thinking dots and response
1402 lastMessageDiv
1403 .html(fullMessage)
1404 .removeClass('bot-message user-message temporary-message')
1405 .addClass(messageClass)
1406 .attr('dir', 'auto');
1407
1408 // Only apply inline colors if AI theme is not active (let CSS handle it)
1409 var skipColors = mxchatChat.skip_inline_colors || shouldSkipInlineColors(botId);
1410 if (!skipColors) {
1411 lastMessageDiv.css({
1412 'background-color': bgColor,
1413 'color': fontColor,
1414 });
1415 }
1416
1417 // Handle link tracking and scroll
1418 if (sender === "bot" || sender === "agent") {
1419 attachLinkTracking(lastMessageDiv, responseText, botId);
1420
1421 const lastUserMessage = $chatBox.find('.user-message').last();
1422 if (lastUserMessage.length) {
1423 scrollElementToTop(lastUserMessage, botId);
1424 }
1425 // Show notification if chat is hidden
1426 var $floatingChatbot = getElement(botId, 'floating-chatbot');
1427 if ($floatingChatbot.hasClass('hidden')) {
1428 showNotification(botId);
1429 }
1430 }
1431
1432 // Re-enable chat input after response is displayed
1433 enableChatInput(botId);
1434 } else {
1435 appendMessage(sender, responseText, responseHtml, images, false, botId);
1436 // Re-enable chat input after response is displayed
1437 enableChatInput(botId);
1438 }
1439 }
1440
1441
1442 function appendThinkingMessage(botId) {
1443 botId = botId || 'default';
1444 var $chatBox = getElement(botId, 'chat-box');
1445
1446 // Remove any existing thinking dots in this bot's chat first
1447 $chatBox.find('.thinking-dots').remove();
1448
1449 // Check if we should skip inline colors (AI theme is active)
1450 var skipColors = shouldSkipInlineColors(botId);
1451
1452 // Retrieve the bot message font color and background color
1453 var botMessageFontColor = mxchatChat.bot_message_font_color;
1454 var botMessageBgColor = mxchatChat.bot_message_bg_color;
1455
1456 // Build thinking dots HTML - skip inline colors if AI theme is active
1457 var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
1458 var thinkingHtml = '<div class="thinking-dots-container">' +
1459 '<div class="thinking-dots">' +
1460 '<span class="dot"' + dotStyle + '></span>' +
1461 '<span class="dot"' + dotStyle + '></span>' +
1462 '<span class="dot"' + dotStyle + '></span>' +
1463 '</div>' +
1464 '</div>';
1465
1466 // Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
1467 var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + ';"';
1468 $chatBox.append('<div class="bot-message temporary-message"' + messageStyle + '>' + thinkingHtml + '</div>');
1469 scrollToBottom(botId);
1470 }
1471
1472 function removeThinkingDots(botId) {
1473 botId = botId || 'default';
1474 var $chatBox = getElement(botId, 'chat-box');
1475 $chatBox.find('.thinking-dots').closest('.temporary-message').remove();
1476 }
1477
1478 // ====================================
1479 // TEXT FORMATTING & PROCESSING
1480 // ====================================
1481
1482 function linkify(inputText) {
1483 if (!inputText) {
1484 return '';
1485 }
1486
1487 // Helper function to check if URL is already encoded
1488 function isUrlEncoded(url) {
1489 // Check for % followed by exactly 2 hex digits
1490 return /%[0-9a-fA-F]{2}/.test(url);
1491 }
1492
1493 // Helper function to safely encode URLs only if needed
1494 function safeEncodeUrl(url) {
1495 // If URL already contains encoded characters, return as-is
1496 if (isUrlEncoded(url)) {
1497 return url;
1498 }
1499 // Otherwise, encode it
1500 return encodeURI(url);
1501 }
1502
1503 // Process markdown headers FIRST
1504 let processedText = formatMarkdownHeaders(inputText);
1505
1506 // Process text styling (bold, italic, strikethrough)
1507 processedText = formatTextStyling(processedText);
1508
1509 // Process code blocks BEFORE processing links
1510 processedText = formatCodeBlocks(processedText);
1511
1512 // NOW convert to paragraphs
1513 processedText = convertNewlinesToBreaks(processedText);
1514
1515 // IMPORTANT: Handle citation-style brackets FIRST [URL]
1516 // This prevents them from being processed as markdown links
1517 // Match [URL] where URL is a complete URL in square brackets (common in AI citations)
1518 processedText = processedText.replace(/\[(https?:\/\/[^\]]+)\]/g, (match, url) => {
1519 // Clean the URL of any trailing punctuation
1520 let cleanUrl = url.replace(/[.,;!?]+$/, '');
1521 const safeUrl = safeEncodeUrl(cleanUrl);
1522 // Return as a proper link without the brackets
1523 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1524 });
1525
1526 // Process proper markdown links with text: [text](url)
1527 // This MUST have non-empty text in the first brackets
1528 const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1529 processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1530 // Make sure we have actual text (not just whitespace)
1531 if (!text || !text.trim()) {
1532 // If no text, treat the URL as the text
1533 let cleanUrl = url.replace(/[.,;!?]+$/, '');
1534 const safeUrl = safeEncodeUrl(cleanUrl);
1535 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1536 }
1537
1538 // Clean the URL
1539 let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1540 const safeUrl = safeEncodeUrl(cleanUrl);
1541 const safeText = sanitizeUserInput(text);
1542 return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1543 });
1544
1545 // Handle empty markdown links: [](url)
1546 // This is a specific case where there's no text
1547 const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1548 processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1549 let cleanUrl = url.replace(/[.,;!?]+$/, '');
1550 const safeUrl = safeEncodeUrl(cleanUrl);
1551 // Use the URL itself as the link text
1552 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1553 });
1554
1555 // Process phone numbers: [text](tel:number)
1556 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1557 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1558 const safePhone = safeEncodeUrl(phone);
1559 const safeText = sanitizeUserInput(text);
1560 return `<a href="${safePhone}">${safeText}</a>`;
1561 });
1562
1563 // Process mailto links: [text](mailto:email)
1564 const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
1565 processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
1566 const safeMailto = safeEncodeUrl(mailto);
1567 const safeText = sanitizeUserInput(text);
1568 return `<a href="${safeMailto}">${safeText}</a>`;
1569 });
1570
1571 // Process standalone URLs - but NOT if they're already in <a> tags or brackets
1572 // Updated pattern to be more careful about what it matches
1573 const urlPattern = /(^|[^">=\[\]])(https?:\/\/[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim;
1574 processedText = processedText.replace(urlPattern, (match, prefix, url) => {
1575 // Extra check: make sure this isn't already linked
1576 if (match.includes('href=') || match.includes('</a>')) {
1577 return match;
1578 }
1579
1580 // Clean trailing punctuation
1581 let cleanUrl = url.replace(/[.,;!?)]+$/, '');
1582 const safeUrl = safeEncodeUrl(cleanUrl);
1583 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1584 });
1585
1586 // Process www. URLs - but NOT if they're already in <a> tags or brackets
1587 const wwwPattern = /(^|[^">/\[\]])(www\.[\S]+)(?![^<]*<\/a>)(?!\])/gim;
1588 processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
1589 // Extra check: make sure this isn't already linked
1590 if (match.includes('href=') || match.includes('</a>')) {
1591 return match;
1592 }
1593
1594 // Clean trailing punctuation
1595 let cleanUrl = url.replace(/[.,;!?)]+$/, '');
1596 const safeUrl = safeEncodeUrl(`http://${cleanUrl}`);
1597 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1598 });
1599
1600 return processedText;
1601 }
1602
1603 function formatMarkdownHeaders(text) {
1604 // Handle h1 to h6 headers
1605 return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
1606 const level = hashes.length;
1607 return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`;
1608 });
1609 }
1610
1611 function formatTextStyling(text) {
1612 // IMPORTANT: Protect BOTH HTML href and Markdown URLs from formatting
1613 const protectedSegments = [];
1614 let protectedText = text;
1615
1616 // Step 1a: Protect HTML href="..." attributes
1617 protectedText = protectedText.replace(/href\s*=\s*["']([^"']+)["']/gi, function(match) {
1618 const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1619 protectedSegments.push(match);
1620 return placeholder;
1621 });
1622
1623 // Step 1b: Protect Markdown links [text](url)
1624 // This is crucial - we need to protect the URLs in markdown format
1625 protectedText = protectedText.replace(/\[([^\]]*)\]\(([^)]+)\)/g, function(match) {
1626 const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1627 protectedSegments.push(match);
1628 return placeholder;
1629 });
1630
1631 // Step 1c: Also protect bare URLs that might exist
1632 protectedText = protectedText.replace(/(https?:\/\/[^\s<>"]+)/gi, function(match) {
1633 const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1634 protectedSegments.push(match);
1635 return placeholder;
1636 });
1637
1638 // Step 2: Now apply text styling to the protected text
1639 // Handle bold text (**text**)
1640 protectedText = protectedText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1641
1642 // Handle italic text (*text* or _text_) - Safari-compatible (no lookbehind)
1643 // Match single asterisks that aren't part of bold (**) by checking they're not followed/preceded by another *
1644 protectedText = protectedText.replace(/(?!\*\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
1645
1646 // Handle underscores for italic - Safari-compatible (no lookbehind)
1647 // Exclude __PROTECTED_N__ placeholders by checking the content doesn't contain PROTECTED
1648 protectedText = protectedText.replace(/(?!__)_((?!PROTECTED)[^_\n]+)_(?!_)/g, '<em>$1</em>');
1649
1650 // Handle strikethrough (~~text~~)
1651 protectedText = protectedText.replace(/~~(.*?)~~/g, '<del>$1</del>');
1652
1653 // Step 3: Restore all protected segments
1654 protectedSegments.forEach((original, index) => {
1655 const placeholder = `__PROTECTED_${index}__`;
1656 protectedText = protectedText.replace(placeholder, original);
1657 });
1658
1659 return protectedText;
1660 }
1661 function formatBoldText(text) {
1662 // This function is kept for compatibility but now uses formatTextStyling
1663 return formatTextStyling(text);
1664 }
1665
1666 function convertNewlinesToBreaks(text) {
1667 // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
1668 const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
1669
1670 // Filter out empty paragraphs and wrap each paragraph in <p> tags
1671 return paragraphs
1672 .map(para => para.trim())
1673 .filter(para => para.length > 0) // Remove empty paragraphs
1674 .map(para => `<p>${para}</p>`)
1675 .join('');
1676 }
1677 function formatCodeBlocks(text) {
1678 // Handle fenced code blocks with language specification (```language)
1679 text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, language, code) => {
1680 const lang = language || 'text';
1681 const escapedCode = escapeHtml(code.trim());
1682 return `<div class="mxchat-code-block-container">
1683 <div class="mxchat-code-header">
1684 <span class="mxchat-code-language">${lang}</span>
1685 <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1686 </div>
1687 <pre class="mxchat-code-block"><code class="language-${lang}">${escapedCode}</code></pre>
1688 </div>`;
1689 });
1690
1691 // Handle inline code with single backticks
1692 text = text.replace(/`([^`\n]+)`/g, '<code class="mxchat-inline-code">$1</code>');
1693
1694 // Handle raw PHP tags (legacy support)
1695 text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
1696 const escapedCode = escapeHtml(match);
1697 return `<div class="mxchat-code-block-container">
1698 <div class="mxchat-code-header">
1699 <span class="mxchat-code-language">php</span>
1700 <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1701 </div>
1702 <pre class="mxchat-code-block"><code class="language-php">${escapedCode}</code></pre>
1703 </div>`;
1704 });
1705
1706 return text;
1707 }
1708
1709 function sanitizeUserInput(text) {
1710 const div = document.createElement('div');
1711 div.textContent = text;
1712 return div.innerHTML;
1713 }
1714
1715 function escapeHtml(unsafe) {
1716 // Skip escaping if it's already escaped or contains HTML code block markup
1717 if (unsafe.includes('&lt;') || unsafe.includes('&gt;') ||
1718 unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
1719 return unsafe;
1720 }
1721
1722 return unsafe
1723 .replace(/&/g, "&amp;")
1724 .replace(/</g, "&lt;")
1725 .replace(/>/g, "&gt;")
1726 .replace(/"/g, "&quot;")
1727 .replace(/'/g, "&#039;");
1728 }
1729
1730 function decodeHTMLEntities(text) {
1731 var textArea = document.createElement('textarea');
1732 textArea.innerHTML = text;
1733 return textArea.value;
1734 }
1735
1736 // ====================================
1737 // UI & SCROLLING CONTROLS
1738 // ====================================
1739
1740 function scrollToBottom(botIdOrInstant, instant) {
1741 // Handle backward compatibility: scrollToBottom() or scrollToBottom(true/false)
1742 var botId = 'default';
1743 if (typeof botIdOrInstant === 'string') {
1744 botId = botIdOrInstant;
1745 instant = instant || false;
1746 } else if (typeof botIdOrInstant === 'boolean') {
1747 instant = botIdOrInstant;
1748 } else {
1749 instant = false;
1750 }
1751
1752 var chatBox = getElement(botId, 'chat-box');
1753 if (instant) {
1754 // Instantly set the scroll position to the bottom
1755 chatBox.scrollTop(chatBox.prop("scrollHeight"));
1756 } else {
1757 // Use requestAnimationFrame for smoother scrolling if needed
1758 let start = null;
1759 const scrollHeight = chatBox.prop("scrollHeight");
1760 const initialScroll = chatBox.scrollTop();
1761 const distance = scrollHeight - initialScroll;
1762 const duration = 500; // Duration in ms
1763
1764 function smoothScroll(timestamp) {
1765 if (!start) start = timestamp;
1766 const progress = timestamp - start;
1767 const currentScroll = initialScroll + (distance * (progress / duration));
1768 chatBox.scrollTop(currentScroll);
1769
1770 if (progress < duration) {
1771 requestAnimationFrame(smoothScroll);
1772 } else {
1773 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
1774 }
1775 }
1776
1777 requestAnimationFrame(smoothScroll);
1778 }
1779 }
1780
1781 function scrollElementToTop(element, botId) {
1782 botId = botId || 'default';
1783 var chatBox = getElement(botId, 'chat-box');
1784 var elementTop = element.position().top + chatBox.scrollTop();
1785 chatBox.animate({ scrollTop: elementTop }, 500);
1786 }
1787
1788 function showChatWidget(botId) {
1789 botId = botId || 'default';
1790 var $button = getElement(botId, 'floating-chatbot-button');
1791 // First ensure display is set
1792 $button.css('display', 'flex');
1793 // Then handle the fade
1794 $button.fadeTo(500, 1);
1795 // Force visibility
1796 $button.removeClass('hidden');
1797 }
1798
1799 function hideChatWidget(botId) {
1800 botId = botId || 'default';
1801 var $button = getElement(botId, 'floating-chatbot-button');
1802 $button.css('display', 'none');
1803 $button.addClass('hidden');
1804 }
1805
1806 function disableScroll() {
1807 if (isMobile()) {
1808 $('body').css('overflow', 'hidden');
1809 }
1810 }
1811
1812 function enableScroll() {
1813 if (isMobile()) {
1814 $('body').css('overflow', '');
1815 }
1816 }
1817
1818 function isMobile() {
1819 // This can be a simple check, or more sophisticated detection of mobile devices
1820 return window.innerWidth <= 768; // Example threshold for mobile devices
1821 }
1822
1823 function setFullHeight() {
1824 var vh = $(window).innerHeight() * 0.01;
1825 $(':root').css('--vh', vh + 'px');
1826 }
1827
1828
1829 // ====================================
1830 // NOTIFICATION SYSTEM
1831 // ====================================
1832
1833 function createNotificationBadge() {
1834 const chatButton = document.getElementById('floating-chatbot-button');
1835
1836 if (!chatButton) return;
1837
1838 // Remove any existing badge first
1839 const existingBadge = chatButton.querySelector('.chat-notification-badge');
1840 if (existingBadge) {
1841 existingBadge.remove();
1842 }
1843
1844 notificationBadge = document.createElement('div');
1845 notificationBadge.className = 'chat-notification-badge';
1846 notificationBadge.style.cssText = `
1847 display: none;
1848 position: absolute;
1849 top: -5px;
1850 right: -5px;
1851 background-color: red;
1852 color: white;
1853 border-radius: 50%;
1854 padding: 4px 8px;
1855 font-size: 12px;
1856 font-weight: bold;
1857 z-index: 10001;
1858 `;
1859 chatButton.style.position = 'relative';
1860 chatButton.appendChild(notificationBadge);
1861
1862 }
1863
1864 function showNotification(botId) {
1865 botId = botId || 'default';
1866 const badge = getElementDOM(botId, 'chat-notification-badge');
1867 var $floatingChatbot = getElement(botId, 'floating-chatbot');
1868 if (badge && $floatingChatbot.hasClass('hidden')) {
1869 badge.style.display = 'block';
1870 badge.textContent = '1';
1871 }
1872 }
1873
1874 function hideNotification(botId) {
1875 botId = botId || 'default';
1876 const badge = getElementDOM(botId, 'chat-notification-badge');
1877 if (badge) {
1878 badge.style.display = 'none';
1879 }
1880 }
1881
1882 function startNotificationChecking(botId) {
1883 botId = botId || 'default';
1884 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1885 if (!chatPersistenceEnabled) return;
1886
1887 createNotificationBadge(botId);
1888 var instance = MxChatInstances.get(botId);
1889 instance.notificationCheckInterval = setInterval(function() {
1890 checkForNewMessages(botId);
1891 }, 30000); // Check every 30 seconds
1892 }
1893
1894 function stopNotificationChecking(botId) {
1895 botId = botId || 'default';
1896 var instance = MxChatInstances.get(botId);
1897 if (instance.notificationCheckInterval) {
1898 clearInterval(instance.notificationCheckInterval);
1899 }
1900 }
1901
1902 function checkForNewMessages() {
1903 const sessionId = getChatSession();
1904 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1905
1906 if (!chatPersistenceEnabled) return;
1907
1908 $.ajax({
1909 url: mxchatChat.ajax_url,
1910 type: 'POST',
1911 data: {
1912 action: 'mxchat_check_new_messages',
1913 session_id: sessionId,
1914 last_seen_id: lastSeenMessageId,
1915 nonce: mxchatChat.nonce
1916 },
1917 success: function(response) {
1918 if (response.success && response.data.hasNewMessages) {
1919 showNotification();
1920 }
1921 }
1922 });
1923 }
1924
1925
1926 // ====================================
1927 // LIVE AGENT FUNCTIONALITY
1928 // ====================================
1929
1930 function startPolling(botId) {
1931 console.log('[MxChat] startPolling called for botId:', botId);
1932 botId = botId || 'default';
1933 var instance = MxChatInstances.get(botId);
1934 // Clear any existing interval first
1935 stopPolling(botId);
1936 // Start new polling interval
1937 console.log('[MxChat] Starting polling interval (5s) for botId:', botId);
1938 instance.pollingInterval = setInterval(function() {
1939 checkForAgentMessages(botId);
1940 }, 5000);
1941 }
1942
1943 function stopPolling(botId) {
1944 console.log('[MxChat] stopPolling called for botId:', botId);
1945 botId = botId || 'default';
1946 var instance = MxChatInstances.get(botId);
1947 if (instance.pollingInterval) {
1948 clearInterval(instance.pollingInterval);
1949 instance.pollingInterval = null;
1950 console.log('[MxChat] Polling stopped for botId:', botId);
1951 }
1952 }
1953
1954 function checkForAgentMessages(botId) {
1955 console.log('[MxChat] checkForAgentMessages called for botId:', botId);
1956 botId = botId || 'default';
1957 var instance = MxChatInstances.get(botId);
1958 const sessionId = getChatSession(botId);
1959 $.ajax({
1960 url: mxchatChat.ajax_url,
1961 type: 'POST',
1962 dataType: 'json',
1963 data: {
1964 action: 'mxchat_fetch_new_messages',
1965 session_id: sessionId,
1966 last_seen_id: instance.lastSeenMessageId,
1967 persistence_enabled: 'true',
1968 nonce: mxchatChat.nonce
1969 },
1970 success: function (response) {
1971 if (response.success && response.data?.new_messages) {
1972 let hasNewMessage = false;
1973
1974 response.data.new_messages.forEach(function (message) {
1975 if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) {
1976 hasNewMessage = true;
1977 appendMessage("agent", message.content, '', [], false, botId);
1978 instance.lastSeenMessageId = message.id;
1979 instance.processedMessageIds.add(message.id);
1980 }
1981 });
1982
1983 var $floatingChatbot = getElement(botId, 'floating-chatbot');
1984 if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
1985 showNotification(botId);
1986 }
1987
1988 scrollToBottom(botId, true);
1989 }
1990 },
1991 error: function (xhr, status, error) {
1992 // Polling error - silently continue
1993 }
1994 });
1995 }
1996
1997 // ====================================
1998 // CHAT HISTORY & PERSISTENCE
1999 // ====================================
2000
2001 function loadChatHistory(botId) {
2002 botId = botId || 'default';
2003 var instance = MxChatInstances.get(botId);
2004
2005 // Prevent duplicate loading
2006 if (instance.chatHistoryLoaded) {
2007 return;
2008 }
2009
2010 var sessionId = getChatSession(botId);
2011 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
2012
2013 if (chatPersistenceEnabled && sessionId) {
2014 $.ajax({
2015 url: mxchatChat.ajax_url,
2016 type: 'POST',
2017 dataType: 'json',
2018 data: {
2019 action: 'mxchat_fetch_conversation_history',
2020 session_id: sessionId
2021 },
2022 success: function(response) {
2023 // Handle session reset (IP changed while user was away)
2024 if (response.success === false && response.data && response.data.action === 'reset_session') {
2025 // Silently reset session - user will start fresh
2026 resetChatSession(botId);
2027 instance.chatHistoryLoaded = true; // Prevent retry loop
2028 return;
2029 }
2030
2031 // Check if the response indicates success
2032 if (response.success) {
2033 // Handle case where conversation data exists and is an array
2034 if (response.data && Array.isArray(response.data.conversation)) {
2035 var $chatBox = getElement(botId, 'chat-box');
2036 var $fragment = $(document.createDocumentFragment());
2037 let highestMessageId = instance.lastSeenMessageId;
2038
2039 // Update chat mode if provided
2040 if (response.data.chat_mode) {
2041 updateChatModeIndicator(response.data.chat_mode, botId);
2042 }
2043
2044 // Only process if there are actual messages
2045 if (response.data.conversation.length > 0) {
2046 // IMPORTANT: Clear existing messages before loading history
2047 $chatBox.empty();
2048
2049 $.each(response.data.conversation, function(index, message) {
2050 // Skip agent messages if persistence is off
2051 if (!chatPersistenceEnabled && message.role === 'agent') {
2052 return;
2053 }
2054
2055 var messageClass, messageBgColor, messageFontColor;
2056
2057 switch (message.role) {
2058 case 'user':
2059 messageClass = 'user-message';
2060 messageBgColor = userMessageBgColor;
2061 messageFontColor = userMessageFontColor;
2062 break;
2063 case 'agent':
2064 messageClass = 'agent-message';
2065 messageBgColor = liveAgentMessageBgColor;
2066 messageFontColor = liveAgentMessageFontColor;
2067 break;
2068 default:
2069 messageClass = 'bot-message';
2070 messageBgColor = botMessageBgColor;
2071 messageFontColor = botMessageFontColor;
2072 break;
2073 }
2074
2075 var messageElement = $('<div>').addClass(messageClass)
2076 .css({
2077 'background': messageBgColor,
2078 'color': messageFontColor
2079 });
2080
2081 var content = message.content;
2082 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
2083 content = decodeHTMLEntities(content);
2084
2085 if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
2086 messageElement.html(content);
2087 } else {
2088 var formattedContent = linkify(content);
2089 messageElement.html(formattedContent);
2090 }
2091
2092 $fragment.append(messageElement);
2093
2094 // Track message IDs
2095 if (message.id) {
2096 highestMessageId = Math.max(highestMessageId, message.id);
2097 instance.processedMessageIds.add(message.id);
2098 }
2099 });
2100
2101 // Only append messages and scroll if we have content
2102 $chatBox.append($fragment);
2103 scrollToBottom(botId, true);
2104
2105 // Collapse quick questions if we have conversation history
2106 // BUT skip auto-collapse for embedded bots (they should stay expanded)
2107 if (hasQuickQuestions(botId) && !isEmbeddedBot(botId)) {
2108 collapseQuickQuestions(botId);
2109 }
2110
2111 // Update lastSeenMessageId after history loads
2112 instance.lastSeenMessageId = highestMessageId;
2113
2114 // Only update chat mode if persistence is enabled and we have messages
2115 if (chatPersistenceEnabled) {
2116 var lastMessage = response.data.conversation[response.data.conversation.length - 1];
2117 if (lastMessage.role === 'agent') {
2118 updateChatModeIndicator('agent', botId);
2119 }
2120 }
2121
2122 // Mark as loaded ONLY after successful load
2123 instance.chatHistoryLoaded = true;
2124 }
2125 }
2126 }
2127 },
2128 error: function(xhr, status, error) {
2129 // Error loading chat history - silently continue
2130 }
2131 });
2132 }
2133 }
2134
2135
2136 // ====================================
2137 // FILE UPLOAD FUNCTIONALITY
2138 // ====================================
2139
2140 function addSafeEventListener(elementId, eventType, handler) {
2141 const element = document.getElementById(elementId);
2142 if (element) {
2143 element.addEventListener(eventType, handler);
2144 }
2145 }
2146
2147 function showActivePdf(filename, botId) {
2148 botId = botId || 'default';
2149 const container = getElementDOM(botId, 'active-pdf-container');
2150 const nameElement = getElementDOM(botId, 'active-pdf-name');
2151
2152 if (!container || !nameElement) {
2153 return;
2154 }
2155
2156 nameElement.textContent = filename;
2157 container.style.display = 'flex';
2158 }
2159
2160 function showActiveWord(filename, botId) {
2161 botId = botId || 'default';
2162 const container = getElementDOM(botId, 'active-word-container');
2163 const nameElement = getElementDOM(botId, 'active-word-name');
2164
2165 if (!container || !nameElement) {
2166 return;
2167 }
2168
2169 nameElement.textContent = filename;
2170 container.style.display = 'flex';
2171 }
2172
2173 function removeActivePdf(botId) {
2174 botId = botId || 'default';
2175 var instance = MxChatInstances.get(botId);
2176 const container = getElementDOM(botId, 'active-pdf-container');
2177 const nameElement = getElementDOM(botId, 'active-pdf-name');
2178
2179 if (!container || !nameElement || !instance.activePdfFile) return;
2180
2181 fetch(mxchatChat.ajax_url, {
2182 method: 'POST',
2183 headers: {
2184 'Content-Type': 'application/x-www-form-urlencoded',
2185 },
2186 body: new URLSearchParams({
2187 'action': 'mxchat_remove_pdf',
2188 'session_id': getChatSession(botId),
2189 'nonce': mxchatChat.nonce
2190 })
2191 })
2192 .then(response => response.json())
2193 .then(data => {
2194 if (data.success) {
2195 container.style.display = 'none';
2196 nameElement.textContent = '';
2197 activePdfFile = null;
2198 appendMessage('bot', 'PDF removed.');
2199 }
2200 })
2201 .catch(error => {
2202 // Error removing PDF - silently continue
2203 });
2204 }
2205
2206 function removeActiveWord() {
2207 const container = document.getElementById('active-word-container');
2208 const nameElement = document.getElementById('active-word-name');
2209
2210 if (!container || !nameElement || !activeWordFile) return;
2211
2212 fetch(mxchatChat.ajax_url, {
2213 method: 'POST',
2214 headers: {
2215 'Content-Type': 'application/x-www-form-urlencoded',
2216 },
2217 body: new URLSearchParams({
2218 'action': 'mxchat_remove_word',
2219 'session_id': sessionId,
2220 'nonce': mxchatChat.nonce
2221 })
2222 })
2223 .then(response => response.json())
2224 .then(data => {
2225 if (data.success) {
2226 container.style.display = 'none';
2227 nameElement.textContent = '';
2228 activeWordFile = null;
2229 appendMessage('bot', 'Word document removed.');
2230 }
2231 })
2232 .catch(error => {
2233 // Error removing Word document - silently continue
2234 });
2235 }
2236
2237 // ====================================
2238 // CONSENT & COMPLIANCE (GDPR)
2239 // ====================================
2240
2241 function initializeChatVisibility(botId) {
2242 botId = botId || 'default';
2243 const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
2244 mxchatChat.complianz_toggle === '1' ||
2245 mxchatChat.complianz_toggle === 1;
2246
2247 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
2248 // Initial check
2249 checkConsentAndShowChat(botId);
2250
2251 // Listen for consent changes
2252 $(document).on('cmplz_status_change', function(event) {
2253 checkConsentAndShowChat(botId);
2254 });
2255 } else {
2256 // If Complianz is not enabled, always show
2257 getElement(botId, 'floating-chatbot-button')
2258 .css('display', 'flex')
2259 .removeClass('hidden no-consent')
2260 .fadeTo(500, 1);
2261
2262 // Also check pre-chat message when Complianz is not enabled
2263 checkPreChatDismissal(botId);
2264 }
2265 }
2266
2267
2268 function checkConsentAndShowChat(botId) {
2269 botId = botId || 'default';
2270 var consentStatus = cmplz_has_consent('marketing');
2271 var consentType = complianz.consenttype;
2272
2273 let $widget = getElement(botId, 'floating-chatbot-button');
2274 let $chatbot = getElement(botId, 'floating-chatbot');
2275 let $preChat = getElement(botId, 'pre-chat-message');
2276
2277 if (consentStatus === true) {
2278 $widget
2279 .removeClass('no-consent')
2280 .css('display', 'flex')
2281 .removeClass('hidden')
2282 .fadeTo(500, 1);
2283 $chatbot.removeClass('no-consent');
2284
2285 // Show pre-chat message if not dismissed
2286 checkPreChatDismissal(botId);
2287 } else {
2288 $widget
2289 .addClass('no-consent')
2290 .fadeTo(500, 0, function() {
2291 $(this)
2292 .css('display', 'none')
2293 .addClass('hidden');
2294 });
2295 $chatbot.addClass('no-consent');
2296
2297 // Hide pre-chat message when no consent
2298 $preChat.hide();
2299 }
2300 }
2301
2302
2303 // ====================================
2304 // PRE-CHAT MESSAGE HANDLING
2305 // ====================================
2306
2307 function checkPreChatDismissal(botId) {
2308 botId = botId || 'default';
2309 $.ajax({
2310 url: mxchatChat.ajax_url,
2311 type: 'POST',
2312 data: {
2313 action: 'mxchat_check_pre_chat_message_status',
2314 _ajax_nonce: mxchatChat.nonce
2315 },
2316 success: function(response) {
2317 if (response.success && !response.data.dismissed) {
2318 getElement(botId, 'pre-chat-message').fadeIn(250);
2319 } else {
2320 getElement(botId, 'pre-chat-message').hide();
2321 }
2322 },
2323 error: function() {
2324 // Error checking pre-chat dismissal - silently continue
2325 }
2326 });
2327 }
2328
2329 function handlePreChatDismissal(botId) {
2330 botId = botId || 'default';
2331 getElement(botId, 'pre-chat-message').fadeOut(200);
2332 $.ajax({
2333 url: mxchatChat.ajax_url,
2334 type: 'POST',
2335 data: {
2336 action: 'mxchat_dismiss_pre_chat_message',
2337 _ajax_nonce: mxchatChat.nonce
2338 },
2339 success: function() {
2340 $('#pre-chat-message').hide();
2341 },
2342 error: function() {
2343 // Error dismissing pre-chat message - silently continue
2344 }
2345 });
2346 }
2347
2348
2349 // ====================================
2350 // UTILITY FUNCTIONS
2351 // ====================================
2352
2353 function copyToClipboard(text) {
2354 var tempInput = $('<input>');
2355 $('body').append(tempInput);
2356 tempInput.val(text).select();
2357 document.execCommand('copy');
2358 tempInput.remove();
2359 }
2360
2361
2362 function isImageHtml(str) {
2363 return str.startsWith('<img') && str.endsWith('>');
2364 }
2365
2366
2367 // ====================================
2368 // EVENT HANDLERS & INITIALIZATION
2369 // ====================================
2370
2371 $(document).on('click', '.mxchat-popular-question', function () {
2372 var question = $(this).text();
2373 var botId = getBotIdFromElement(this);
2374
2375 // Append the question as if the user typed it
2376 appendMessage("user", question, '', [], false, botId);
2377
2378 // Only collapse if there are questions
2379 if (hasQuickQuestions(botId)) {
2380 collapseQuickQuestions(botId);
2381 }
2382
2383 // Send the question to the server
2384 sendMessageToChatbot(question, botId);
2385 });
2386
2387 $(document).on('click', '.questions-toggle-btn', function(e) {
2388 e.preventDefault();
2389 e.stopPropagation();
2390 var botId = getBotIdFromElement(this);
2391 expandQuickQuestions(botId);
2392 });
2393
2394 $(document).on('click', '.questions-collapse-btn', function(e) {
2395 e.preventDefault();
2396 e.stopPropagation();
2397 var botId = getBotIdFromElement(this);
2398 collapseQuickQuestions(botId);
2399 });
2400
2401 // Chatbot visibility toggle handlers - use class selector for multi-instance support
2402 $(document).on('click', '.floating-chatbot-button', function() {
2403 var botId = getBotIdFromElement(this);
2404 var $chatbot = getElement(botId, 'floating-chatbot');
2405 var $badge = getElement(botId, 'chat-notification-badge');
2406 var $preChat = getElement(botId, 'pre-chat-message');
2407
2408 if ($chatbot.hasClass('hidden')) {
2409 $chatbot.removeClass('hidden').addClass('visible');
2410 $(this).addClass('hidden');
2411 $badge.hide(); // Hide notification when opening chat
2412 disableScroll();
2413 $preChat.fadeOut(250);
2414 } else {
2415 $chatbot.removeClass('visible').addClass('hidden');
2416 $(this).removeClass('hidden');
2417 enableScroll();
2418 checkPreChatDismissal(botId);
2419 }
2420 });
2421
2422 // Allow clicking anywhere on the title bar to close the chatbot
2423 $(document).on('click', '.chatbot-top-bar', function() {
2424 var botId = getBotIdFromElement(this);
2425 getElement(botId, 'floating-chatbot').addClass('hidden').removeClass('visible');
2426 getElement(botId, 'floating-chatbot-button').removeClass('hidden');
2427 enableScroll();
2428 });
2429
2430 $(document).on('click', '.close-pre-chat-message', function(e) {
2431 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2432 var botId = getBotIdFromElement(this);
2433 getElement(botId, 'pre-chat-message').fadeOut(200, function() {
2434 $(this).remove();
2435 });
2436 });
2437
2438
2439 // PDF upload button handlers - use class selector
2440 $(document).on('click', '.pdf-upload-btn', function() {
2441 var botId = getBotIdFromElement(this);
2442 var pdfInput = getElementDOM(botId, 'pdf-upload');
2443 if (pdfInput) pdfInput.click();
2444 });
2445
2446 // Word upload button handlers - use class selector
2447 $(document).on('click', '.word-upload-btn', function() {
2448 var botId = getBotIdFromElement(this);
2449 var wordInput = getElementDOM(botId, 'word-upload');
2450 if (wordInput) wordInput.click();
2451 });
2452
2453 // PDF file input change handler
2454 addSafeEventListener('pdf-upload', 'change', async function(e) {
2455 const file = e.target.files[0];
2456
2457 if (!file || file.type !== 'application/pdf') {
2458 alert('Please select a valid PDF file.');
2459 return;
2460 }
2461
2462 if (!sessionId) {
2463 alert('Error: No session ID found');
2464 return;
2465 }
2466
2467 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
2468 alert('Error: Ajax configuration missing');
2469 return;
2470 }
2471
2472 // Disable buttons and show loading state
2473 const uploadBtn = document.getElementById('pdf-upload-btn');
2474 const sendBtn = document.getElementById('send-button');
2475 const originalBtnContent = uploadBtn.innerHTML;
2476
2477 try {
2478 const formData = new FormData();
2479 formData.append('action', 'mxchat_upload_pdf');
2480 formData.append('pdf_file', file);
2481 formData.append('session_id', sessionId);
2482 formData.append('nonce', mxchatChat.nonce);
2483
2484 uploadBtn.disabled = true;
2485 sendBtn.disabled = true;
2486 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2487 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2488 </svg>`;
2489
2490 const response = await fetch(mxchatChat.ajax_url, {
2491 method: 'POST',
2492 body: formData
2493 });
2494
2495 const data = await response.json();
2496
2497 if (data.success) {
2498 // Hide popular questions if they exist
2499 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2500 if (hasQuickQuestions()) {
2501 collapseQuickQuestions();
2502 }
2503
2504 // Show the active PDF name
2505 showActivePdf(data.data.filename);
2506
2507 appendMessage('bot', data.data.message);
2508 scrollToBottom();
2509 activePdfFile = data.data.filename;
2510 } else {
2511 alert('Failed to upload PDF. Please try again.');
2512 }
2513 } catch (error) {
2514 alert('Error uploading file. Please try again.');
2515 } finally {
2516 uploadBtn.disabled = false;
2517 sendBtn.disabled = false;
2518 uploadBtn.innerHTML = originalBtnContent;
2519 this.value = ''; // Reset file input
2520 }
2521 });
2522
2523 // Word file input change handler
2524 addSafeEventListener('word-upload', 'change', async function(e) {
2525 const file = e.target.files[0];
2526
2527 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
2528 alert('Please select a valid Word document (.docx).');
2529 return;
2530 }
2531
2532 if (!sessionId) {
2533 alert('Error: No session ID found');
2534 return;
2535 }
2536
2537 // Disable buttons and show loading state
2538 const uploadBtn = document.getElementById('word-upload-btn');
2539 const sendBtn = document.getElementById('send-button');
2540 const originalBtnContent = uploadBtn.innerHTML;
2541
2542 try {
2543 const formData = new FormData();
2544 formData.append('action', 'mxchat_upload_word');
2545 formData.append('word_file', file);
2546 formData.append('session_id', sessionId);
2547 formData.append('nonce', mxchatChat.nonce);
2548
2549 uploadBtn.disabled = true;
2550 sendBtn.disabled = true;
2551 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2552 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2553 </svg>`;
2554
2555 const response = await fetch(mxchatChat.ajax_url, {
2556 method: 'POST',
2557 body: formData
2558 });
2559
2560 const data = await response.json();
2561
2562 if (data.success) {
2563 // Hide popular questions if they exist
2564 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2565 if (hasQuickQuestions()) {
2566 collapseQuickQuestions();
2567 }
2568
2569 // Show the active Word document name
2570 showActiveWord(data.data.filename);
2571
2572 appendMessage('bot', data.data.message);
2573 scrollToBottom();
2574 activeWordFile = data.data.filename;
2575 } else {
2576 alert('Failed to upload Word document. Please try again.');
2577 }
2578 } catch (error) {
2579 alert('Error uploading file. Please try again.');
2580 } finally {
2581 uploadBtn.disabled = false;
2582 sendBtn.disabled = false;
2583 uploadBtn.innerHTML = originalBtnContent;
2584 this.value = ''; // Reset file input
2585 }
2586 });
2587
2588 // Remove button click handlers
2589 document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
2590 e.preventDefault();
2591 e.stopPropagation();
2592 removeActivePdf();
2593 });
2594
2595 document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
2596 e.preventDefault();
2597 e.stopPropagation();
2598 removeActiveWord();
2599 });
2600
2601 // Window resize handlers
2602 $(window).on('resize orientationchange', function() {
2603 setFullHeight();
2604 });
2605
2606
2607 // ====================================
2608 // TOOLBAR & STYLING SETUP
2609 // ====================================
2610
2611 // Apply toolbar settings
2612 if (mxchatChat.chat_toolbar_toggle === 'on') {
2613 $('.chat-toolbar').show();
2614 } else {
2615 $('.chat-toolbar').hide();
2616 }
2617
2618 // Apply toolbar icon colors
2619 const toolbarElements = [
2620 '#mxchat-chatbot .toolbar-btn svg',
2621 '#mxchat-chatbot .active-pdf-name',
2622 '#mxchat-chatbot .active-word-name',
2623 '#mxchat-chatbot .remove-pdf-btn svg',
2624 '#mxchat-chatbot .remove-word-btn svg',
2625 '#mxchat-chatbot .toolbar-perplexity svg'
2626 ];
2627
2628 toolbarElements.forEach(selector => {
2629 $(selector).css({
2630 'fill': toolbarIconColor,
2631 'stroke': toolbarIconColor,
2632 'color': toolbarIconColor
2633 });
2634 });
2635
2636
2637 // ====================================
2638 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
2639 // ====================================
2640 // Only run email collection setup if it's enabled
2641 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2642
2643 // Track submitting state per bot
2644 const emailSubmittingState = {};
2645
2646 // Add CSS animations for email form (once globally)
2647 if (!document.getElementById('email-error-styles')) {
2648 const style = document.createElement('style');
2649 style.id = 'email-error-styles';
2650 style.textContent = `
2651 @keyframes fadeInError {
2652 from { opacity: 0; transform: translateY(-5px); }
2653 to { opacity: 1; transform: translateY(0); }
2654 }
2655 .email-input-shake {
2656 animation: shake 0.5s ease-in-out;
2657 }
2658 @keyframes shake {
2659 0%, 100% { transform: translateX(0); }
2660 25% { transform: translateX(-5px); }
2661 75% { transform: translateX(5px); }
2662 }
2663 @keyframes spin {
2664 from { transform: rotate(0deg); }
2665 to { transform: rotate(360deg); }
2666 }
2667 .email-spinner {
2668 display: inline-block;
2669 vertical-align: middle;
2670 }
2671 `;
2672 document.head.appendChild(style);
2673 }
2674
2675 // Helper functions for email collection (multi-instance aware)
2676 function showEmailFormForBot(botId) {
2677 var emailBlocker = getElementDOM(botId, 'email-blocker');
2678 var chatContainer = getElementDOM(botId, 'chat-container');
2679 if (emailBlocker) emailBlocker.style.display = 'flex';
2680 if (chatContainer) chatContainer.style.display = 'none';
2681 }
2682
2683 function showChatContainerForBot(botId) {
2684 var emailBlocker = getElementDOM(botId, 'email-blocker');
2685 var chatContainer = getElementDOM(botId, 'chat-container');
2686 if (emailBlocker) emailBlocker.style.display = 'none';
2687 if (chatContainer) chatContainer.style.display = 'flex';
2688
2689 // Load chat history for this bot
2690 if (typeof loadChatHistory === 'function') {
2691 loadChatHistory(botId);
2692 }
2693 }
2694
2695 function isValidEmailAddress(email) {
2696 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2697 return emailRegex.test(email.trim()) && email.length <= 254;
2698 }
2699
2700 function isValidNameInput(name) {
2701 return name && name.trim().length >= 2 && name.trim().length <= 100;
2702 }
2703
2704 function setEmailSubmissionState(botId, loading) {
2705 var submitButton = getElementDOM(botId, 'email-submit-button');
2706 var emailInput = getElementDOM(botId, 'user-email');
2707 var nameInput = getElementDOM(botId, 'user-name');
2708
2709 if (loading) {
2710 emailSubmittingState[botId] = true;
2711 if (submitButton) submitButton.disabled = true;
2712 if (emailInput) emailInput.disabled = true;
2713 if (nameInput) nameInput.disabled = true;
2714
2715 if (submitButton && !submitButton.getAttribute('data-original-html')) {
2716 submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2717 const originalText = submitButton.textContent;
2718 submitButton.innerHTML = `
2719 <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2720 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2721 <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2722 <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2723 </circle>
2724 </svg>
2725 ${originalText}
2726 `;
2727 submitButton.style.opacity = '0.8';
2728 }
2729 } else {
2730 emailSubmittingState[botId] = false;
2731 if (submitButton) submitButton.disabled = false;
2732 if (emailInput) emailInput.disabled = false;
2733 if (nameInput) nameInput.disabled = false;
2734
2735 if (submitButton) {
2736 const originalHtml = submitButton.getAttribute('data-original-html');
2737 if (originalHtml) {
2738 submitButton.innerHTML = originalHtml;
2739 }
2740 submitButton.style.opacity = '1';
2741 }
2742 }
2743 }
2744
2745 function showEmailError(botId, message) {
2746 clearEmailError(botId);
2747
2748 var emailForm = getElementDOM(botId, 'email-collection-form');
2749 if (!emailForm) return;
2750
2751 const errorDiv = document.createElement('div');
2752 errorDiv.className = 'email-error';
2753 errorDiv.style.cssText = `
2754 color: #e74c3c;
2755 font-size: 12px;
2756 margin-top: 8px;
2757 padding: 4px 0;
2758 animation: fadeInError 0.3s ease;
2759 `;
2760 errorDiv.textContent = message;
2761 emailForm.appendChild(errorDiv);
2762
2763 // Add shake animation to inputs
2764 var emailInput = getElementDOM(botId, 'user-email');
2765 var nameInput = getElementDOM(botId, 'user-name');
2766
2767 if (emailInput) {
2768 emailInput.classList.add('email-input-shake');
2769 setTimeout(() => emailInput.classList.remove('email-input-shake'), 500);
2770 }
2771 if (nameInput) {
2772 nameInput.classList.add('email-input-shake');
2773 setTimeout(() => nameInput.classList.remove('email-input-shake'), 500);
2774 }
2775 }
2776
2777 function clearEmailError(botId) {
2778 var emailForm = getElementDOM(botId, 'email-collection-form');
2779 if (emailForm) {
2780 const existingErrors = emailForm.querySelectorAll('.email-error');
2781 existingErrors.forEach(error => error.remove());
2782 }
2783 }
2784
2785 function checkSessionAndEmailForBot(botId) {
2786 const sessionId = getChatSession(botId);
2787
2788 fetch(mxchatChat.ajax_url, {
2789 method: 'POST',
2790 headers: {
2791 'Content-Type': 'application/x-www-form-urlencoded',
2792 },
2793 body: new URLSearchParams({
2794 action: 'mxchat_check_email_provided',
2795 session_id: sessionId,
2796 nonce: mxchatChat.nonce,
2797 })
2798 })
2799 .then((response) => {
2800 if (!response.ok) {
2801 throw new Error(`HTTP error! status: ${response.status}`);
2802 }
2803 return response.json();
2804 })
2805 .then((data) => {
2806 if (data.success) {
2807 if (data.data.logged_in || data.data.email) {
2808 showChatContainerForBot(botId);
2809 } else {
2810 showEmailFormForBot(botId);
2811 }
2812 } else {
2813 showEmailFormForBot(botId);
2814 }
2815 })
2816 .catch((error) => {
2817 showEmailFormForBot(botId);
2818 });
2819 }
2820
2821 // Event delegation for email form submission
2822 $(document).on('submit', '.email-collection-form', function(e) {
2823 e.preventDefault();
2824 e.stopPropagation();
2825
2826 var botId = getBotIdFromElement(this);
2827
2828 // Prevent double submission
2829 if (emailSubmittingState[botId]) {
2830 return false;
2831 }
2832
2833 var emailInput = getElementDOM(botId, 'user-email');
2834 var nameInput = getElementDOM(botId, 'user-name');
2835 var userEmail = emailInput ? emailInput.value.trim() : '';
2836 var userName = nameInput ? nameInput.value.trim() : '';
2837 var sessionId = getChatSession(botId);
2838
2839 // Validate email
2840 if (!userEmail) {
2841 showEmailError(botId, 'Please enter your email address.');
2842 return false;
2843 }
2844
2845 if (!isValidEmailAddress(userEmail)) {
2846 showEmailError(botId, 'Please enter a valid email address.');
2847 return false;
2848 }
2849
2850 // Validate name if field exists and has content
2851 if (nameInput && userName && !isValidNameInput(userName)) {
2852 showEmailError(botId, 'Please enter a valid name (2-100 characters).');
2853 return false;
2854 }
2855
2856 clearEmailError(botId);
2857 setEmailSubmissionState(botId, true);
2858
2859 // Prepare form data
2860 const formData = new URLSearchParams({
2861 action: 'mxchat_handle_save_email_and_response',
2862 email: userEmail,
2863 session_id: sessionId,
2864 nonce: mxchatChat.nonce,
2865 });
2866
2867 if (userName) {
2868 formData.append('name', userName);
2869 }
2870
2871 fetch(mxchatChat.ajax_url, {
2872 method: 'POST',
2873 headers: {
2874 'Content-Type': 'application/x-www-form-urlencoded',
2875 },
2876 body: formData
2877 })
2878 .then((response) => {
2879 if (!response.ok) {
2880 throw new Error(`HTTP error! status: ${response.status}`);
2881 }
2882 return response.json();
2883 })
2884 .then((data) => {
2885 setEmailSubmissionState(botId, false);
2886
2887 if (data.success) {
2888 showChatContainerForBot(botId);
2889
2890 if (data.message && typeof appendMessage === 'function') {
2891 setTimeout(() => {
2892 appendMessage('bot', data.message, '', [], false, botId);
2893 if (typeof scrollToBottom === 'function') {
2894 scrollToBottom(botId);
2895 }
2896 }, 100);
2897 }
2898 } else {
2899 showEmailError(botId, data.message || 'Failed to save email. Please try again.');
2900 }
2901 })
2902 .catch((error) => {
2903 setEmailSubmissionState(botId, false);
2904 showEmailError(botId, 'An error occurred. Please try again.');
2905 });
2906
2907 return false;
2908 });
2909
2910 // Real-time email validation using event delegation
2911 $(document).on('input', '.mxchat-email-input', function() {
2912 var botId = getBotIdFromElement(this);
2913 var $input = $(this);
2914
2915 // Clear previous timeout
2916 clearTimeout($input.data('validationTimeout'));
2917
2918 // Debounce validation
2919 var timeout = setTimeout(() => {
2920 var email = this.value.trim();
2921 clearEmailError(botId);
2922
2923 if (email && !isValidEmailAddress(email)) {
2924 showEmailError(botId, 'Please enter a valid email address.');
2925 }
2926 }, 500);
2927
2928 $input.data('validationTimeout', timeout);
2929 });
2930
2931 // Handle Enter key in email input
2932 $(document).on('keypress', '.mxchat-email-input', function(e) {
2933 if (e.key === 'Enter') {
2934 e.preventDefault();
2935 var botId = getBotIdFromElement(this);
2936 if (!emailSubmittingState[botId]) {
2937 $(this).closest('.email-collection-form').submit();
2938 }
2939 }
2940 });
2941
2942 // Handle Enter key in name input
2943 $(document).on('keypress', '.mxchat-name-input', function(e) {
2944 if (e.key === 'Enter') {
2945 e.preventDefault();
2946 var botId = getBotIdFromElement(this);
2947 if (!emailSubmittingState[botId]) {
2948 $(this).closest('.email-collection-form').submit();
2949 }
2950 }
2951 });
2952
2953 // Initialize email check for all bot instances
2954 $('.mxchat-chatbot-wrapper').each(function() {
2955 var botId = $(this).data('bot-id') || 'default';
2956 var emailBlocker = getElementDOM(botId, 'email-blocker');
2957
2958 // Only check if email blocker exists for this bot
2959 if (emailBlocker) {
2960 if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2961 if (mxchatChat.initial_email_state.show_email_form) {
2962 showEmailFormForBot(botId);
2963 } else {
2964 showChatContainerForBot(botId);
2965 }
2966 } else {
2967 setTimeout(function() {
2968 checkSessionAndEmailForBot(botId);
2969 }, 100);
2970 }
2971 }
2972 });
2973 }
2974
2975 // Open chatbot when pre-chat message is clicked - use class selector for multi-instance
2976 $(document).on('click', '.pre-chat-message', function() {
2977 var botId = getBotIdFromElement(this);
2978 var $chatbot = getElement(botId, 'floating-chatbot');
2979 if ($chatbot.hasClass('hidden')) {
2980 $chatbot.removeClass('hidden').addClass('visible');
2981 getElement(botId, 'floating-chatbot-button').addClass('hidden');
2982 $(this).fadeOut(250); // Hide pre-chat message
2983 disableScroll(); // Disable scroll when chatbot opens
2984 }
2985 });
2986
2987 // Dismiss pre-chat message via close button - handled by event delegation above at line ~2376
2988 // This is a fallback for legacy support
2989 $(document).on('click', '.close-pre-chat-message', function() {
2990 var botId = getBotIdFromElement(this);
2991 var $preChat = getElement(botId, 'pre-chat-message');
2992 $preChat.fadeOut(200); // Hide the message
2993
2994 // Send an AJAX request to set the transient flag for 24 hours
2995 $.ajax({
2996 url: mxchatChat.ajax_url,
2997 type: 'POST',
2998 data: {
2999 action: 'mxchat_dismiss_pre_chat_message',
3000 _ajax_nonce: mxchatChat.nonce
3001 },
3002 success: function() {
3003 // Ensure the message is hidden after dismissal
3004 $preChat.hide();
3005 },
3006 error: function() {
3007 // Error dismissing pre-chat message - silently continue
3008 }
3009 });
3010 });
3011
3012
3013 function hasQuickQuestions(botId) {
3014 botId = botId || 'default';
3015 var questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3016 if (!questionsContainer) return false;
3017 const questionButtons = questionsContainer.querySelectorAll('.mxchat-popular-question');
3018 return questionButtons.length > 0;
3019 }
3020
3021 /**
3022 * Check if a bot is embedded (not floating)
3023 * Embedded bots don't have a .floating-chatbot wrapper
3024 */
3025 function isEmbeddedBot(botId) {
3026 botId = botId || 'default';
3027 var floatingWrapper = document.getElementById('floating-chatbot-' + botId);
3028 return !floatingWrapper;
3029 }
3030
3031 function collapseQuickQuestions(botId) {
3032 botId = botId || 'default';
3033 const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3034 if (questionsContainer && hasQuickQuestions(botId)) {
3035 questionsContainer.classList.add('collapsed');
3036 questionsContainer.classList.add('has-been-collapsed');
3037 try {
3038 sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'true');
3039 sessionStorage.setItem('mxchat_questions_has_been_collapsed_' + botId, 'true');
3040 } catch (e) {
3041 // Ignore if sessionStorage is not available
3042 }
3043 }
3044 }
3045
3046 function expandQuickQuestions(botId) {
3047 botId = botId || 'default';
3048 const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3049 if (questionsContainer && hasQuickQuestions(botId)) {
3050 questionsContainer.classList.remove('collapsed');
3051 try {
3052 sessionStorage.setItem('mxchat_questions_collapsed_' + botId, 'false');
3053 } catch (e) {
3054 // Ignore if sessionStorage is not available
3055 }
3056 }
3057 }
3058
3059 function checkQuickQuestionsState(botId) {
3060 botId = botId || 'default';
3061 if (!hasQuickQuestions(botId)) {
3062 return; // Don't do anything if no questions exist
3063 }
3064
3065 // Skip restoring collapsed state for embedded bots - they should always start expanded
3066 if (isEmbeddedBot(botId)) {
3067 return;
3068 }
3069
3070 try {
3071 const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed_' + botId);
3072 const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed_' + botId);
3073
3074 const questionsContainer = getElementDOM(botId, 'mxchat-popular-questions');
3075 if (questionsContainer) {
3076 if (hasBeenCollapsed === 'true') {
3077 questionsContainer.classList.add('has-been-collapsed');
3078 }
3079 if (isCollapsed === 'true') {
3080 questionsContainer.classList.add('collapsed');
3081 }
3082 }
3083 } catch (e) {
3084 // Ignore if sessionStorage is not available
3085 }
3086 }
3087
3088 // Global delegation for dynamically added links as fallback
3089 // Use class selector for multi-instance support
3090 $(document).on('click', '.chat-box a[href]:not([data-tracked])', function(e) {
3091 const $link = $(this);
3092 const messageDiv = $link.closest('.bot-message, .agent-message');
3093
3094 // Only process bot/agent message links
3095 if (messageDiv.length > 0) {
3096 const originalHref = $link.attr('href');
3097
3098 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
3099 e.preventDefault();
3100 e.stopPropagation();
3101
3102 // Mark as tracked
3103 $link.attr('data-tracked', 'true');
3104
3105 // Get bot ID from the chat box context
3106 var botId = getBotIdFromElement(this);
3107
3108 // Get message context from the message div
3109 const messageText = messageDiv.text().substring(0, 200);
3110
3111 $.ajax({
3112 url: mxchatChat.ajax_url,
3113 type: 'POST',
3114 data: {
3115 action: 'mxchat_track_url_click',
3116 session_id: getChatSession(botId),
3117 url: originalHref,
3118 message_context: messageText,
3119 nonce: mxchatChat.nonce
3120 },
3121 complete: function() {
3122 if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
3123 window.open(originalHref, '_blank');
3124 } else {
3125 window.location.href = originalHref;
3126 }
3127 }
3128 });
3129
3130 return false;
3131 }
3132 }
3133 });
3134
3135 // ====================================
3136 // MAIN INITIALIZATION
3137 // ====================================
3138
3139 // Initialize all chatbot instances on the page
3140 initializeAllInstances();
3141
3142 // Legacy initialization for single bot compatibility
3143 $('.floating-chatbot.hidden').each(function() {
3144 var botId = getBotIdFromElement(this);
3145 getElement(botId, 'floating-chatbot-button').removeClass('hidden');
3146 });
3147
3148 // Initialize when document is ready
3149 setFullHeight();
3150 trackOriginatingPage();
3151
3152 // Only load chat history if email collection is disabled
3153 if (mxchatChat.email_collection_enabled !== 'on') {
3154 // Load history for all instances
3155 $('.mxchat-chatbot-wrapper').each(function() {
3156 var botId = $(this).data('bot-id') || 'default';
3157 loadChatHistory(botId);
3158 });
3159 }
3160
3161 // Initialize chat visibility for all instances
3162 $('.mxchat-chatbot-wrapper').each(function() {
3163 var botId = $(this).data('bot-id') || 'default';
3164 initializeChatVisibility(botId);
3165 });
3166
3167 // Make functions globally available for add-ons
3168 window.hasQuickQuestions = hasQuickQuestions;
3169 window.collapseQuickQuestions = collapseQuickQuestions;
3170 window.appendMessage = appendMessage;
3171 window.appendThinkingMessage = appendThinkingMessage;
3172 window.scrollToBottom = scrollToBottom;
3173 window.scrollElementToTop = scrollElementToTop;
3174 window.replaceLastMessage = replaceLastMessage;
3175 window.callMxChat = callMxChat;
3176 window.callMxChatStream = callMxChatStream;
3177 window.shouldUseStreaming = shouldUseStreaming;
3178 window.getChatSession = getChatSession;
3179 window.getPageContext = getPageContext;
3180 window.updateStreamingMessage = updateStreamingMessage;
3181 window.MxChatInstances = MxChatInstances;
3182 window.getElement = getElement;
3183 window.getElementDOM = getElementDOM;
3184 window.getBotIdFromElement = getBotIdFromElement;
3185
3186 }); // End of jQuery ready
3187
3188
3189 // ====================================
3190 // GLOBAL EVENT LISTENERS (Outside jQuery)
3191 // ====================================
3192
3193 // Event listener for copy button (code blocks)
3194 document.addEventListener("click", (e) => {
3195 if (e.target.classList.contains("mxchat-copy-button")) {
3196 const copyButton = e.target;
3197 const codeBlock = copyButton
3198 .closest(".mxchat-code-block-container")
3199 .querySelector(".mxchat-code-block code");
3200
3201 if (codeBlock) {
3202 // Preserve formatting using innerText
3203 navigator.clipboard.writeText(codeBlock.innerText).then(() => {
3204 copyButton.textContent = "Copied!";
3205 copyButton.setAttribute("aria-label", "Copied to clipboard");
3206
3207 setTimeout(() => {
3208 copyButton.textContent = "Copy";
3209 copyButton.setAttribute("aria-label", "Copy to clipboard");
3210 }, 2000);
3211 });
3212 }
3213 }
3214 });
3215
3216