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

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

1,815 lines 69.4 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
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 // ====================================
62 // CORE CHAT FUNCTIONALITY
63 // ====================================
64
65 // Update your existing sendMessage function
66 function sendMessage() {
67 var message = $('#chat-input').val();
68 if (message) {
69 appendMessage("user", message);
70 $('#chat-input').val('');
71 $('#chat-input').css('height', 'auto');
72
73 $('#mxchat-popular-questions').hide();
74 appendThinkingMessage();
75 scrollToBottom();
76
77 const currentModel = mxchatChat.model || 'gpt-4o';
78
79 // Check if streaming is enabled AND supported for this model
80 if (shouldUseStreaming(currentModel)) {
81 callMxChatStream(message, function(response) {
82 $('.bot-message.temporary-message').removeClass('temporary-message');
83 });
84 } else {
85 callMxChat(message, function(response) {
86 replaceLastMessage("bot", response);
87 });
88 }
89 }
90 }
91
92 // Update your existing sendMessageToChatbot function
93 function sendMessageToChatbot(message) {
94 var sessionId = getChatSession();
95
96 $('#mxchat-popular-questions').hide();
97 appendThinkingMessage();
98 scrollToBottom();
99
100 const currentModel = mxchatChat.model || 'gpt-4o';
101
102 // Check if streaming is enabled AND supported for this model
103 if (shouldUseStreaming(currentModel)) {
104 callMxChatStream(message, function(response) {
105 $('.bot-message.temporary-message').removeClass('temporary-message');
106 });
107 } else {
108 callMxChat(message, function(response) {
109 $('.temporary-message').remove();
110 replaceLastMessage("bot", response);
111 });
112 }
113 }
114
115
116 // Updated shouldUseStreaming function with debugging
117 function shouldUseStreaming(model) {
118 // Check if streaming is enabled in settings (using your toggle naming pattern)
119 const streamingEnabled = mxchatChat.enable_streaming_toggle === 'on';
120
121 // Check if model supports streaming
122 const streamingSupported = isStreamingSupported(model);
123
124
125 // Only use streaming if both enabled and supported
126 return streamingEnabled && streamingSupported;
127 }
128
129 function callMxChat(message, callback) {
130 $.ajax({
131 url: mxchatChat.ajax_url,
132 type: 'POST',
133 dataType: 'json',
134 data: {
135 action: 'mxchat_handle_chat_request',
136 message: message,
137 session_id: getChatSession(),
138 nonce: mxchatChat.nonce
139 },
140 success: function(response) {
141 // Log the full response for debugging
142 //console.log("API Response:", response);
143
144 // First check if this is a successful response by looking for text, html, or message fields
145 // This preserves compatibility with your server response format
146 if (response.text !== undefined || response.html !== undefined || response.message !== undefined ||
147 (response.success === true && response.data && response.data.status === 'waiting_for_agent')) {
148
149 // Handle successful response - this is your original success handling code
150
151 // Existing chat mode check
152 if (response.chat_mode) {
153 updateChatModeIndicator(response.chat_mode);
154 }
155 else if (response.fallbackResponse && response.fallbackResponse.chat_mode) {
156 updateChatModeIndicator(response.fallbackResponse.chat_mode);
157 }
158
159 // Add PDF filename handling
160 if (response.data && response.data.filename) {
161 showActivePdf(response.data.filename);
162 activePdfFile = response.data.filename;
163 }
164
165 // Add redirect check here
166 if (response.redirect_url) {
167 let responseText = response.text || '';
168 if (responseText) {
169 replaceLastMessage("bot", responseText);
170 }
171 setTimeout(() => {
172 window.location.href = response.redirect_url;
173 }, 1500);
174 return;
175 }
176
177 // Check for live agent response
178 if (response.success && response.data && response.data.status === 'waiting_for_agent') {
179 updateChatModeIndicator('agent');
180 return;
181 }
182
183 // Handle other responses
184 let responseText = response.text || '';
185 let responseHtml = response.html || '';
186 let responseMessage = response.message || '';
187
188 if (responseText === 'You are now chatting with the AI chatbot.') {
189 updateChatModeIndicator('ai');
190 }
191
192 // Handle the message and show notification if chat is hidden
193 if (responseText || responseHtml || responseMessage) {
194 // Update the messages as before
195 if (responseText && responseHtml) {
196 replaceLastMessage("bot", responseText, responseHtml);
197 } else if (responseText) {
198 replaceLastMessage("bot", responseText);
199 } else if (responseHtml) {
200 replaceLastMessage("bot", "", responseHtml);
201 } else if (responseMessage) {
202 replaceLastMessage("bot", responseMessage);
203 }
204
205 // Check if chat is hidden and show notification
206 if ($('#floating-chatbot').hasClass('hidden')) {
207 const badge = $('#chat-notification-badge');
208 if (badge.length) {
209 badge.show();
210 }
211 }
212 } else {
213 //console.error("Unexpected response format:", response);
214 replaceLastMessage("bot", "I received an empty response. Please try again or contact support if this persists.");
215 }
216
217 if (response.message_id) {
218 lastSeenMessageId = response.message_id;
219 }
220
221 return;
222 }
223
224 // If we got here, it's likely an error response
225 // Now we can check for error conditions with our robust error handling
226
227 let errorMessage = "";
228 let errorCode = "";
229
230 // Check various possible error locations in the response
231 if (response.data && response.data.error_message) {
232 errorMessage = response.data.error_message;
233 errorCode = response.data.error_code || "";
234 } else if (response.error_message) {
235 errorMessage = response.error_message;
236 errorCode = response.error_code || "";
237 } else if (response.message) {
238 errorMessage = response.message;
239 } else if (typeof response.data === 'string') {
240 errorMessage = response.data;
241 } else if (!response.success) {
242 // Explicit check for success: false without other error info
243 errorMessage = "An error occurred. Please try again or contact support.";
244 } else {
245 // Fallback for any other unexpected response format
246 errorMessage = "Unexpected response received. Please try again or contact support.";
247 }
248
249 // Log the error with code for debugging
250 //console.log("Response data:", response.data);
251 //console.error("API Error:", errorMessage, "Code:", errorCode);
252
253 // Format user-friendly error message
254 let displayMessage = errorMessage;
255
256 // Customize message for admin users
257 if (mxchatChat.is_admin) {
258 // For admin users, show more technical details including error code
259 displayMessage = errorMessage + (errorCode ? " (Error code: " + errorCode + ")" : "");
260 }
261
262 replaceLastMessage("bot", displayMessage);
263 },
264 error: function(xhr, status, error) {
265 console.error("AJAX Error:", status, error);
266 //console.log("Response Text:", xhr.responseText);
267
268 let errorMessage = "An unexpected error occurred.";
269
270 // Try to parse the response if it's JSON
271 try {
272 const responseJson = JSON.parse(xhr.responseText);
273 //console.log("Parsed error response:", responseJson);
274
275 if (responseJson.data && responseJson.data.error_message) {
276 errorMessage = responseJson.data.error_message;
277 } else if (responseJson.message) {
278 errorMessage = responseJson.message;
279 }
280 } catch (e) {
281 // Not JSON or parsing failed, use HTTP status based messages
282 if (xhr.status === 0) {
283 errorMessage = "Network error: Please check your internet connection.";
284 } else if (xhr.status === 403) {
285 errorMessage = "Access denied: Your session may have expired. Please refresh the page.";
286 } else if (xhr.status === 404) {
287 errorMessage = "API endpoint not found. Please contact support.";
288 } else if (xhr.status === 429) {
289 errorMessage = "Too many requests. Please try again in a moment.";
290 } else if (xhr.status >= 500) {
291 errorMessage = "Server error: The server encountered an issue. Please try again later.";
292 }
293 }
294
295 replaceLastMessage("bot", errorMessage);
296 }
297 });
298 }
299
300 function callMxChatStream(message, callback) {
301 //console.log("Using streaming for message:", message);
302
303 const currentModel = mxchatChat.model || 'gpt-4o';
304 if (!isStreamingSupported(currentModel)) {
305 //console.log("Streaming not supported, falling back to regular call");
306 callMxChat(message, callback);
307 return;
308 }
309
310 const formData = new FormData();
311 formData.append('action', 'mxchat_stream_chat');
312 formData.append('message', message);
313 formData.append('session_id', getChatSession());
314 formData.append('nonce', mxchatChat.nonce);
315
316 let accumulatedContent = '';
317
318 fetch(mxchatChat.ajax_url, {
319 method: 'POST',
320 body: formData,
321 credentials: 'same-origin'
322 })
323 .then(response => {
324 //console.log("Streaming response received:", response);
325
326 if (!response.ok) {
327 throw new Error('Network response was not ok');
328 }
329
330 // Check if response is JSON instead of streaming
331 const contentType = response.headers.get('content-type');
332 if (contentType && contentType.includes('application/json')) {
333 //console.log("Received JSON response instead of stream, handling as regular response");
334 return response.json().then(data => {
335 // Handle as regular JSON response - this is the key fix!
336 // Remove thinking dots first
337 $('.bot-message.temporary-message').remove();
338
339 // Handle different response formats (including intent responses)
340 if (data.text || data.html || data.message) {
341 if (data.text && data.html) {
342 replaceLastMessage("bot", data.text, data.html);
343 } else if (data.text) {
344 replaceLastMessage("bot", data.text);
345 } else if (data.html) {
346 replaceLastMessage("bot", "", data.html);
347 } else if (data.message) {
348 // This handles intent responses that use 'message' field
349 replaceLastMessage("bot", data.message);
350 }
351 }
352
353 // Handle other response properties
354 if (data.chat_mode) {
355 updateChatModeIndicator(data.chat_mode);
356 }
357
358 if (data.data && data.data.filename) {
359 showActivePdf(data.data.filename);
360 activePdfFile = data.data.filename;
361 }
362
363 if (callback) {
364 callback(data.text || data.message || '');
365 }
366 });
367 }
368
369 // Continue with streaming processing
370 const reader = response.body.getReader();
371 const decoder = new TextDecoder();
372 let buffer = '';
373
374 function processStream() {
375 reader.read().then(({ done, value }) => {
376 if (done) {
377 //console.log("Streaming completed, final content:", accumulatedContent);
378 if (callback) {
379 callback(accumulatedContent);
380 }
381 return;
382 }
383
384 buffer += decoder.decode(value, { stream: true });
385 const lines = buffer.split('\n');
386 buffer = lines.pop() || '';
387
388 for (const line of lines) {
389 if (line.startsWith('data: ')) {
390 const data = line.substring(6);
391
392 if (data === '[DONE]') {
393 //console.log("Received [DONE] signal");
394 if (callback) {
395 callback(accumulatedContent);
396 }
397 return;
398 }
399
400 try {
401 const json = JSON.parse(data);
402 if (json.content) {
403 accumulatedContent += json.content;
404 updateStreamingMessage(accumulatedContent);
405 } else if (json.error) {
406 console.error("Streaming error:", json.error);
407 replaceLastMessage("bot", "Error: " + json.error);
408 return;
409 }
410 } catch (e) {
411 console.error('Error parsing SSE data:', e);
412 }
413 }
414 }
415
416 processStream();
417 });
418 }
419
420 processStream();
421 })
422 .catch(error => {
423 console.error('Streaming error:', error);
424 callMxChat(message, callback);
425 });
426 }
427
428 // Function to update message during streaming
429 function updateStreamingMessage(content) {
430 const formattedContent = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(content))));
431
432 // Find the temporary message
433 const tempMessage = $('.bot-message.temporary-message').last();
434
435 if (tempMessage.length) {
436 // Update existing message
437 tempMessage.html(formattedContent);
438 } else {
439 // Create new temporary message if it doesn't exist
440 appendMessage("bot", content, '', [], true);
441 }
442
443 }
444
445 // Function to check if streaming is supported for the current model
446 function isStreamingSupported(model) {
447 if (!model) return false;
448
449 //console.log("Checking streaming support for model:", model); // Debug log
450
451 // Get the model prefix
452 const modelPrefix = model.split('-')[0].toLowerCase();
453
454 //console.log("Model prefix:", modelPrefix); // Debug log
455
456 // Support streaming for OpenAI and Claude models
457 const isSupported = modelPrefix === 'gpt' || modelPrefix === 'o1' || modelPrefix === 'claude';
458
459 //console.log("Streaming supported:", isSupported); // Debug log
460
461 return isSupported;
462 }
463
464 // Update the event handlers to use the correct function names
465 $('#send-button').off('click').on('click', function() {
466 sendMessage(); // Use the updated sendMessage function
467 });
468
469 // Override enter key handler
470 $('#chat-input').off('keypress').on('keypress', function(e) {
471 if (e.which == 13 && !e.shiftKey) {
472 e.preventDefault();
473 sendMessage(); // Use the updated sendMessage function
474 }
475 });
476
477
478 function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) {
479 try {
480 // Determine styles based on sender type
481 let messageClass, bgColor, fontColor;
482
483 if (sender === "user") {
484 messageClass = "user-message";
485 bgColor = userMessageBgColor;
486 fontColor = userMessageFontColor;
487 // Only sanitize user input
488 messageText = sanitizeUserInput(messageText);
489 } else if (sender === "agent") {
490 messageClass = "agent-message";
491 bgColor = liveAgentMessageBgColor;
492 fontColor = liveAgentMessageFontColor;
493 } else {
494 messageClass = "bot-message";
495 bgColor = botMessageBgColor;
496 fontColor = botMessageFontColor;
497 }
498
499 const messageDiv = $('<div>')
500 .addClass(messageClass)
501 .attr('dir', 'auto') // Add dir="auto" for automatic text direction
502 .css({
503 'background': bgColor,
504 'color': fontColor,
505 'margin-bottom': '1em'
506 });
507
508 // Process the message content based on sender
509 let fullMessage;
510 if (sender === "user") {
511 // For user messages, apply linkify after sanitization
512 fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(messageText))));
513 } else {
514 // For bot/agent messages, preserve HTML
515 fullMessage = messageText;
516 }
517
518 // Add images if provided
519 if (images && images.length > 0) {
520 fullMessage += '<div class="image-gallery" dir="auto">'; // Add dir="auto" to image gallery
521 images.forEach(img => {
522 // Ensure image URLs and titles are properly escaped
523 const safeTitle = sanitizeUserInput(img.title);
524 const safeUrl = encodeURI(img.image_url);
525 const safeThumbnail = encodeURI(img.thumbnail_url);
526
527 fullMessage += `
528 <div style="margin-bottom: 10px;">
529 <strong>${safeTitle}</strong><br>
530 <a href="${safeUrl}" target="_blank">
531 <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" />
532 </a>
533 </div>`;
534 });
535 fullMessage += '</div>';
536 }
537
538 // Append HTML content if provided
539 if (messageHtml && sender !== "user") {
540 fullMessage += '<br><br>' + messageHtml;
541 }
542
543 messageDiv.html(fullMessage);
544
545 if (isTemporary) {
546 messageDiv.addClass('temporary-message');
547 }
548
549 messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() {
550 if (sender === "bot") {
551 const lastUserMessage = $('#chat-box').find('.user-message').last();
552 if (lastUserMessage.length) {
553 scrollElementToTop(lastUserMessage);
554 }
555 }
556 });
557
558 if (messageText.id) {
559 lastSeenMessageId = messageText.id;
560 hideNotification();
561 }
562 } catch (error) {
563 console.error("Error rendering message:", error);
564 }
565 }
566
567 function replaceLastMessage(sender, responseText, responseHtml = '', images = []) {
568 var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
569 var lastMessageDiv = $('#chat-box').find('.bot-message.temporary-message, .agent-message.temporary-message').last();
570
571 // Determine styles
572 let bgColor, fontColor;
573 if (sender === "user") {
574 bgColor = userMessageBgColor;
575 fontColor = userMessageFontColor;
576 } else if (sender === "agent") {
577 bgColor = liveAgentMessageBgColor;
578 fontColor = liveAgentMessageFontColor;
579 } else {
580 bgColor = botMessageBgColor;
581 fontColor = botMessageFontColor;
582 }
583
584 var fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(responseText))));
585 if (responseHtml) {
586 fullMessage += '<br><br>' + responseHtml;
587 }
588
589 if (images.length > 0) {
590 fullMessage += '<div class="image-gallery" dir="auto">'; // Add dir="auto" to image gallery
591 images.forEach(img => {
592 fullMessage += `
593 <div style="margin-bottom: 10px;">
594 <strong>${img.title}</strong><br>
595 <a href="${img.image_url}" target="_blank">
596 <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" />
597 </a>
598 </div>`;
599 });
600 fullMessage += '</div>';
601 }
602
603 if (lastMessageDiv.length) {
604 lastMessageDiv.fadeOut(200, function() {
605 $(this)
606 .html(fullMessage)
607 .removeClass('bot-message user-message')
608 .addClass(messageClass)
609 .attr('dir', 'auto') // Add dir="auto" for automatic text direction
610 .css({
611 'background-color': bgColor,
612 'color': fontColor,
613 })
614 .removeClass('temporary-message')
615 .fadeIn(200, function() {
616 if (sender === "bot" || sender === "agent") {
617 const lastUserMessage = $('#chat-box').find('.user-message').last();
618 if (lastUserMessage.length) {
619 scrollElementToTop(lastUserMessage);
620 }
621 // Show notification if chat is hidden
622 if ($('#floating-chatbot').hasClass('hidden')) {
623 showNotification();
624 }
625 }
626 });
627 });
628 } else {
629 appendMessage(sender, responseText, responseHtml, images);
630 }
631 }
632
633 function appendThinkingMessage() {
634 // Remove any existing thinking dots first
635 $('.thinking-dots').remove();
636
637 // Retrieve the bot message font color and background color
638 var botMessageFontColor = mxchatChat.bot_message_font_color;
639 var botMessageBgColor = mxchatChat.bot_message_bg_color;
640
641
642 var thinkingHtml = '<div class="thinking-dots-container">' +
643 '<div class="thinking-dots">' +
644 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
645 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
646 '<span class="dot" style="background-color: ' + botMessageFontColor + ';"></span>' +
647 '</div>' +
648 '</div>';
649
650 // Append the thinking dots to the chat container (or within the temporary message div)
651 $("#chat-box").append('<div class="bot-message temporary-message" style="background-color: ' + botMessageBgColor + ';">' + thinkingHtml + '</div>');
652 scrollToBottom();
653 }
654
655 function removeThinkingDots() {
656 $('.thinking-dots').closest('.temporary-message').remove();
657 }
658
659
660 // ====================================
661 // TEXT FORMATTING & PROCESSING
662 // ====================================
663
664 function linkify(inputText) {
665 if (!inputText) return '';
666
667 // Process markdown headers
668 let processedText = formatMarkdownHeaders(inputText);
669
670 // Process markdown links
671 const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s]+)\)/g;
672 processedText = processedText.replace(markdownLinkPattern, (match, text, url) => {
673 const safeUrl = encodeURI(url);
674 const safeText = sanitizeUserInput(text);
675 return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`;
676 });
677
678 // Process phone numbers (tel:)
679 const phonePattern = /\[([^\]]+)\]\((tel:[\d+]+)\)/g;
680 processedText = processedText.replace(phonePattern, (match, text, phone) => {
681 const safePhone = encodeURI(phone);
682 const safeText = sanitizeUserInput(text);
683 return `<a href="${safePhone}">${safeText}</a>`;
684 });
685
686 // Process standalone URLs
687 const urlPattern = /(^|[^">])(https?:\/\/[^\s<]+)/gim;
688 processedText = processedText.replace(urlPattern, (match, prefix, url) => {
689 const safeUrl = encodeURI(url);
690 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
691 });
692
693 // Process www. URLs
694 const wwwPattern = /(^|[^">])(www\.[\S]+(\b|$))(?![^<]*<\/a>)/gim;
695 processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
696 const safeUrl = encodeURI(`http://${url}`);
697 return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`;
698 });
699
700 return processedText;
701 }
702
703 function formatMarkdownHeaders(text) {
704 // Handle h1 to h6 headers
705 return text.replace(/^(#{1,6})\s(.+)$/gm, function(match, hashes, content) {
706 const level = hashes.length;
707 return `<h${level} class="chat-heading">${content}</h${level}>`;
708 });
709 }
710
711 function formatBoldText(text) {
712 return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
713 }
714
715 function convertNewlinesToBreaks(text) {
716 // Split the text into paragraphs (marked by double newlines or multiple <br> tags)
717 const paragraphs = text.split(/(?:\n\n|\<br\>\s*\<br\>)/g);
718
719 // Wrap each paragraph in <p> tags
720 return paragraphs
721 .map(para => `<p>${para.trim()}</p>`)
722 .join('');
723 }
724
725 function formatCodeBlocks(text) {
726 // First handle raw PHP tags
727 text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
728 return `<pre><code class="language-php">${escapeHtml(match)}</code></pre>`;
729 });
730
731 // Then handle code blocks with backticks
732 text = text.replace(/```php5?\n([\s\S]+?)```/gi, (match, code) => {
733 return `<pre><code class="language-php">${escapeHtml(code)}</code></pre>`;
734 });
735
736 return text;
737 }
738
739 function sanitizeUserInput(text) {
740 const div = document.createElement('div');
741 div.textContent = text;
742 return div.innerHTML;
743 }
744
745
746 function escapeHtml(unsafe) {
747 // First check if it's already a code block
748 if (unsafe.includes('<pre><code') || unsafe.includes('</code></pre>')) {
749 return unsafe;
750 }
751
752 return unsafe
753 .replace(/&/g, "&amp;")
754 .replace(/</g, "&lt;")
755 .replace(/>/g, "&gt;")
756 .replace(/"/g, "&quot;")
757 .replace(/'/g, "&#039;");
758 }
759
760 function decodeHTMLEntities(text) {
761 var textArea = document.createElement('textarea');
762 textArea.innerHTML = text;
763 return textArea.value;
764 }
765
766
767 // ====================================
768 // UI & SCROLLING CONTROLS
769 // ====================================
770
771 function scrollToBottom(instant = false) {
772 var chatBox = $('#chat-box');
773 if (instant) {
774 // Instantly set the scroll position to the bottom
775 chatBox.scrollTop(chatBox.prop("scrollHeight"));
776 } else {
777 // Use requestAnimationFrame for smoother scrolling if needed
778 let start = null;
779 const scrollHeight = chatBox.prop("scrollHeight");
780 const initialScroll = chatBox.scrollTop();
781 const distance = scrollHeight - initialScroll;
782 const duration = 500; // Duration in ms
783
784 function smoothScroll(timestamp) {
785 if (!start) start = timestamp;
786 const progress = timestamp - start;
787 const currentScroll = initialScroll + (distance * (progress / duration));
788 chatBox.scrollTop(currentScroll);
789
790 if (progress < duration) {
791 requestAnimationFrame(smoothScroll);
792 } else {
793 chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
794 }
795 }
796
797 requestAnimationFrame(smoothScroll);
798 }
799 }
800
801 function scrollElementToTop(element) {
802 var chatBox = $('#chat-box');
803 var elementTop = element.position().top + chatBox.scrollTop();
804 chatBox.animate({ scrollTop: elementTop }, 500);
805 }
806
807 function showChatWidget() {
808 // First ensure display is set
809 $('#floating-chatbot-button').css('display', 'flex');
810 // Then handle the fade
811 $('#floating-chatbot-button').fadeTo(500, 1);
812 // Force visibility
813 $('#floating-chatbot-button').removeClass('hidden');
814 //console.log('Showing widget');
815 }
816
817 function hideChatWidget() {
818 $('#floating-chatbot-button').css('display', 'none');
819 $('#floating-chatbot-button').addClass('hidden');
820 //console.log('Hiding widget');
821 }
822
823 function disableScroll() {
824 if (isMobile()) {
825 $('body').css('overflow', 'hidden');
826 }
827 }
828
829 function enableScroll() {
830 if (isMobile()) {
831 $('body').css('overflow', '');
832 }
833 }
834
835 function isMobile() {
836 // This can be a simple check, or more sophisticated detection of mobile devices
837 return window.innerWidth <= 768; // Example threshold for mobile devices
838 }
839
840 function setFullHeight() {
841 var vh = $(window).innerHeight() * 0.01;
842 $(':root').css('--vh', vh + 'px');
843 }
844
845
846 // ====================================
847 // NOTIFICATION SYSTEM
848 // ====================================
849
850 function createNotificationBadge() {
851 //console.log("Creating notification badge...");
852 const chatButton = document.getElementById('floating-chatbot-button');
853 //console.log("Chat button found:", !!chatButton);
854
855 if (!chatButton) return;
856
857 // Remove any existing badge first
858 const existingBadge = chatButton.querySelector('.chat-notification-badge');
859 if (existingBadge) {
860 //console.log("Removing existing badge");
861 existingBadge.remove();
862 }
863
864 notificationBadge = document.createElement('div');
865 notificationBadge.className = 'chat-notification-badge';
866 notificationBadge.style.cssText = `
867 display: none;
868 position: absolute;
869 top: -5px;
870 right: -5px;
871 background-color: red;
872 color: white;
873 border-radius: 50%;
874 padding: 4px 8px;
875 font-size: 12px;
876 font-weight: bold;
877 z-index: 10001;
878 `;
879 chatButton.style.position = 'relative';
880 chatButton.appendChild(notificationBadge);
881
882 }
883
884 function showNotification() {
885 const badge = document.getElementById('chat-notification-badge');
886 if (badge && $('#floating-chatbot').hasClass('hidden')) {
887 badge.style.display = 'block';
888 badge.textContent = '1';
889 }
890 }
891
892 function hideNotification() {
893 const badge = document.getElementById('chat-notification-badge');
894 if (badge) {
895 badge.style.display = 'none';
896 }
897 }
898
899 function startNotificationChecking() {
900 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
901 if (!chatPersistenceEnabled) return;
902
903 createNotificationBadge();
904 notificationCheckInterval = setInterval(checkForNewMessages, 30000); // Check every 30 seconds
905 }
906
907 function stopNotificationChecking() {
908 if (notificationCheckInterval) {
909 clearInterval(notificationCheckInterval);
910 }
911 }
912
913 function checkForNewMessages() {
914 const sessionId = getChatSession();
915 const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
916
917 if (!chatPersistenceEnabled) return;
918
919 $.ajax({
920 url: mxchatChat.ajax_url,
921 type: 'POST',
922 data: {
923 action: 'mxchat_check_new_messages',
924 session_id: sessionId,
925 last_seen_id: lastSeenMessageId,
926 nonce: mxchatChat.nonce
927 },
928 success: function(response) {
929 if (response.success && response.data.hasNewMessages) {
930 showNotification();
931 }
932 }
933 });
934 }
935
936
937 // ====================================
938 // LIVE AGENT FUNCTIONALITY
939 // ====================================
940
941 function updateChatModeIndicator(mode) {
942 const indicator = document.getElementById('chat-mode-indicator');
943 if (indicator) {
944 // For Live Agent, keep as is; for AI mode, use the customized text
945 if (mode === 'agent') {
946 indicator.textContent = 'Live Agent';
947 } else {
948 // Get the custom AI agent text from a data attribute we'll add to the element
949 const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent';
950 indicator.textContent = customAiText;
951 }
952 }
953 // Start or stop polling based on mode
954 if (mode === 'agent') {
955 startPolling();
956 } else {
957 stopPolling();
958 }
959 }
960
961 function startPolling() {
962 // Clear any existing interval first
963 stopPolling();
964 // Start new polling interval
965 pollingInterval = setInterval(checkForAgentMessages, 5000);
966 //console.log("Started agent message polling");
967 }
968
969 function stopPolling() {
970 if (pollingInterval) {
971 clearInterval(pollingInterval);
972 pollingInterval = null;
973 //console.log("Stopped agent message polling");
974 }
975 }
976
977 function checkForAgentMessages() {
978 const sessionId = getChatSession();
979 $.ajax({
980 url: mxchatChat.ajax_url,
981 type: 'POST',
982 dataType: 'json',
983 data: {
984 action: 'mxchat_fetch_new_messages',
985 session_id: sessionId,
986 last_seen_id: lastSeenMessageId,
987 persistence_enabled: 'true', // Add this too
988 nonce: mxchatChat.nonce
989 },
990 success: function (response) {
991 if (response.success && response.data?.new_messages) {
992 let hasNewMessage = false;
993
994 response.data.new_messages.forEach(function (message) {
995 if (message.role === "agent" && !processedMessageIds.has(message.id)) {
996 hasNewMessage = true;
997 // CHANGE THIS LINE:
998 appendMessage("agent", message.content); // Instead of replaceLastMessage
999 lastSeenMessageId = message.id;
1000 processedMessageIds.add(message.id);
1001 }
1002 });
1003
1004 if (hasNewMessage && $('#floating-chatbot').hasClass('hidden')) {
1005 showNotification();
1006 }
1007
1008 scrollToBottom(true);
1009 }
1010 },
1011 error: function (xhr, status, error) {
1012 console.error("Polling error:", xhr, status, error);
1013 }
1014 });
1015 }
1016
1017 // ====================================
1018 // CHAT HISTORY & PERSISTENCE
1019 // ====================================
1020
1021 function loadChatHistory() {
1022 var sessionId = getChatSession();
1023 var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
1024
1025 if (chatPersistenceEnabled && sessionId) {
1026 $.ajax({
1027 url: mxchatChat.ajax_url,
1028 type: 'POST',
1029 dataType: 'json',
1030 data: {
1031 action: 'mxchat_fetch_conversation_history',
1032 session_id: sessionId
1033 },
1034 success: function(response) {
1035 if (response.success && response.data && Array.isArray(response.data.conversation)) {
1036
1037
1038 var $chatBox = $('#chat-box');
1039 var $fragment = $(document.createDocumentFragment());
1040 let highestMessageId = lastSeenMessageId;
1041
1042 if (response.data.chat_mode) {
1043 updateChatModeIndicator(response.data.chat_mode);
1044 }
1045
1046 $.each(response.data.conversation, function(index, message) {
1047 // Skip agent messages if persistence is off
1048 if (!chatPersistenceEnabled && message.role === 'agent') {
1049 return;
1050 }
1051
1052 var messageClass, messageBgColor, messageFontColor;
1053
1054 switch (message.role) {
1055 case 'user':
1056 messageClass = 'user-message';
1057 messageBgColor = userMessageBgColor;
1058 messageFontColor = userMessageFontColor;
1059 break;
1060 case 'agent':
1061 messageClass = 'agent-message';
1062 messageBgColor = liveAgentMessageBgColor;
1063 messageFontColor = liveAgentMessageFontColor;
1064 break;
1065 default:
1066 messageClass = 'bot-message';
1067 messageBgColor = botMessageBgColor;
1068 messageFontColor = botMessageFontColor;
1069 break;
1070 }
1071
1072 var messageElement = $('<div>').addClass(messageClass)
1073 .css({
1074 'background': messageBgColor,
1075 'color': messageFontColor
1076 });
1077
1078 var content = message.content;
1079 content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
1080 content = decodeHTMLEntities(content);
1081
1082 if (content.includes("mxchat-product-card") || content.includes("mxchat-image-gallery")) {
1083 messageElement.html(content);
1084 } else {
1085 var formattedContent = linkify(
1086 formatBoldText(
1087 convertNewlinesToBreaks(formatCodeBlocks(content))
1088 )
1089 );
1090 messageElement.html(formattedContent);
1091 }
1092
1093 $fragment.append(messageElement);
1094
1095 // In loadChatHistory, change this part:
1096 if (message.id) {
1097 highestMessageId = Math.max(highestMessageId, message.id);
1098 processedMessageIds.add(message.id); // Add all message IDs to processed set
1099 }
1100 });
1101
1102 $chatBox.append($fragment);
1103 scrollToBottom(true);
1104
1105 if (response.data.conversation.length > 0) {
1106 $('#mxchat-popular-questions').hide();
1107 }
1108
1109 // Update lastSeenMessageId after history loads
1110 lastSeenMessageId = highestMessageId;
1111
1112 // Only update chat mode if persistence is enabled
1113 if (chatPersistenceEnabled && response.data.conversation.length > 0) {
1114 var lastMessage = response.data.conversation[response.data.conversation.length - 1];
1115 if (lastMessage.role === 'agent') {
1116 updateChatModeIndicator('agent');
1117 }
1118 }
1119 } else {
1120 console.warn("No conversation history found.");
1121 }
1122 },
1123 error: function(xhr, status, error) {
1124 console.error("Error loading chat history:", status, error);
1125 appendMessage("bot", "Unable to load chat history.");
1126 }
1127 });
1128 } else {
1129 console.warn("Chat persistence is disabled or no session ID found. Not loading history.");
1130 }
1131 }
1132
1133
1134 // ====================================
1135 // FILE UPLOAD FUNCTIONALITY
1136 // ====================================
1137
1138 function addSafeEventListener(elementId, eventType, handler) {
1139 const element = document.getElementById(elementId);
1140 if (element) {
1141 element.addEventListener(eventType, handler);
1142 }
1143 }
1144
1145 function showActivePdf(filename) {
1146 const container = document.getElementById('active-pdf-container');
1147 const nameElement = document.getElementById('active-pdf-name');
1148
1149 if (!container || !nameElement) {
1150 console.error('PDF container elements not found');
1151 return;
1152 }
1153
1154 nameElement.textContent = filename;
1155 container.style.display = 'flex';
1156 }
1157
1158 function showActiveWord(filename) {
1159 const container = document.getElementById('active-word-container');
1160 const nameElement = document.getElementById('active-word-name');
1161
1162 if (!container || !nameElement) {
1163 console.error('Word document container elements not found');
1164 return;
1165 }
1166
1167 nameElement.textContent = filename;
1168 container.style.display = 'flex';
1169 }
1170
1171 function removeActivePdf() {
1172 const container = document.getElementById('active-pdf-container');
1173 const nameElement = document.getElementById('active-pdf-name');
1174
1175 if (!container || !nameElement || !activePdfFile) return;
1176
1177 fetch(mxchatChat.ajax_url, {
1178 method: 'POST',
1179 headers: {
1180 'Content-Type': 'application/x-www-form-urlencoded',
1181 },
1182 body: new URLSearchParams({
1183 'action': 'mxchat_remove_pdf',
1184 'session_id': sessionId,
1185 'nonce': mxchatChat.nonce
1186 })
1187 })
1188 .then(response => response.json())
1189 .then(data => {
1190 if (data.success) {
1191 container.style.display = 'none';
1192 nameElement.textContent = '';
1193 activePdfFile = null;
1194 appendMessage('bot', 'PDF removed.');
1195 }
1196 })
1197 .catch(error => {
1198 console.error('Error removing PDF:', error);
1199 });
1200 }
1201
1202 function removeActiveWord() {
1203 const container = document.getElementById('active-word-container');
1204 const nameElement = document.getElementById('active-word-name');
1205
1206 if (!container || !nameElement || !activeWordFile) return;
1207
1208 fetch(mxchatChat.ajax_url, {
1209 method: 'POST',
1210 headers: {
1211 'Content-Type': 'application/x-www-form-urlencoded',
1212 },
1213 body: new URLSearchParams({
1214 'action': 'mxchat_remove_word',
1215 'session_id': sessionId,
1216 'nonce': mxchatChat.nonce
1217 })
1218 })
1219 .then(response => response.json())
1220 .then(data => {
1221 if (data.success) {
1222 container.style.display = 'none';
1223 nameElement.textContent = '';
1224 activeWordFile = null;
1225 appendMessage('bot', 'Word document removed.');
1226 }
1227 })
1228 .catch(error => {
1229 console.error('Error removing Word document:', error);
1230 });
1231 }
1232
1233 // ====================================
1234 // CONSENT & COMPLIANCE (GDPR)
1235 // ====================================
1236
1237 function initializeChatVisibility() {
1238 //console.log('Initializing chat visibility');
1239 const complianzEnabled = mxchatChat.complianz_toggle === 'on' ||
1240 mxchatChat.complianz_toggle === '1' ||
1241 mxchatChat.complianz_toggle === 1;
1242
1243 if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
1244 // Initial check
1245 checkConsentAndShowChat();
1246
1247 // Listen for consent changes
1248 $(document).on('cmplz_status_change', function(event) {
1249 //console.log('Status change detected');
1250 checkConsentAndShowChat();
1251 });
1252 } else {
1253 // If Complianz is not enabled, always show
1254 $('#floating-chatbot-button')
1255 .css('display', 'flex')
1256 .removeClass('hidden no-consent')
1257 .fadeTo(500, 1);
1258
1259 // Also check pre-chat message when Complianz is not enabled
1260 checkPreChatDismissal();
1261 }
1262 }
1263
1264
1265 function checkConsentAndShowChat() {
1266 var consentStatus = cmplz_has_consent('marketing');
1267 var consentType = complianz.consenttype;
1268
1269 //console.log('Checking consent:', {status: consentStatus,type: consentType});
1270
1271 let $widget = $('#floating-chatbot-button');
1272 let $chatbot = $('#floating-chatbot');
1273 let $preChat = $('#pre-chat-message');
1274
1275 if (consentStatus === true) {
1276 //console.log('Consent granted - showing widget');
1277 $widget
1278 .removeClass('no-consent')
1279 .css('display', 'flex')
1280 .removeClass('hidden')
1281 .fadeTo(500, 1);
1282 $chatbot.removeClass('no-consent');
1283
1284 // Show pre-chat message if not dismissed
1285 checkPreChatDismissal();
1286 } else {
1287 //console.log('No consent - hiding widget');
1288 $widget
1289 .addClass('no-consent')
1290 .fadeTo(500, 0, function() {
1291 $(this)
1292 .css('display', 'none')
1293 .addClass('hidden');
1294 });
1295 $chatbot.addClass('no-consent');
1296
1297 // Hide pre-chat message when no consent
1298 $preChat.hide();
1299 }
1300 }
1301
1302
1303 // ====================================
1304 // PRE-CHAT MESSAGE HANDLING
1305 // ====================================
1306
1307 function checkPreChatDismissal() {
1308 $.ajax({
1309 url: mxchatChat.ajax_url,
1310 type: 'POST',
1311 data: {
1312 action: 'mxchat_check_pre_chat_message_status',
1313 _ajax_nonce: mxchatChat.nonce
1314 },
1315 success: function(response) {
1316 if (response.success && !response.data.dismissed) {
1317 $('#pre-chat-message').fadeIn(250);
1318 } else {
1319 $('#pre-chat-message').hide();
1320 }
1321 },
1322 error: function() {
1323 console.error('Failed to check pre-chat message dismissal status.');
1324 }
1325 });
1326 }
1327
1328 function handlePreChatDismissal() {
1329 $('#pre-chat-message').fadeOut(200);
1330 $.ajax({
1331 url: mxchatChat.ajax_url,
1332 type: 'POST',
1333 data: {
1334 action: 'mxchat_dismiss_pre_chat_message',
1335 _ajax_nonce: mxchatChat.nonce
1336 },
1337 success: function() {
1338 $('#pre-chat-message').hide();
1339 },
1340 error: function() {
1341 console.error('Failed to dismiss pre-chat message.');
1342 }
1343 });
1344 }
1345
1346
1347 // ====================================
1348 // UTILITY FUNCTIONS
1349 // ====================================
1350
1351 function copyToClipboard(text) {
1352 var tempInput = $('<input>');
1353 $('body').append(tempInput);
1354 tempInput.val(text).select();
1355 document.execCommand('copy');
1356 tempInput.remove();
1357 }
1358
1359
1360 function isImageHtml(str) {
1361 return str.startsWith('<img') && str.endsWith('>');
1362 }
1363
1364
1365 // ====================================
1366 // EVENT HANDLERS & INITIALIZATION
1367 // ====================================
1368
1369 // Popular questions click handler
1370 $('.mxchat-popular-question').on('click', function () {
1371 var question = $(this).text(); // Get the text of the clicked question
1372
1373 // Append the question as if the user typed it
1374 appendMessage("user", question);
1375
1376 // Send the question to the server (backend)
1377 sendMessageToChatbot(question);
1378 });
1379
1380 // Pre-chat message dismissal handlers
1381 $(document).on('click', '.close-pre-chat-message', function(e) {
1382 e.stopPropagation();
1383 handlePreChatDismissal();
1384 });
1385
1386 // Chatbot visibility toggle handlers
1387 $(document).on('click', '#floating-chatbot-button', function() {
1388 var chatbot = $('#floating-chatbot');
1389 if (chatbot.hasClass('hidden')) {
1390 chatbot.removeClass('hidden').addClass('visible');
1391 $(this).addClass('hidden');
1392 $('#chat-notification-badge').hide(); // Hide notification when opening chat
1393 disableScroll();
1394 $('#pre-chat-message').fadeOut(250);
1395 } else {
1396 chatbot.removeClass('visible').addClass('hidden');
1397 $(this).removeClass('hidden');
1398 enableScroll();
1399 checkPreChatDismissal();
1400 }
1401 });
1402
1403 $(document).on('click', '#exit-chat-button', function() {
1404 $('#floating-chatbot').addClass('hidden').removeClass('visible');
1405 $('#floating-chatbot-button').removeClass('hidden');
1406 enableScroll();
1407 });
1408
1409 $(document).on('click', '.close-pre-chat-message', function(e) {
1410 e.stopPropagation(); // Prevent triggering the parent .pre-chat-message click
1411 $('#pre-chat-message').fadeOut(200, function() {
1412 $(this).remove();
1413 });
1414 });
1415
1416 // Add to Cart button handler
1417 $(document).on('click', '.mxchat-add-to-cart-button', function() {
1418 var productId = $(this).data('product-id');
1419
1420 // Get the button text instead of hardcoded "add to cart"
1421 var buttonText = $(this).text() || "add to cart";
1422
1423 // Add a special prefix to indicate this is from button
1424 appendMessage("user", buttonText);
1425 sendMessageToChatbot("!addtocart"); // Special command to indicate button click
1426 });
1427
1428 // PDF upload button handlers
1429 if (document.getElementById('pdf-upload-btn')) {
1430 document.getElementById('pdf-upload-btn').addEventListener('click', function() {
1431 document.getElementById('pdf-upload').click();
1432 });
1433 }
1434
1435 // Word upload button handlers
1436 if (document.getElementById('word-upload-btn')) {
1437 document.getElementById('word-upload-btn').addEventListener('click', function() {
1438 document.getElementById('word-upload').click();
1439 });
1440 }
1441
1442 // PDF file input change handler
1443 addSafeEventListener('pdf-upload', 'change', async function(e) {
1444 const file = e.target.files[0];
1445
1446 if (!file || file.type !== 'application/pdf') {
1447 alert('Please select a valid PDF file.');
1448 return;
1449 }
1450
1451 if (!sessionId) {
1452 console.error('No session ID found');
1453 alert('Error: No session ID found');
1454 return;
1455 }
1456
1457 if (!mxchatChat || !mxchatChat.ajax_url || !mxchatChat.nonce) {
1458 console.error('mxchatChat not properly configured:', mxchatChat);
1459 alert('Error: Ajax configuration missing');
1460 return;
1461 }
1462
1463 // Disable buttons and show loading state
1464 const uploadBtn = document.getElementById('pdf-upload-btn');
1465 const sendBtn = document.getElementById('send-button');
1466 const originalBtnContent = uploadBtn.innerHTML;
1467
1468 try {
1469 const formData = new FormData();
1470 formData.append('action', 'mxchat_upload_pdf');
1471 formData.append('pdf_file', file);
1472 formData.append('session_id', sessionId);
1473 formData.append('nonce', mxchatChat.nonce);
1474
1475 uploadBtn.disabled = true;
1476 sendBtn.disabled = true;
1477 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
1478 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
1479 </svg>`;
1480
1481 const response = await fetch(mxchatChat.ajax_url, {
1482 method: 'POST',
1483 body: formData
1484 });
1485
1486 const data = await response.json();
1487
1488 if (data.success) {
1489 // Hide popular questions if they exist
1490 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
1491 if (popularQuestionsContainer) {
1492 popularQuestionsContainer.style.display = 'none';
1493 }
1494
1495 // Show the active PDF name
1496 showActivePdf(data.data.filename);
1497
1498 appendMessage('bot', data.data.message);
1499 scrollToBottom();
1500 activePdfFile = data.data.filename;
1501 } else {
1502 console.error('Upload failed:', data.data);
1503 alert('Failed to upload PDF. Please try again.');
1504 }
1505 } catch (error) {
1506 console.error('Upload error:', error);
1507 alert('Error uploading file. Please try again.');
1508 } finally {
1509 uploadBtn.disabled = false;
1510 sendBtn.disabled = false;
1511 uploadBtn.innerHTML = originalBtnContent;
1512 this.value = ''; // Reset file input
1513 }
1514 });
1515
1516 // Word file input change handler
1517 addSafeEventListener('word-upload', 'change', async function(e) {
1518 const file = e.target.files[0];
1519
1520 if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
1521 alert('Please select a valid Word document (.docx).');
1522 return;
1523 }
1524
1525 if (!sessionId) {
1526 console.error('No session ID found');
1527 alert('Error: No session ID found');
1528 return;
1529 }
1530
1531 // Disable buttons and show loading state
1532 const uploadBtn = document.getElementById('word-upload-btn');
1533 const sendBtn = document.getElementById('send-button');
1534 const originalBtnContent = uploadBtn.innerHTML;
1535
1536 try {
1537 const formData = new FormData();
1538 formData.append('action', 'mxchat_upload_word');
1539 formData.append('word_file', file);
1540 formData.append('session_id', sessionId);
1541 formData.append('nonce', mxchatChat.nonce);
1542
1543 uploadBtn.disabled = true;
1544 sendBtn.disabled = true;
1545 uploadBtn.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
1546 <circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
1547 </svg>`;
1548
1549 const response = await fetch(mxchatChat.ajax_url, {
1550 method: 'POST',
1551 body: formData
1552 });
1553
1554 const data = await response.json();
1555
1556 if (data.success) {
1557 // Hide popular questions if they exist
1558 const popularQuestionsContainer = document.getElementById('mxchat-popular-questions');
1559 if (popularQuestionsContainer) {
1560 popularQuestionsContainer.style.display = 'none';
1561 }
1562
1563 // Show the active Word document name
1564 showActiveWord(data.data.filename);
1565
1566 appendMessage('bot', data.data.message);
1567 scrollToBottom();
1568 activeWordFile = data.data.filename;
1569 } else {
1570 console.error('Upload failed:', data.data);
1571 alert('Failed to upload Word document. Please try again.');
1572 }
1573 } catch (error) {
1574 console.error('Upload error:', error);
1575 alert('Error uploading file. Please try again.');
1576 } finally {
1577 uploadBtn.disabled = false;
1578 sendBtn.disabled = false;
1579 uploadBtn.innerHTML = originalBtnContent;
1580 this.value = ''; // Reset file input
1581 }
1582 });
1583
1584 // Remove button click handlers
1585 document.getElementById('remove-pdf-btn')?.addEventListener('click', function(e) {
1586 e.preventDefault();
1587 e.stopPropagation();
1588 removeActivePdf();
1589 });
1590
1591 document.getElementById('remove-word-btn')?.addEventListener('click', function(e) {
1592 e.preventDefault();
1593 e.stopPropagation();
1594 removeActiveWord();
1595 });
1596
1597 // Window resize handlers
1598 $(window).on('resize orientationchange', function() {
1599 setFullHeight();
1600 });
1601
1602
1603 // ====================================
1604 // TOOLBAR & STYLING SETUP
1605 // ====================================
1606
1607 // Apply toolbar settings
1608 if (mxchatChat.chat_toolbar_toggle === 'on') {
1609 $('.chat-toolbar').show();
1610 } else {
1611 $('.chat-toolbar').hide();
1612 }
1613
1614 // Apply toolbar icon colors
1615 const toolbarElements = [
1616 '#mxchat-chatbot .toolbar-btn svg',
1617 '#mxchat-chatbot .active-pdf-name',
1618 '#mxchat-chatbot .active-word-name',
1619 '#mxchat-chatbot .remove-pdf-btn svg',
1620 '#mxchat-chatbot .remove-word-btn svg',
1621 '#mxchat-chatbot .toolbar-perplexity svg'
1622 ];
1623
1624 toolbarElements.forEach(selector => {
1625 $(selector).css({
1626 'fill': toolbarIconColor,
1627 'stroke': toolbarIconColor,
1628 'color': toolbarIconColor
1629 });
1630 });
1631
1632
1633 // ====================================
1634 // EMAIL COLLECTION SETUP
1635 // ====================================
1636
1637 // Email collection form setup and handlers
1638 const emailForm = document.getElementById('email-collection-form');
1639 const emailBlocker = document.getElementById('email-blocker');
1640 const chatbotWrapper = document.getElementById('chat-container');
1641
1642 if (emailForm && emailBlocker && chatbotWrapper) {
1643 // Check if email exists for the current session
1644 function checkSessionAndEmail() {
1645 const sessionId = getChatSession();
1646 //console.log("[DEBUG JS] checkSessionAndEmail -> sessionId:", sessionId);
1647
1648 fetch(mxchatChat.ajax_url, {
1649 method: 'POST',
1650 headers: {
1651 'Content-Type': 'application/x-www-form-urlencoded',
1652 },
1653 body: new URLSearchParams({
1654 action: 'mxchat_check_email_provided',
1655 session_id: sessionId,
1656 nonce: mxchatChat.nonce,
1657 }),
1658 })
1659 .then((response) => response.json())
1660 .then((data) => {
1661 //console.log("[DEBUG JS] mxchat_check_email_provided response:", data);
1662
1663 if (data.success) {
1664 if (data.data.logged_in) {
1665 //console.log("[DEBUG JS] User is logged in. Hiding email form.");
1666 emailBlocker.style.display = 'none';
1667 chatbotWrapper.style.display = 'flex';
1668 } else if (data.data.email) {
1669 //console.log("[DEBUG JS] Email found for session. Hiding email form.");
1670 emailBlocker.style.display = 'none';
1671 chatbotWrapper.style.display = 'flex';
1672 } else {
1673 //console.log("[DEBUG JS] No email provided. Showing email form.");
1674 emailBlocker.style.display = 'flex';
1675 chatbotWrapper.style.display = 'none';
1676 }
1677 } else {
1678 //console.log("[DEBUG JS] Error or no data received. Showing email form.");
1679 emailBlocker.style.display = 'flex';
1680 chatbotWrapper.style.display = 'none';
1681 }
1682 })
1683 .catch((error) => {
1684 // console.error("[DEBUG JS] Fetch error -> forcing email form visible:", error);
1685 emailBlocker.style.display = 'flex';
1686 chatbotWrapper.style.display = 'none';
1687 });
1688 }
1689
1690 // Handle email form submission
1691 emailForm.addEventListener('submit', function (event) {
1692 event.preventDefault();
1693 const userEmail = document.getElementById('user-email').value;
1694 const sessionId = getChatSession();
1695
1696 if (userEmail) {
1697 fetch(mxchatChat.ajax_url, {
1698 method: 'POST',
1699 headers: {
1700 'Content-Type': 'application/x-www-form-urlencoded',
1701 },
1702 body: new URLSearchParams({
1703 action: 'mxchat_handle_save_email_and_response',
1704 email: userEmail,
1705 session_id: sessionId,
1706 nonce: mxchatChat.nonce,
1707 }),
1708 })
1709 .then((response) => response.json())
1710 .then((data) => {
1711 //console.log('Backend response:', data);
1712 if (data.success) {
1713 //console.log('Email saved successfully:', userEmail);
1714 emailBlocker.style.display = 'none';
1715 chatbotWrapper.style.display = 'flex';
1716
1717 // Optionally handle bot response
1718 if (data.message) {
1719 appendMessage('bot', data.message);
1720 scrollToBottom();
1721 }
1722 } else {
1723 console.error('Error saving email:', data.message || 'Unknown error');
1724 }
1725 })
1726 .catch((error) => {
1727 console.error('AJAX error:', error);
1728 });
1729 }
1730 });
1731
1732 // Check session and email status on page load
1733 checkSessionAndEmail();
1734 } else if (mxchatChat.email_collection_enabled) {
1735 // Only show error if email collection is enabled but elements are missing
1736 console.error('Essential elements for email handling are missing.');
1737 }
1738
1739 // Open chatbot when pre-chat message is clicked
1740 $(document).on('click', '#pre-chat-message', function() {
1741 var chatbot = $('#floating-chatbot');
1742 if (chatbot.hasClass('hidden')) {
1743 chatbot.removeClass('hidden').addClass('visible');
1744 $('#floating-chatbot-button').addClass('hidden');
1745 $('#pre-chat-message').fadeOut(250); // Hide pre-chat message
1746 disableScroll(); // Disable scroll when chatbot opens
1747 }
1748 });
1749
1750 var closeButton = document.querySelector('.close-pre-chat-message');
1751 if (closeButton) {
1752 closeButton.addEventListener('click', function() {
1753 $('#pre-chat-message').fadeOut(200); // Hide the message
1754
1755 // Send an AJAX request to set the transient flag for 24 hours
1756 $.ajax({
1757 url: mxchatChat.ajax_url,
1758 type: 'POST',
1759 data: {
1760 action: 'mxchat_dismiss_pre_chat_message',
1761 _ajax_nonce: mxchatChat.nonce
1762 },
1763 success: function() {
1764 //console.log('Pre-chat message dismissed for 24 hours.');
1765
1766 // Ensure the message is hidden after dismissal
1767 $('#pre-chat-message').hide();
1768 },
1769 error: function() {
1770 //console.error('Failed to dismiss pre-chat message.');
1771 }
1772 });
1773 });
1774 }
1775 // ====================================
1776 // MAIN INITIALIZATION
1777 // ====================================
1778
1779 if ($('#floating-chatbot').hasClass('hidden')) {
1780 $('#floating-chatbot-button').removeClass('hidden');
1781 }
1782 // Initialize when document is ready
1783 setFullHeight();
1784 initializeChatVisibility();
1785 loadChatHistory();
1786
1787 }); // End of jQuery ready
1788
1789 // ====================================
1790 // GLOBAL EVENT LISTENERS (Outside jQuery)
1791 // ====================================
1792
1793 // Event listener for copy button (code blocks)
1794 document.addEventListener("click", (e) => {
1795 if (e.target.classList.contains("mxchat-copy-button")) {
1796 const copyButton = e.target;
1797 const codeBlock = copyButton
1798 .closest(".mxchat-code-block-container")
1799 .querySelector(".mxchat-code-block code");
1800
1801 if (codeBlock) {
1802 // Preserve formatting using innerText
1803 navigator.clipboard.writeText(codeBlock.innerText).then(() => {
1804 copyButton.textContent = "Copied!";
1805 copyButton.setAttribute("aria-label", "Copied to clipboard");
1806
1807 setTimeout(() => {
1808 copyButton.textContent = "Copy";
1809 copyButton.setAttribute("aria-label", "Copy to clipboard");
1810 }, 2000);
1811 });
1812 }
1813 }
1814 });
1815