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

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

2,968 lines 113.5 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 var fullMessage = linkify(responseText);
1139 if (responseHtml) {
1140 // Only add line breaks if there's actual text content before the HTML
1141 if (fullMessage && fullMessage.trim()) {
1142 fullMessage += '<br><br>' + responseHtml;
1143 } else {
1144 fullMessage = responseHtml;
1145 }
1146 }
1147
1148 if (images.length > 0) {
1149 fullMessage += '<div class="image-gallery" dir="auto">';
1150 images.forEach(img => {
1151 fullMessage += `
1152 <div style="margin-bottom: 10px;">
1153 <strong>${img.title}</strong><br>
1154 <a href="${img.image_url}" target="_blank">
1155 <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
1156 </a>
1157 </div>`;
1158 });
1159 fullMessage += '</div>';
1160 }
1161
1162 if (lastMessageDiv.length) {
1163 lastMessageDiv.fadeOut(200, function() {
1164 $(this)
1165 .html(fullMessage)
1166 .removeClass('bot-message user-message')
1167 .addClass(messageClass)
1168 .attr('dir', 'auto')
1169 .css({
1170 'background-color': bgColor,
1171 'color': fontColor,
1172 })
1173 .removeClass('temporary-message')
1174 .fadeIn(200, function() {
1175 // FIXED: Use the helper function for link tracking
1176 if (sender === "bot" || sender === "agent") {
1177 attachLinkTracking($(this), responseText);
1178 }
1179
1180 if (sender === "bot" || sender === "agent") {
1181 const lastUserMessage = $('#chat-box').find('.user-message').last();
1182 if (lastUserMessage.length) {
1183 scrollElementToTop(lastUserMessage);
1184 }
1185 // Show notification if chat is hidden
1186 if ($('#floating-chatbot').hasClass('hidden')) {
1187 showNotification();
1188 }
1189 }
1190 });
1191 });
1192 } else {
1193 appendMessage(sender, responseText, responseHtml, images);
1194 }
1195 }
1196
1197
1198 function appendThinkingMessage() {
1199 // Remove any existing thinking dots first
1200 $('.thinking-dots').remove();
1201
1202 // Retrieve the bot message font color and background color
1203 var botMessageFontColor = mxchatChat.bot_message_font_color;
1204 var botMessageBgColor = mxchatChat.bot_message_bg_color;
1205
1206
1207 var thinkingHtml = '<div class="thinking-dots-container">' +
1208 '<div class="thinking-dots">' +
1209 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1210 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1211 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
1212 '</div>' +
1213 '</div>';
1214
1215 // Append the thinking dots to the chat container (or within the temporary message div)
1216 $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
1217 scrollToBottom();
1218 }
1219
1220 function removeThinkingDots() {
1221 $('.thinking-dots').closest('.temporary-message').remove();
1222 }
1223
1224 // ====================================
1225 // TEXT FORMATTING & PROCESSING
1226 // ====================================
1227
1228 function linkify(inputText) {
1229 if (!inputText) {
1230 return '';
1231 }
1232
1233 // Helper function to check if URL is already encoded
1234 function isUrlEncoded(url) {
1235 // Check for % followed by exactly 2 hex digits
1236 return /%[0-9a-fA-F]{2}/.test(url);
1237 }
1238
1239 // Helper function to safely encode URLs only if needed
1240 function safeEncodeUrl(url) {
1241 // If URL already contains encoded characters, return as-is
1242 if (isUrlEncoded(url)) {
1243 return url;
1244 }
1245 // Otherwise, encode it
1246 return encodeURI(url);
1247 }
1248
1249 // Process markdown headers FIRST
1250 let processedText = formatMarkdownHeaders(inputText);
1251
1252 // Process text styling (bold, italic, strikethrough)
1253 processedText = formatTextStyling(processedText);
1254
1255 // Process code blocks BEFORE processing links
1256 processedText = formatCodeBlocks(processedText);
1257
1258 // NOW convert to paragraphs
1259 processedText = convertNewlinesToBreaks(processedText);
1260
1261 // IMPORTANT: Handle citation-style brackets FIRST [URL]
1262 // This prevents them from being processed as markdown links
1263 // Match [URL] where URL is a complete URL in square brackets (common in AI citations)
1264 processedText = processedText.replace(/\[(https?:\/\/[^\]]+)\]/g, (match, url) => {
1265 // Clean the URL of any trailing punctuation
1266 let cleanUrl = url.replace(/[.,;!?]+$/, '');
1267 const safeUrl = safeEncodeUrl(cleanUrl);
1268 // Return as a proper link without the brackets
1269 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1270 });
1271
1272 // Process proper markdown links with text: [text](url)
1273 // This MUST have non-empty text in the first brackets
1274 const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
1275 processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
1276 // Make sure we have actual text (not just whitespace)
1277 if (!text || !text.trim()) {
1278 // If no text, treat the URL as the text
1279 let cleanUrl = url.replace(/[.,;!?]+$/, '');
1280 const safeUrl = safeEncodeUrl(cleanUrl);
1281 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1282 }
1283
1284 // Clean the URL
1285 let cleanUrl = url.replace(/[\].,;!?]+$/, '');
1286 const safeUrl = safeEncodeUrl(cleanUrl);
1287 const safeText = sanitizeUserInput(text);
1288 return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
1289 });
1290
1291 // Handle empty markdown links: [](url)
1292 // This is a specific case where there's no text
1293 const emptyMarkdownPattern = /\[\]\((https?:\/\/[^\s)]+)\)/g;
1294 processedText = processedText.replace(emptyMarkdownPattern, (match, url) => {
1295 let cleanUrl = url.replace(/[.,;!?]+$/, '');
1296 const safeUrl = safeEncodeUrl(cleanUrl);
1297 // Use the URL itself as the link text
1298 return `<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1299 });
1300
1301 // Process phone numbers: [text](tel:number)
1302 const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
1303 processedText = processedText.replace(phonePattern, (match, text, phone) => {
1304 const safePhone = safeEncodeUrl(phone);
1305 const safeText = sanitizeUserInput(text);
1306 return `<a href="${safePhone}">${safeText}</a>`;
1307 });
1308
1309 // Process mailto links: [text](mailto:email)
1310 const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
1311 processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
1312 const safeMailto = safeEncodeUrl(mailto);
1313 const safeText = sanitizeUserInput(text);
1314 return `<a href="${safeMailto}">${safeText}</a>`;
1315 });
1316
1317 // Process standalone URLs - but NOT if they're already in <a> tags or brackets
1318 // Updated pattern to be more careful about what it matches
1319 const urlPattern = /(^|[^">=\[\]])(https?:\/\/[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim;
1320 processedText = processedText.replace(urlPattern, (match, prefix, url) => {
1321 // Extra check: make sure this isn't already linked
1322 if (match.includes('href=') || match.includes('</a>')) {
1323 return match;
1324 }
1325
1326 // Clean trailing punctuation
1327 let cleanUrl = url.replace(/[.,;!?)]+$/, '');
1328 const safeUrl = safeEncodeUrl(cleanUrl);
1329 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1330 });
1331
1332 // Process www. URLs - but NOT if they're already in <a> tags or brackets
1333 const wwwPattern = /(^|[^">/\[\]])(www\.[\S]+)(?![^<]*<\/a>)(?!\])/gim;
1334 processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
1335 // Extra check: make sure this isn't already linked
1336 if (match.includes('href=') || match.includes('</a>')) {
1337 return match;
1338 }
1339
1340 // Clean trailing punctuation
1341 let cleanUrl = url.replace(/[.,;!?)]+$/, '');
1342 const safeUrl = safeEncodeUrl(`http://${cleanUrl}`);
1343 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${cleanUrl}</a>`;
1344 });
1345
1346 return processedText;
1347 }
1348
1349 function formatMarkdownHeaders(text) {
1350 // Handle h1 to h6 headers
1351 return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
1352 const level = hashes.length;
1353 return `<h${level} class="chat-heading chat-heading-${level}">${content.trim()}</h${level}>`;
1354 });
1355 }
1356
1357 function formatTextStyling(text) {
1358 // IMPORTANT: Protect BOTH HTML href and Markdown URLs from formatting
1359 const protectedSegments = [];
1360 let protectedText = text;
1361
1362 // Step 1a: Protect HTML href="..." attributes
1363 protectedText = protectedText.replace(/href\s*=\s*["']([^"']+)["']/gi, function(match) {
1364 const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1365 protectedSegments.push(match);
1366 return placeholder;
1367 });
1368
1369 // Step 1b: Protect Markdown links [text](url)
1370 // This is crucial - we need to protect the URLs in markdown format
1371 protectedText = protectedText.replace(/\[([^\]]*)\]\(([^)]+)\)/g, function(match) {
1372 const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1373 protectedSegments.push(match);
1374 return placeholder;
1375 });
1376
1377 // Step 1c: Also protect bare URLs that might exist
1378 protectedText = protectedText.replace(/(https?:\/\/[^\s<>"]+)/gi, function(match) {
1379 const placeholder = `__PROTECTED_${protectedSegments.length}__`;
1380 protectedSegments.push(match);
1381 return placeholder;
1382 });
1383
1384 // Step 2: Now apply text styling to the protected text
1385 // Handle bold text (**text**)
1386 protectedText = protectedText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1387
1388 // Handle italic text (*text* or _text_) - Safari-compatible (no lookbehind)
1389 // Match single asterisks that aren't part of bold (**) by checking they're not followed/preceded by another *
1390 protectedText = protectedText.replace(/(?!\*\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
1391
1392 // Handle underscores for italic - Safari-compatible (no lookbehind)
1393 // Exclude __PROTECTED_N__ placeholders by checking the content doesn't contain PROTECTED
1394 protectedText = protectedText.replace(/(?!__)_((?!PROTECTED)[^_\n]+)_(?!_)/g, '<em>$1</em>');
1395
1396 // Handle strikethrough (~~text~~)
1397 protectedText = protectedText.replace(/~~(.*?)~~/g, '<del>$1</del>');
1398
1399 // Step 3: Restore all protected segments
1400 protectedSegments.forEach((original, index) => {
1401 const placeholder = `__PROTECTED_${index}__`;
1402 protectedText = protectedText.replace(placeholder, original);
1403 });
1404
1405 return protectedText;
1406 }
1407 function formatBoldText(text) {
1408 // This function is kept for compatibility but now uses formatTextStyling
1409 return formatTextStyling(text);
1410 }
1411
1412 function convertNewlinesToBreaks(text) {
1413 // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
1414 const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
1415
1416 // Filter out empty paragraphs and wrap each paragraph in <p> tags
1417 return paragraphs
1418 .map(para => para.trim())
1419 .filter(para => para.length > 0) // Remove empty paragraphs
1420 .map(para => `<p>${para}</p>`)
1421 .join('');
1422 }
1423 function formatCodeBlocks(text) {
1424 // Handle fenced code blocks with language specification (```language)
1425 text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, language, code) => {
1426 const lang = language || 'text';
1427 const escapedCode = escapeHtml(code.trim());
1428 return `<div class="mxchat-code-block-container">
1429 <div class="mxchat-code-header">
1430 <span class="mxchat-code-language">${lang}</span>
1431 <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1432 </div>
1433 <pre class="mxchat-code-block"><code class="language-${lang}">${escapedCode}</code></pre>
1434 </div>`;
1435 });
1436
1437 // Handle inline code with single backticks
1438 text = text.replace(/`([^`\n]+)`/g, '<code class="mxchat-inline-code">$1</code>');
1439
1440 // Handle raw PHP tags (legacy support)
1441 text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
1442 const escapedCode = escapeHtml(match);
1443 return `<div class="mxchat-code-block-container">
1444 <div class="mxchat-code-header">
1445 <span class="mxchat-code-language">php</span>
1446 <button class="mxchat-copy-button" aria-label="Copy to clipboard">Copy</button>
1447 </div>
1448 <pre class="mxchat-code-block"><code class="language-php">${escapedCode}</code></pre>
1449 </div>`;
1450 });
1451
1452 return text;
1453 }
1454
1455 function sanitizeUserInput(text) {
1456 const div = document.createElement('div');
1457 div.textContent = text;
1458 return div.innerHTML;
1459 }
1460
1461 function escapeHtml(unsafe) {
1462 // Skip escaping if it's already escaped or contains HTML code block markup
1463 if (unsafe.includes('&lt;') || unsafe.includes('&gt;') ||
1464 unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
1465 return unsafe;
1466 }
1467
1468 return unsafe
1469 .replace(/&/g, "&amp;")
1470 .replace(/</g, "&lt;")
1471 .replace(/>/g, "&gt;")
1472 .replace(/"/g, "&quot;")
1473 .replace(/'/g, "&#039;");
1474 }
1475
1476 function decodeHTMLEntities(text) {
1477 var textArea = document.createElement('textarea');
1478 textArea.innerHTML = text;
1479 return textArea.value;
1480 }
1481
1482 // ====================================
1483 // UI & SCROLLING CONTROLS
1484 // ====================================
1485
1486 function scrollToBottom(instant = false) {
1487 var chatBox = $('#chat-box');
1488 if (instant) {
1489 // Instantly set the scroll position to the bottom
1490 chatBox.scrollTop(chatBox.prop("scrollHeight"));
1491 } else {
1492 // Use requestAnimationFrame for smoother scrolling if needed
1493 let start = null;
1494 const scrollHeight = chatBox.prop("scrollHeight");
1495 const initialScroll = chatBox.scrollTop();
1496 const distance = scrollHeight - initialScroll;
1497 const duration = 500; // Duration in ms
1498
1499 function smoothScroll(timestamp) {
1500 if (!start) start = timestamp;
1501 const progress = timestamp - start;
1502 const currentScroll = initialScroll + (distance * (progress / duration));
1503 chatBox.scrollTop(currentScroll);
1504
1505 if (progress < duration) {
1506 requestAnimationFrame(smoothScroll);
1507 } else {
1508 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
1509 }
1510 }
1511
1512 requestAnimationFrame(smoothScroll);
1513 }
1514 }
1515
1516 function scrollElementToTop(element) {
1517 var chatBox = $('#chat-box');
1518 var elementTop = element.position().top + chatBox.scrollTop();
1519 chatBox.animate({ scrollTop: elementTop }, 500);
1520 }
1521
1522 function showChatWidget() {
1523 // First ensure display is set
1524 $('#floating-chatbot-button').css('display', 'flex');
1525 // Then handle the fade
1526 $('#floating-chatbot-button').fadeTo(500, 1);
1527 // Force visibility
1528 $('#floating-chatbot-button').removeClass('hidden');
1529 //console.log('Showing widget');
1530 }
1531
1532 function hideChatWidget() {
1533 $('#floating-chatbot-button').css('display', 'none');
1534 $('#floating-chatbot-button').addClass('hidden');
1535 //console.log('Hiding widget');
1536 }
1537
1538 function disableScroll() {
1539 if (isMobile()) {
1540 $('body').css('overflow', 'hidden');
1541 }
1542 }
1543
1544 function enableScroll() {
1545 if (isMobile()) {
1546 $('body').css('overflow', '');
1547 }
1548 }
1549
1550 function isMobile() {
1551 // This can be a simple check, or more sophisticated detection of mobile devices
1552 return window.innerWidth <= 768; // Example threshold for mobile devices
1553 }
1554
1555 function setFullHeight() {
1556 var vh = $(window).innerHeight() * 0.01;
1557 $(':root').css('--vh', vh + 'px');
1558 }
1559
1560
1561 // ====================================
1562 // NOTIFICATION SYSTEM
1563 // ====================================
1564
1565 function createNotificationBadge() {
1566 //console.log("Creating notification badge...");
1567 const chatButton = document.getElementById('floating-chatbot-button');
1568 //console.log("Chat button found:", !!chatButton);
1569
1570 if (!chatButton) return;
1571
1572 // Remove any existing badge first
1573 const existingBadge = chatButton.querySelector('.chat-notification-badge');
1574 if (existingBadge) {
1575 //console.log("Removing existing badge");
1576 existingBadge.remove();
1577 }
1578
1579 notificationBadge = document.createElement('div');
1580 notificationBadge.className = 'chat-notification-badge';
1581 notificationBadge.style.cssText = `
1582 display: none;
1583 position: absolute;
1584 top: -5px;
1585 right: -5px;
1586 background-color: red;
1587 color: white;
1588 border-radius: 50%;
1589 padding: 4px 8px;
1590 font-size: 12px;
1591 font-weight: bold;
1592 z-index: 10001;
1593 `;
1594 chatButton.style.position = 'relative';
1595 chatButton.appendChild(notificationBadge);
1596
1597 }
1598
1599 function showNotification() {
1600 const badge = document.getElementById('chat-notification-badge');
1601 if (badge && $('#floating-chatbot').hasClass('hidden')) {
1602 badge.style.display = 'block';
1603 badge.textContent = '1';
1604 }
1605 }
1606
1607 function hideNotification() {
1608 const badge = document.getElementById('chat-notification-badge');
1609 if (badge) {
1610 badge.style.display = 'none';
1611 }
1612 }
1613
1614 function startNotificationChecking() {
1615 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1616 if (!chatPersistenceEnabled) return;
1617
1618 createNotificationBadge();
1619 notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
1620 }
1621
1622 function stopNotificationChecking() {
1623 if (notificationCheckInterval) {
1624 clearInterval(notificationCheckInterval);
1625 }
1626 }
1627
1628 function checkForNewMessages() {
1629 const sessionId = getChatSession();
1630 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1631
1632 if (!chatPersistenceEnabled) return;
1633
1634 $.ajax({
1635 url: mxchatChat.ajax_url,
1636 type: 'POST',
1637 data: {
1638 action: 'mxchat_check_new_messages',
1639 session_id: sessionId,
1640 last_seen_id: lastSeenMessageId,
1641 nonce: mxchatChat.nonce
1642 },
1643 success: function(response) {
1644 if (response.success && response.data.hasNewMessages) {
1645 showNotification();
1646 }
1647 }
1648 });
1649 }
1650
1651
1652 // ====================================
1653 // LIVE AGENT FUNCTIONALITY
1654 // ====================================
1655
1656 function updateChatModeIndicator(mode) {
1657 //console.log("updateChatModeIndicator called with mode:", mode);
1658 const indicator = document.getElementById('chat-mode-indicator');
1659 if (indicator) {
1660 const oldText = indicator.textContent;
1661
1662 if (mode === 'agent') {
1663 indicator.textContent = 'Live Agent';
1664 startPolling();
1665 //console.log("Set indicator to Live Agent");
1666 } else {
1667 // Everything else is AI mode
1668 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
1669 indicator.textContent = customAiText;
1670 stopPolling();
1671 //console.log("Set indicator to AI Agent:", customAiText);
1672 }
1673
1674 // Force immediate DOM update
1675 if (oldText !== indicator.textContent) {
1676 // Force a reflow to ensure the change is visible immediately
1677 indicator.offsetHeight;
1678
1679 // Double-check after a brief moment
1680 setTimeout(() => {
1681 //console.log("Final indicator text:", indicator.textContent);
1682 }, 50);
1683 }
1684 } else {
1685 //console.error("Chat mode indicator element not found!");
1686 }
1687 }
1688
1689 function startPolling() {
1690 // Clear any existing interval first
1691 stopPolling();
1692 // Start new polling interval
1693 pollingInterval = setInterval(checkForAgentMessages, 5000);
1694 //console.log("Started agent message polling");
1695 }
1696
1697 function stopPolling() {
1698 if (pollingInterval) {
1699 clearInterval(pollingInterval);
1700 pollingInterval = null;
1701 //console.log("Stopped agent message polling");
1702 }
1703 }
1704
1705 function checkForAgentMessages() {
1706 const sessionId = getChatSession();
1707 $.ajax({
1708 url: mxchatChat.ajax_url,
1709 type: 'POST',
1710 dataType: 'json',
1711 data: {
1712 action: 'mxchat_fetch_new_messages',
1713 session_id: sessionId,
1714 last_seen_id: lastSeenMessageId,
1715 persistence_enabled: 'true', // Add this too
1716 nonce: mxchatChat.nonce
1717 },
1718 success: function (response) {
1719 if (response.success && response.data?.new_messages) {
1720 let hasNewMessage = false;
1721
1722 response.data.new_messages.forEach(function (message) {
1723 if (message.role === "agent" && !processedMessageIds.has(message.id)) {
1724 hasNewMessage = true;
1725 // CHANGE THIS LINE:
1726 appendMessage("agent", message.content); // Instead of replaceLastMessage
1727 lastSeenMessageId = message.id;
1728 processedMessageIds.add(message.id);
1729 }
1730 });
1731
1732 if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
1733 showNotification();
1734 }
1735
1736 scrollToBottom(true);
1737 }
1738 },
1739 error: function (xhr, status, error) {
1740 //console.error("Polling error:", xhr, status, error);
1741 }
1742 });
1743 }
1744
1745 // ====================================
1746 // CHAT HISTORY & PERSISTENCE
1747 // ====================================
1748 // Update the loadChatHistory function:
1749 function loadChatHistory() {
1750 // Prevent duplicate loading
1751 if (chatHistoryLoaded) {
1752 // console.log('Chat history already loaded, skipping...');
1753 return;
1754 }
1755
1756 var sessionId = getChatSession();
1757 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1758
1759 if (chatPersistenceEnabled && sessionId) {
1760 $.ajax({
1761 url: mxchatChat.ajax_url,
1762 type: 'POST',
1763 dataType: 'json',
1764 data: {
1765 action: 'mxchat_fetch_conversation_history',
1766 session_id: sessionId
1767 },
1768 success: function(response) {
1769 // Check if the response indicates success
1770 if (response.success) {
1771 // Handle case where conversation data exists and is an array
1772 if (response.data && Array.isArray(response.data.conversation)) {
1773 var $chatBox = $('#chat-box');
1774 var $fragment = $(document.createDocumentFragment());
1775 let highestMessageId = lastSeenMessageId;
1776
1777 // Update chat mode if provided
1778 if (response.data.chat_mode) {
1779 updateChatModeIndicator(response.data.chat_mode);
1780 }
1781
1782 // Only process if there are actual messages
1783 if (response.data.conversation.length > 0) {
1784 // IMPORTANT: Clear existing messages before loading history
1785 $chatBox.empty();
1786
1787 $.each(response.data.conversation, function(index, message) {
1788 // Skip agent messages if persistence is off
1789 if (!chatPersistenceEnabled && message.role === 'agent') {
1790 return;
1791 }
1792
1793 var messageClass, messageBgColor, messageFontColor;
1794
1795 switch (message.role) {
1796 case 'user':
1797 messageClass = 'user-message';
1798 messageBgColor = userMessageBgColor;
1799 messageFontColor = userMessageFontColor;
1800 break;
1801 case 'agent':
1802 messageClass = 'agent-message';
1803 messageBgColor = liveAgentMessageBgColor;
1804 messageFontColor = liveAgentMessageFontColor;
1805 break;
1806 default:
1807 messageClass = 'bot-message';
1808 messageBgColor = botMessageBgColor;
1809 messageFontColor = botMessageFontColor;
1810 break;
1811 }
1812
1813 var messageElement = $('<div>').addClass(messageClass)
1814 .css({
1815 'background': messageBgColor,
1816 'color': messageFontColor
1817 });
1818
1819 var content = message.content;
1820 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
1821 content = decodeHTMLEntities(content);
1822
1823 if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
1824 messageElement.html(content);
1825 } else {
1826 var formattedContent = linkify(content);
1827 messageElement.html(formattedContent);
1828 }
1829
1830 $fragment.append(messageElement);
1831
1832 // Track message IDs
1833 if (message.id) {
1834 highestMessageId = Math.max(highestMessageId, message.id);
1835 processedMessageIds.add(message.id);
1836 }
1837 });
1838
1839 // Only append messages and scroll if we have content
1840 $chatBox.append($fragment);
1841 scrollToBottom(true);
1842
1843 // Collapse quick questions if we have conversation history
1844 if (hasQuickQuestions()) {
1845 collapseQuickQuestions();
1846 }
1847
1848 // Update lastSeenMessageId after history loads
1849 lastSeenMessageId = highestMessageId;
1850
1851 // Only update chat mode if persistence is enabled and we have messages
1852 if (chatPersistenceEnabled) {
1853 var lastMessage = response.data.conversation[response.data.conversation.length - 1];
1854 if (lastMessage.role === 'agent') {
1855 updateChatModeIndicator('agent');
1856 }
1857 }
1858
1859 // Mark as loaded ONLY after successful load
1860 chatHistoryLoaded = true;
1861 }
1862 }
1863 }
1864 },
1865 error: function(xhr, status, error) {
1866 //console.error("AJAX error loading chat history:", status, error);
1867 }
1868 });
1869 }
1870 }
1871
1872
1873 // ====================================
1874 // FILE UPLOAD FUNCTIONALITY
1875 // ====================================
1876
1877 function addSafeEventListener(elementId, eventType, handler) {
1878 const element = document.getElementById(elementId);
1879 if (element) {
1880 element.addEventListener(eventType, handler);
1881 }
1882 }
1883
1884 function showActivePdf(filename) {
1885 const container = document.getElementById('active-pdf-container');
1886 const nameElement = document.getElementById('active-pdf-name');
1887
1888 if (!container || !nameElement) {
1889 //console.error('PDF container elements not found');
1890 return;
1891 }
1892
1893 nameElement.textContent = filename;
1894 container.style.display = 'flex';
1895 }
1896
1897 function showActiveWord(filename) {
1898 const container = document.getElementById('active-word-container');
1899 const nameElement = document.getElementById('active-word-name');
1900
1901 if (!container || !nameElement) {
1902 //console.error('Word document container elements not found');
1903 return;
1904 }
1905
1906 nameElement.textContent = filename;
1907 container.style.display = 'flex';
1908 }
1909
1910 function removeActivePdf() {
1911 const container = document.getElementById('active-pdf-container');
1912 const nameElement = document.getElementById('active-pdf-name');
1913
1914 if (!container || !nameElement || !activePdfFile) return;
1915
1916 fetch(mxchatChat.ajax_url, {
1917 method: 'POST',
1918 headers: {
1919 'Content-Type': 'application/x-www-form-urlencoded',
1920 },
1921 body: new URLSearchParams({
1922 'action': 'mxchat_remove_pdf',
1923 'session_id': sessionId,
1924 'nonce': mxchatChat.nonce
1925 })
1926 })
1927 .then(response => response.json())
1928 .then(data => {
1929 if (data.success) {
1930 container.style.display = 'none';
1931 nameElement.textContent = '';
1932 activePdfFile = null;
1933 appendMessage('bot', 'PDF removed.');
1934 }
1935 })
1936 .catch(error => {
1937 //console.error('Error removing PDF:', error);
1938 });
1939 }
1940
1941 function removeActiveWord() {
1942 const container = document.getElementById('active-word-container');
1943 const nameElement = document.getElementById('active-word-name');
1944
1945 if (!container || !nameElement || !activeWordFile) return;
1946
1947 fetch(mxchatChat.ajax_url, {
1948 method: 'POST',
1949 headers: {
1950 'Content-Type': 'application/x-www-form-urlencoded',
1951 },
1952 body: new URLSearchParams({
1953 'action': 'mxchat_remove_word',
1954 'session_id': sessionId,
1955 'nonce': mxchatChat.nonce
1956 })
1957 })
1958 .then(response => response.json())
1959 .then(data => {
1960 if (data.success) {
1961 container.style.display = 'none';
1962 nameElement.textContent = '';
1963 activeWordFile = null;
1964 appendMessage('bot', 'Word document removed.');
1965 }
1966 })
1967 .catch(error => {
1968 //console.error('Error removing Word document:', error);
1969 });
1970 }
1971
1972 // ====================================
1973 // CONSENT & COMPLIANCE (GDPR)
1974 // ====================================
1975
1976 function initializeChatVisibility() {
1977 //console.log('Initializing chat visibility');
1978 const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
1979 mxchatChat.complianz_toggle === '1' ||
1980 mxchatChat.complianz_toggle === 1;
1981
1982 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
1983 // Initial check
1984 checkConsentAndShowChat();
1985
1986 // Listen for consent changes
1987 $(document).on('cmplz_status_change', function(event) {
1988 //console.log('Status change detected');
1989 checkConsentAndShowChat();
1990 });
1991 } else {
1992 // If Complianz is not enabled, always show
1993 $('#floating-chatbot-button')
1994 .css('display', 'flex')
1995 .removeClass('hidden no-consent')
1996 .fadeTo(500, 1);
1997
1998 // Also check pre-chat message when Complianz is not enabled
1999 checkPreChatDismissal();
2000 }
2001 }
2002
2003
2004 function checkConsentAndShowChat() {
2005 var consentStatus = cmplz_has_consent('marketing');
2006 var consentType = complianz.consenttype;
2007
2008 //console.log('Checking consent:', {status: consentStatus,type: consentType});
2009
2010 let $widget = $('#floating-chatbot-button');
2011 let $chatbot = $('#floating-chatbot');
2012 let $preChat = $('#pre-chat-message');
2013
2014 if (consentStatus === true) {
2015 //console.log('Consent granted - showing widget');
2016 $widget
2017 .removeClass('no-consent')
2018 .css('display', 'flex')
2019 .removeClass('hidden')
2020 .fadeTo(500, 1);
2021 $chatbot.removeClass('no-consent');
2022
2023 // Show pre-chat message if not dismissed
2024 checkPreChatDismissal();
2025 } else {
2026 //console.log('No consent - hiding widget');
2027 $widget
2028 .addClass('no-consent')
2029 .fadeTo(500, 0, function() {
2030 $(this)
2031 .css('display', 'none')
2032 .addClass('hidden');
2033 });
2034 $chatbot.addClass('no-consent');
2035
2036 // Hide pre-chat message when no consent
2037 $preChat.hide();
2038 }
2039 }
2040
2041
2042 // ====================================
2043 // PRE-CHAT MESSAGE HANDLING
2044 // ====================================
2045
2046 function checkPreChatDismissal() {
2047 $.ajax({
2048 url: mxchatChat.ajax_url,
2049 type: 'POST',
2050 data: {
2051 action: 'mxchat_check_pre_chat_message_status',
2052 _ajax_nonce: mxchatChat.nonce
2053 },
2054 success: function(response) {
2055 if (response.success && !response.data.dismissed) {
2056 $('#pre-chat-message').fadeIn(250);
2057 } else {
2058 $('#pre-chat-message').hide();
2059 }
2060 },
2061 error: function() {
2062 //console.error('Failed to check pre-chat message dismissal status.');
2063 }
2064 });
2065 }
2066
2067 function handlePreChatDismissal() {
2068 $('#pre-chat-message').fadeOut(200);
2069 $.ajax({
2070 url: mxchatChat.ajax_url,
2071 type: 'POST',
2072 data: {
2073 action: 'mxchat_dismiss_pre_chat_message',
2074 _ajax_nonce: mxchatChat.nonce
2075 },
2076 success: function() {
2077 $('#pre-chat-message').hide();
2078 },
2079 error: function() {
2080 //console.error('Failed to dismiss pre-chat message.');
2081 }
2082 });
2083 }
2084
2085
2086 // ====================================
2087 // UTILITY FUNCTIONS
2088 // ====================================
2089
2090 function copyToClipboard(text) {
2091 var tempInput = $('<input>');
2092 $('body').append(tempInput);
2093 tempInput.val(text).select();
2094 document.execCommand('copy');
2095 tempInput.remove();
2096 }
2097
2098
2099 function isImageHtml(str) {
2100 return str.startsWith('<img') && str.endsWith('>');
2101 }
2102
2103
2104 // ====================================
2105 // EVENT HANDLERS & INITIALIZATION
2106 // ====================================
2107
2108 $(document).on('click', '.mxchat-popular-question', function () {
2109 var question = $(this).text();
2110
2111 // Append the question as if the user typed it
2112 appendMessage("user", question);
2113
2114 // Only collapse if there are questions
2115 if (hasQuickQuestions()) {
2116 collapseQuickQuestions();
2117 }
2118
2119 // Send the question to the server
2120 sendMessageToChatbot(question);
2121 });
2122
2123 $(document).on('click', '.questions-toggle-btn', function(e) {
2124 e.preventDefault();
2125 e.stopPropagation();
2126 expandQuickQuestions();
2127 });
2128
2129 $(document).on('click', '.questions-collapse-btn', function(e) {
2130 e.preventDefault();
2131 e.stopPropagation();
2132 collapseQuickQuestions();
2133 });
2134
2135 // Chatbot visibility toggle handlers
2136 $(document).on('click', '#floating-chatbot-button', function() {
2137 var chatbot = $('#floating-chatbot');
2138 if (chatbot.hasClass('hidden')) {
2139 chatbot.removeClass('hidden').addClass('visible');
2140 $(this).addClass('hidden');
2141 $('#chat-notification-badge').hide(); // Hide notification when opening chat
2142 disableScroll();
2143 $('#pre-chat-message').fadeOut(250);
2144 } else {
2145 chatbot.removeClass('visible').addClass('hidden');
2146 $(this).removeClass('hidden');
2147 enableScroll();
2148 checkPreChatDismissal();
2149 }
2150 });
2151
2152 $(document).on('click', '#exit-chat-button', function() {
2153 $('#floating-chatbot').addClass('hidden').removeClass('visible');
2154 $('#floating-chatbot-button').removeClass('hidden');
2155 enableScroll();
2156 });
2157
2158 $(document).on('click', '.close-pre-chat-message', function(e) {
2159 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
2160 $('#pre-chat-message').fadeOut(200, function() {
2161 $(this).remove();
2162 });
2163 });
2164
2165
2166 // PDF upload button handlers
2167 if (document.getElementById('pdf-upload-btn')) {
2168 document.getElementById('pdf-upload-btn').addEventListener('click', function() {
2169 document.getElementById('pdf-upload').click();
2170 });
2171 }
2172
2173 // Word upload button handlers
2174 if (document.getElementById('word-upload-btn')) {
2175 document.getElementById('word-upload-btn').addEventListener('click', function() {
2176 document.getElementById('word-upload').click();
2177 });
2178 }
2179
2180 // PDF file input change handler
2181 addSafeEventListener('pdf-upload', 'change', async function(e) {
2182 const file = e.target.files[0];
2183
2184 if (!file || file.type !== 'application/pdf') {
2185 alert('Please select a valid PDF file.');
2186 return;
2187 }
2188
2189 if (!sessionId) {
2190 //console.error('No session ID found');
2191 alert('Error: No session ID found');
2192 return;
2193 }
2194
2195 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
2196 //console.error('mxchatChat not properly configured:', mxchatChat);
2197 alert('Error: Ajax configuration missing');
2198 return;
2199 }
2200
2201 // Disable buttons and show loading state
2202 const uploadBtn = document.getElementById('pdf-upload-btn');
2203 const sendBtn = document.getElementById('send-button');
2204 const originalBtnContent = uploadBtn.innerHTML;
2205
2206 try {
2207 const formData = new FormData();
2208 formData.append('action', 'mxchat_upload_pdf');
2209 formData.append('pdf_file', file);
2210 formData.append('session_id', sessionId);
2211 formData.append('nonce', mxchatChat.nonce);
2212
2213 uploadBtn.disabled = true;
2214 sendBtn.disabled = true;
2215 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2216 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2217 </svg>`;
2218
2219 const response = await fetch(mxchatChat.ajax_url, {
2220 method: 'POST',
2221 body: formData
2222 });
2223
2224 const data = await response.json();
2225
2226 if (data.success) {
2227 // Hide popular questions if they exist
2228 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2229 if (hasQuickQuestions()) {
2230 collapseQuickQuestions();
2231 }
2232
2233 // Show the active PDF name
2234 showActivePdf(data.data.filename);
2235
2236 appendMessage('bot', data.data.message);
2237 scrollToBottom();
2238 activePdfFile = data.data.filename;
2239 } else {
2240 //console.error('Upload failed:', data.data);
2241 alert('Failed to upload PDF. Please try again.');
2242 }
2243 } catch (error) {
2244 //console.error('Upload error:', error);
2245 alert('Error uploading file. Please try again.');
2246 } finally {
2247 uploadBtn.disabled = false;
2248 sendBtn.disabled = false;
2249 uploadBtn.innerHTML = originalBtnContent;
2250 this.value = ''; // Reset file input
2251 }
2252 });
2253
2254 // Word file input change handler
2255 addSafeEventListener('word-upload', 'change', async function(e) {
2256 const file = e.target.files[0];
2257
2258 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
2259 alert('Please select a valid Word document (.docx).');
2260 return;
2261 }
2262
2263 if (!sessionId) {
2264 //console.error('No session ID found');
2265 alert('Error: No session ID found');
2266 return;
2267 }
2268
2269 // Disable buttons and show loading state
2270 const uploadBtn = document.getElementById('word-upload-btn');
2271 const sendBtn = document.getElementById('send-button');
2272 const originalBtnContent = uploadBtn.innerHTML;
2273
2274 try {
2275 const formData = new FormData();
2276 formData.append('action', 'mxchat_upload_word');
2277 formData.append('word_file', file);
2278 formData.append('session_id', sessionId);
2279 formData.append('nonce', mxchatChat.nonce);
2280
2281 uploadBtn.disabled = true;
2282 sendBtn.disabled = true;
2283 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
2284 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
2285 </svg>`;
2286
2287 const response = await fetch(mxchatChat.ajax_url, {
2288 method: 'POST',
2289 body: formData
2290 });
2291
2292 const data = await response.json();
2293
2294 if (data.success) {
2295 // Hide popular questions if they exist
2296 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
2297 if (hasQuickQuestions()) {
2298 collapseQuickQuestions();
2299 }
2300
2301 // Show the active Word document name
2302 showActiveWord(data.data.filename);
2303
2304 appendMessage('bot', data.data.message);
2305 scrollToBottom();
2306 activeWordFile = data.data.filename;
2307 } else {
2308 //console.error('Upload failed:', data.data);
2309 alert('Failed to upload Word document. Please try again.');
2310 }
2311 } catch (error) {
2312 //console.error('Upload error:', error);
2313 alert('Error uploading file. Please try again.');
2314 } finally {
2315 uploadBtn.disabled = false;
2316 sendBtn.disabled = false;
2317 uploadBtn.innerHTML = originalBtnContent;
2318 this.value = ''; // Reset file input
2319 }
2320 });
2321
2322 // Remove button click handlers
2323 document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
2324 e.preventDefault();
2325 e.stopPropagation();
2326 removeActivePdf();
2327 });
2328
2329 document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
2330 e.preventDefault();
2331 e.stopPropagation();
2332 removeActiveWord();
2333 });
2334
2335 // Window resize handlers
2336 $(window).on('resize orientationchange', function() {
2337 setFullHeight();
2338 });
2339
2340
2341 // ====================================
2342 // TOOLBAR & STYLING SETUP
2343 // ====================================
2344
2345 // Apply toolbar settings
2346 if (mxchatChat.chat_toolbar_toggle === 'on') {
2347 $('.chat-toolbar').show();
2348 } else {
2349 $('.chat-toolbar').hide();
2350 }
2351
2352 // Apply toolbar icon colors
2353 const toolbarElements = [
2354 '#mxchat-chatbot .toolbar-btn svg',
2355 '#mxchat-chatbot .active-pdf-name',
2356 '#mxchat-chatbot .active-word-name',
2357 '#mxchat-chatbot .remove-pdf-btn svg',
2358 '#mxchat-chatbot .remove-word-btn svg',
2359 '#mxchat-chatbot .toolbar-perplexity svg'
2360 ];
2361
2362 toolbarElements.forEach(selector => {
2363 $(selector).css({
2364 'fill': toolbarIconColor,
2365 'stroke': toolbarIconColor,
2366 'color': toolbarIconColor
2367 });
2368 });
2369
2370
2371 // ====================================
2372 // EMAIL COLLECTION SETUP - FIXED VERSION
2373 // ====================================
2374 // Only run email collection setup if it's enabled
2375 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') {
2376 //console.log('Email collection is enabled, setting up handlers...');
2377
2378 // Email collection form setup and handlers
2379 const emailForm = document.getElementById('email-collection-form');
2380 const emailBlocker = document.getElementById('email-blocker');
2381 const chatbotWrapper = document.getElementById('chat-container');
2382
2383 if (emailForm && emailBlocker && chatbotWrapper) {
2384
2385 // Add loading state management
2386 let isSubmitting = false;
2387
2388 // Optimized UI transition functions
2389 function showEmailForm() {
2390 emailBlocker.style.display = 'flex';
2391 chatbotWrapper.style.display = 'none';
2392 }
2393
2394 function showChatContainer() {
2395 // Show chat immediately without delay
2396 emailBlocker.style.display = 'none';
2397 chatbotWrapper.style.display = 'flex';
2398
2399 // Load chat history only after showing chat container
2400 if (typeof loadChatHistory === 'function') {
2401 loadChatHistory();
2402 }
2403 }
2404
2405 // Enhanced email validation
2406 function isValidEmail(email) {
2407 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2408 return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit
2409 }
2410
2411 // Enhanced name validation
2412 function isValidName(name) {
2413 return name && name.trim().length >= 2 && name.trim().length <= 100;
2414 }
2415
2416 // Show loading state with spinner
2417 function setSubmissionState(loading) {
2418 const submitButton = document.getElementById('email-submit-button');
2419 const emailInput = document.getElementById('user-email');
2420 const nameInput = document.getElementById('user-name');
2421
2422 if (loading) {
2423 isSubmitting = true;
2424 if (submitButton) submitButton.disabled = true;
2425 if (emailInput) emailInput.disabled = true;
2426 if (nameInput) nameInput.disabled = true;
2427
2428 // Store original content and add spinner
2429 if (submitButton && !submitButton.getAttribute('data-original-html')) {
2430 submitButton.setAttribute('data-original-html', submitButton.innerHTML);
2431
2432 // Add loading spinner while keeping original text
2433 const originalText = submitButton.textContent;
2434 submitButton.innerHTML = `
2435 <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24">
2436 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416">
2437 <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
2438 <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
2439 </circle>
2440 </svg>
2441 ${originalText}
2442 `;
2443
2444 submitButton.style.opacity = '0.8';
2445 }
2446 } else {
2447 isSubmitting = false;
2448 if (submitButton) submitButton.disabled = false;
2449 if (emailInput) emailInput.disabled = false;
2450 if (nameInput) nameInput.disabled = false;
2451
2452 // Restore original content
2453 if (submitButton) {
2454 const originalHtml = submitButton.getAttribute('data-original-html');
2455 if (originalHtml) {
2456 submitButton.innerHTML = originalHtml;
2457 }
2458 submitButton.style.opacity = '1';
2459 }
2460 }
2461 }
2462
2463 // Error display functions
2464 function showEmailError(message) {
2465 clearEmailError();
2466
2467 const errorDiv = document.createElement('div');
2468 errorDiv.className = 'email-error';
2469 errorDiv.style.cssText = `
2470 color: #e74c3c;
2471 font-size: 12px;
2472 margin-top: 8px;
2473 padding: 4px 0;
2474 animation: fadeInError 0.3s ease;
2475 `;
2476 errorDiv.textContent = message;
2477
2478 // Add CSS animation if not already present
2479 if (!document.getElementById('email-error-styles')) {
2480 const style = document.createElement('style');
2481 style.id = 'email-error-styles';
2482 style.textContent = `
2483 @keyframes fadeInError {
2484 from { opacity: 0; transform: translateY(-5px); }
2485 to { opacity: 1; transform: translateY(0); }
2486 }
2487 .email-input-shake {
2488 animation: shake 0.5s ease-in-out;
2489 }
2490 @keyframes shake {
2491 0%, 100% { transform: translateX(0); }
2492 25% { transform: translateX(-5px); }
2493 75% { transform: translateX(5px); }
2494 }
2495 @keyframes spin {
2496 from { transform: rotate(0deg); }
2497 to { transform: rotate(360deg); }
2498 }
2499 .email-spinner {
2500 display: inline-block;
2501 vertical-align: middle;
2502 }
2503 `;
2504 document.head.appendChild(style);
2505 }
2506
2507 emailForm.appendChild(errorDiv);
2508
2509 // Add shake animation to inputs
2510 const emailInput = document.getElementById('user-email');
2511 const nameInput = document.getElementById('user-name');
2512
2513 if (emailInput) {
2514 emailInput.classList.add('email-input-shake');
2515 setTimeout(() => {
2516 emailInput.classList.remove('email-input-shake');
2517 }, 500);
2518 }
2519
2520 if (nameInput) {
2521 nameInput.classList.add('email-input-shake');
2522 setTimeout(() => {
2523 nameInput.classList.remove('email-input-shake');
2524 }, 500);
2525 }
2526 }
2527
2528 function clearEmailError() {
2529 const existingErrors = emailForm.querySelectorAll('.email-error');
2530 existingErrors.forEach(error => error.remove());
2531 }
2532
2533 // MAIN FORM SUBMIT HANDLER - This is the critical fix
2534 //console.log('Setting up form submit handler...');
2535
2536 // Remove any existing event listeners first
2537 emailForm.removeEventListener('submit', handleFormSubmit);
2538
2539 // Add the form submit handler
2540 emailForm.addEventListener('submit', handleFormSubmit);
2541
2542 function handleFormSubmit(event) {
2543 //console.log('Form submit handler triggered!');
2544 event.preventDefault();
2545 event.stopPropagation();
2546
2547 // Prevent double submission
2548 if (isSubmitting) {
2549 //console.log('Already submitting, ignoring...');
2550 return false;
2551 }
2552
2553 const userEmail = document.getElementById('user-email').value.trim();
2554 const nameInput = document.getElementById('user-name');
2555 const userName = nameInput ? nameInput.value.trim() : '';
2556 const sessionId = getChatSession();
2557
2558 //console.log('Form data:', { userEmail, userName, sessionId });
2559
2560 // Validate email before submission
2561 if (!userEmail) {
2562 showEmailError('Please enter your email address.');
2563 return false;
2564 }
2565
2566 if (!isValidEmail(userEmail)) {
2567 showEmailError('Please enter a valid email address.');
2568 return false;
2569 }
2570
2571 // Validate name if field exists
2572 if (nameInput && !isValidName(userName)) {
2573 showEmailError('Please enter a valid name (2-100 characters).');
2574 return false;
2575 }
2576
2577 // Clear any existing errors
2578 clearEmailError();
2579 setSubmissionState(true);
2580
2581 //console.log('Sending AJAX request...');
2582
2583 // Prepare form data with optional name
2584 const formData = new URLSearchParams({
2585 action: 'mxchat_handle_save_email_and_response',
2586 email: userEmail,
2587 session_id: sessionId,
2588 nonce: mxchatChat.nonce,
2589 });
2590
2591 // Add name to form data if provided
2592 if (userName) {
2593 formData.append('name', userName);
2594 }
2595
2596 fetch(mxchatChat.ajax_url, {
2597 method: 'POST',
2598 headers: {
2599 'Content-Type': 'application/x-www-form-urlencoded',
2600 },
2601 body: formData
2602 })
2603 .then((response) => {
2604 //console.log('Response received:', response);
2605 if (!response.ok) {
2606 throw new Error(`HTTP error! status: ${response.status}`);
2607 }
2608 return response.json();
2609 })
2610 .then((data) => {
2611 //console.log('Response data:', data);
2612 setSubmissionState(false);
2613
2614 if (data.success) {
2615 // Show chat immediately
2616 showChatContainer();
2617
2618 // Handle bot response if provided
2619 if (data.message && typeof appendMessage === 'function') {
2620 setTimeout(() => {
2621 appendMessage('bot', data.message);
2622 if (typeof scrollToBottom === 'function') {
2623 scrollToBottom();
2624 }
2625 }, 100);
2626 }
2627 } else {
2628 showEmailError(data.message || 'Failed to save email. Please try again.');
2629 }
2630 })
2631 .catch((error) => {
2632 //console.error('Email submission error:', error);
2633 setSubmissionState(false);
2634 showEmailError('An error occurred. Please try again.');
2635 });
2636
2637 return false; // Extra prevention
2638 }
2639
2640 // Real-time email validation
2641 const emailInput = document.getElementById('user-email');
2642 if (emailInput) {
2643 let validationTimeout;
2644
2645 emailInput.addEventListener('input', function() {
2646 // Clear previous validation timeout
2647 if (validationTimeout) {
2648 clearTimeout(validationTimeout);
2649 }
2650
2651 // Debounce validation
2652 validationTimeout = setTimeout(() => {
2653 const email = this.value.trim();
2654 clearEmailError();
2655
2656 if (email && !isValidEmail(email)) {
2657 showEmailError('Please enter a valid email address.');
2658 }
2659 }, 500);
2660 });
2661
2662 // Handle Enter key
2663 emailInput.addEventListener('keypress', function(e) {
2664 if (e.key === 'Enter' && !isSubmitting) {
2665 e.preventDefault();
2666 emailForm.dispatchEvent(new Event('submit'));
2667 }
2668 });
2669 }
2670
2671 // Real-time name validation
2672 const nameInput = document.getElementById('user-name');
2673 if (nameInput) {
2674 let nameValidationTimeout;
2675
2676 nameInput.addEventListener('input', function() {
2677 // Clear previous validation timeout
2678 if (nameValidationTimeout) {
2679 clearTimeout(nameValidationTimeout);
2680 }
2681
2682 // Debounce validation
2683 nameValidationTimeout = setTimeout(() => {
2684 const name = this.value.trim();
2685 clearEmailError();
2686
2687 if (name && !isValidName(name)) {
2688 showEmailError('Name must be between 2 and 100 characters.');
2689 }
2690 }, 500);
2691 });
2692
2693 // Handle Enter key
2694 nameInput.addEventListener('keypress', function(e) {
2695 if (e.key === 'Enter' && !isSubmitting) {
2696 e.preventDefault();
2697 emailForm.dispatchEvent(new Event('submit'));
2698 }
2699 });
2700 }
2701
2702 // Initial state check
2703 if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
2704 //console.log('Using server-provided email state:', mxchatChat.initial_email_state);
2705
2706 const emailState = mxchatChat.initial_email_state;
2707 if (emailState.show_email_form) {
2708 showEmailForm();
2709 } else {
2710 showChatContainer();
2711 }
2712 } else {
2713 // Check email status via AJAX
2714 setTimeout(checkSessionAndEmail, 100);
2715 }
2716
2717 // Check if email exists for the current session
2718 function checkSessionAndEmail() {
2719 const sessionId = getChatSession();
2720
2721 fetch(mxchatChat.ajax_url, {
2722 method: 'POST',
2723 headers: {
2724 'Content-Type': 'application/x-www-form-urlencoded',
2725 },
2726 body: new URLSearchParams({
2727 action: 'mxchat_check_email_provided',
2728 session_id: sessionId,
2729 nonce: mxchatChat.nonce,
2730 })
2731 })
2732 .then((response) => {
2733 if (!response.ok) {
2734 throw new Error(`HTTP error! status: ${response.status}`);
2735 }
2736 return response.json();
2737 })
2738 .then((data) => {
2739 if (data.success) {
2740 if (data.data.logged_in || data.data.email) {
2741 showChatContainer();
2742 } else {
2743 showEmailForm();
2744 }
2745 } else {
2746 // On error, default to showing email form
2747 showEmailForm();
2748 }
2749 })
2750 .catch((error) => {
2751 //console.warn('Email check failed, defaulting to email form:', error);
2752 showEmailForm();
2753 });
2754 }
2755
2756 } else {
2757 console.error('Email collection is enabled but essential elements are missing:', {
2758 emailForm: !!emailForm,
2759 emailBlocker: !!emailBlocker,
2760 chatbotWrapper: !!chatbotWrapper
2761 });
2762 }
2763 } else {
2764 //console.log('Email collection is disabled');
2765 }
2766
2767 // Open chatbot when pre-chat message is clicked
2768 $(document).on('click', '#pre-chat-message', function() {
2769 var chatbot = $('#floating-chatbot');
2770 if (chatbot.hasClass('hidden')) {
2771 chatbot.removeClass('hidden').addClass('visible');
2772 $('#floating-chatbot-button').addClass('hidden');
2773 $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
2774 disableScroll(); // Disable scroll when chatbot opens
2775 }
2776 });
2777
2778 var closeButton = document.querySelector('.close-pre-chat-message');
2779 if (closeButton) {
2780 closeButton.addEventListener('click', function() {
2781 $('#pre-chat-message').fadeOut(200); // Hide the message
2782
2783 // Send an AJAX request to set the transient flag for 24 hours
2784 $.ajax({
2785 url: mxchatChat.ajax_url,
2786 type: 'POST',
2787 data: {
2788 action: 'mxchat_dismiss_pre_chat_message',
2789 _ajax_nonce: mxchatChat.nonce
2790 },
2791 success: function() {
2792 //console.log('Pre-chat message dismissed for 24 hours.');
2793
2794 // Ensure the message is hidden after dismissal
2795 $('#pre-chat-message').hide();
2796 },
2797 error: function() {
2798 //console.error('Failed to dismiss pre-chat message.');
2799 }
2800 });
2801 });
2802 }
2803
2804
2805 function hasQuickQuestions() {
2806 const questionButtons = document.querySelectorAll('#mxchat-popular-questions .mxchat-popular-question');
2807 return questionButtons.length > 0;
2808 }
2809
2810 function collapseQuickQuestions() {
2811 const questionsContainer = document.getElementById('mxchat-popular-questions');
2812 if (questionsContainer && hasQuickQuestions()) {
2813 questionsContainer.classList.add('collapsed');
2814 questionsContainer.classList.add('has-been-collapsed');
2815 try {
2816 sessionStorage.setItem('mxchat_questions_collapsed', 'true');
2817 sessionStorage.setItem('mxchat_questions_has_been_collapsed', 'true');
2818 } catch (e) {
2819 // Ignore if sessionStorage is not available
2820 }
2821 }
2822 }
2823
2824 function expandQuickQuestions() {
2825 const questionsContainer = document.getElementById('mxchat-popular-questions');
2826 if (questionsContainer && hasQuickQuestions()) {
2827 questionsContainer.classList.remove('collapsed');
2828 try {
2829 sessionStorage.setItem('mxchat_questions_collapsed', 'false');
2830 } catch (e) {
2831 // Ignore if sessionStorage is not available
2832 }
2833 }
2834 }
2835
2836 function checkQuickQuestionsState() {
2837 if (!hasQuickQuestions()) {
2838 return; // Don't do anything if no questions exist
2839 }
2840
2841 try {
2842 const isCollapsed = sessionStorage.getItem('mxchat_questions_collapsed');
2843 const hasBeenCollapsed = sessionStorage.getItem('mxchat_questions_has_been_collapsed');
2844
2845 const questionsContainer = document.getElementById('mxchat-popular-questions');
2846 if (questionsContainer) {
2847 if (hasBeenCollapsed === 'true') {
2848 questionsContainer.classList.add('has-been-collapsed');
2849 }
2850 if (isCollapsed === 'true') {
2851 questionsContainer.classList.add('collapsed');
2852 }
2853 }
2854 } catch (e) {
2855 // Ignore if sessionStorage is not available
2856 }
2857 }
2858
2859 // Global delegation for dynamically added links as fallback
2860 $(document).on('click', '#chat-box a[href]:not([data-tracked])', function(e) {
2861 const $link = $(this);
2862 const messageDiv = $link.closest('.bot-message, .agent-message');
2863
2864 // Only process bot/agent message links
2865 if (messageDiv.length > 0) {
2866 const originalHref = $link.attr('href');
2867
2868 if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
2869 e.preventDefault();
2870 e.stopPropagation();
2871
2872 // Mark as tracked
2873 $link.attr('data-tracked', 'true');
2874
2875 // Get message context from the message div
2876 const messageText = messageDiv.text().substring(0, 200);
2877
2878 $.ajax({
2879 url: mxchatChat.ajax_url,
2880 type: 'POST',
2881 data: {
2882 action: 'mxchat_track_url_click',
2883 session_id: getChatSession(),
2884 url: originalHref,
2885 message_context: messageText,
2886 nonce: mxchatChat.nonce
2887 },
2888 complete: function() {
2889 if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
2890 window.open(originalHref, '_blank');
2891 } else {
2892 window.location.href = originalHref;
2893 }
2894 }
2895 });
2896
2897 return false;
2898 }
2899 }
2900 });
2901
2902
2903
2904
2905 // ====================================
2906 // MAIN INITIALIZATION
2907 // ====================================
2908
2909 if ($('#floating-chatbot').hasClass('hidden')) {
2910 $('#floating-chatbot-button').removeClass('hidden');
2911 }
2912 // Initialize when document is ready
2913 setFullHeight();
2914 trackOriginatingPage();
2915
2916 // Only load chat history if email collection is disabled
2917 if (mxchatChat.email_collection_enabled !== 'on') {
2918 loadChatHistory();
2919 }
2920
2921 initializeChatVisibility();
2922
2923 // Make functions globally available for add-ons
2924 window.hasQuickQuestions = hasQuickQuestions;
2925 window.collapseQuickQuestions = collapseQuickQuestions;
2926 window.appendMessage = appendMessage;
2927 window.appendThinkingMessage = appendThinkingMessage;
2928 window.scrollToBottom = scrollToBottom;
2929 window.scrollElementToTop = scrollElementToTop;
2930 window.replaceLastMessage = replaceLastMessage;
2931 window.callMxChat = callMxChat;
2932 window.callMxChatStream = callMxChatStream;
2933 window.shouldUseStreaming = shouldUseStreaming;
2934 window.getChatSession = getChatSession;
2935 window.getPageContext = getPageContext;
2936 window.updateStreamingMessage = updateStreamingMessage;
2937
2938 }); // End of jQuery ready
2939
2940
2941 // ====================================
2942 // GLOBAL EVENT LISTENERS (Outside jQuery)
2943 // ====================================
2944
2945 // Event listener for copy button (code blocks)
2946 document.addEventListener("click", (e) => {
2947 if (e.target.classList.contains("mxchat-copy-button")) {
2948 const copyButton = e.target;
2949 const codeBlock = copyButton
2950 .closest(".mxchat-code-block-container")
2951 .querySelector(".mxchat-code-block code");
2952
2953 if (codeBlock) {
2954 // Preserve formatting using innerText
2955 navigator.clipboard.writeText(codeBlock.innerText).then(() => {
2956 copyButton.textContent = "Copied!";
2957 copyButton.setAttribute("aria-label", "Copied to clipboard");
2958
2959 setTimeout(() => {
2960 copyButton.textContent = "Copy";
2961 copyButton.setAttribute("aria-label", "Copy to clipboard");
2962 }, 2000);
2963 });
2964 }
2965 }
2966 });
2967
2968