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

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