PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.5.5
MxChat – AI Chatbot & Content Generation for WordPress v2.5.5
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 2.5.5, at js/chat-script.js

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