')
.addClass(messageClass)
.attr('dir', 'auto');
// Only apply inline colors if AI theme is not active (let CSS handle it)
var skipColors = shouldSkipInlineColors(botId);
if (skipColors) {
messageDiv.css({
'margin-bottom': '1em'
});
} else {
messageDiv.css({
'background': bgColor,
'color': fontColor,
'margin-bottom': '1em'
});
}
// Process the message content - always run linkify to convert markdown
// links and format text. linkify() handles existing HTML safely via
// negative lookaheads that skip URLs already inside
tags.
let fullMessage = linkify(messageText);
// Add images if provided
if (images && images.length > 0) {
fullMessage += '';
images.forEach(img => {
const safeTitle = sanitizeUserInput(img.title);
const safeUrl = encodeURI(img.image_url);
const safeThumbnail = encodeURI(img.thumbnail_url);
fullMessage += `
${safeTitle}
`;
});
fullMessage += '
';
}
// Append HTML content if provided
if (messageHtml && sender !== "user") {
// Only add line breaks if there's actual text content before the HTML
if (fullMessage && fullMessage.trim()) {
fullMessage += '
' + messageHtml;
} else {
fullMessage = messageHtml;
}
}
messageDiv.html(fullMessage);
if (isTemporary) {
messageDiv.addClass('temporary-message');
}
// Append to the correct chatbot instance's chat-box
var $chatBox = getElement(botId, 'chat-box');
messageDiv.hide().appendTo($chatBox).fadeIn(300, function() {
// FIXED: Use event delegation for link tracking
if (sender === "bot" || sender === "agent") {
attachLinkTracking(messageDiv, messageText, botId);
}
if (sender === "bot") {
const lastUserMessage = $chatBox.find('.user-message').last();
if (lastUserMessage.length) {
scrollElementToTop(lastUserMessage, botId);
}
}
if ((sender === "bot" || sender === "agent") && !isTemporary) {
mxchatEnsurePrintRoot(botId);
}
});
if (messageText.id) {
var instance = MxChatInstances.get(botId);
instance.lastSeenMessageId = messageText.id;
hideNotification(botId);
}
} catch (error) {
// Error rendering message - silently continue
}
}
// Helper function to attach link tracking with proper event handling
function attachLinkTracking(messageDiv, messageText, botId) {
botId = botId || 'default';
// Use a slight delay to ensure DOM is ready
setTimeout(function() {
const links = messageDiv.find('a[href]').not('[data-tracked]');
links.each(function() {
const $link = $(this);
const originalHref = $link.attr('href');
// Mark as tracked to avoid duplicate handlers
$link.attr('data-tracked', 'true');
// Only track external URLs
if (originalHref && (originalHref.startsWith('http://') || originalHref.startsWith('https://'))) {
// Remove any existing click handlers first
$link.off('click.tracking');
// Add new click handler with namespace
$link.on('click.tracking', function(e) {
e.preventDefault();
e.stopPropagation();
const messageContext = typeof messageText === 'string'
? messageText.substring(0, 200)
: '';
// Track the click
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
data: {
action: 'mxchat_track_url_click',
session_id: getChatSession(botId),
url: originalHref,
message_context: messageContext,
nonce: mxchatChat.nonce
},
complete: function() {
// Always redirect, even if tracking fails
if ($link.attr('target') === '_blank' || linkTarget === '_blank') {
window.open(originalHref, '_blank');
} else {
window.location.href = originalHref;
}
}
});
return false; // Extra insurance to prevent default
});
}
});
}, 100); // Small delay to ensure DOM is ready
}
function replaceLastMessage(sender, responseText, responseHtml = '', images = [], botId = 'default') {
var messageClass = sender === "user" ? "user-message" : sender === "agent" ? "agent-message" : "bot-message";
var $chatBox = getElement(botId, 'chat-box');
var lastMessageDiv = $chatBox.find('.bot-message.temporary-message, .agent-message.temporary-message').last();
// Determine styles
let bgColor, fontColor;
if (sender === "user") {
bgColor = userMessageBgColor;
fontColor = userMessageFontColor;
} else if (sender === "agent") {
bgColor = liveAgentMessageBgColor;
fontColor = liveAgentMessageFontColor;
} else {
bgColor = botMessageBgColor;
fontColor = botMessageFontColor;
}
// Always run linkify to convert markdown links and format text.
// linkify() already handles existing HTML (its URL patterns use negative lookaheads
// to avoid double-processing URLs that are already inside tags).
var fullMessage = linkify(responseText);
if (responseHtml) {
// Only add line breaks if there's actual text content before the HTML
if (fullMessage && fullMessage.trim()) {
fullMessage += '
' + responseHtml;
} else {
fullMessage = responseHtml;
}
}
if (images.length > 0) {
fullMessage += '';
images.forEach(img => {
fullMessage += `
${img.title}
`;
});
fullMessage += '
';
}
if (lastMessageDiv.length) {
// Replace content immediately to prevent visual gap between thinking dots and response
lastMessageDiv
.html(fullMessage)
.removeClass('bot-message user-message temporary-message')
.addClass(messageClass)
.attr('dir', 'auto');
// Only apply inline colors if AI theme is not active (let CSS handle it)
var skipColors = mxchatChat.skip_inline_colors || shouldSkipInlineColors(botId);
if (!skipColors) {
lastMessageDiv.css({
'background-color': bgColor,
'color': fontColor,
});
}
// Handle link tracking and scroll
if (sender === "bot" || sender === "agent") {
attachLinkTracking(lastMessageDiv, responseText, botId);
const lastUserMessage = $chatBox.find('.user-message').last();
if (lastUserMessage.length) {
scrollElementToTop(lastUserMessage, botId);
}
// Show notification if chat is hidden
var $floatingChatbot = getElement(botId, 'floating-chatbot');
if ($floatingChatbot.hasClass('hidden')) {
showNotification(botId);
}
}
// Re-enable chat input after response is displayed
enableChatInput(botId);
if (sender === "bot" || sender === "agent") {
mxchatEnsurePrintRoot(botId);
}
} else {
appendMessage(sender, responseText, responseHtml, images, false, botId);
// Re-enable chat input after response is displayed
enableChatInput(botId);
}
}
function appendThinkingMessage(botId) {
botId = botId || 'default';
// Don't show thinking dots in live agent mode - message is just forwarded to a human
var indicator = getElementDOM(botId, 'chat-mode-indicator');
if (indicator && indicator.textContent === 'Live Agent') {
return;
}
var $chatBox = getElement(botId, 'chat-box');
// Remove any existing thinking dots in this bot's chat first
$chatBox.find('.thinking-dots').remove();
// Check if we should skip inline colors (AI theme is active)
var skipColors = shouldSkipInlineColors(botId);
// Retrieve the bot message font color and background color
var botMessageFontColor = mxchatChat.bot_message_font_color;
var botMessageBgColor = mxchatChat.bot_message_bg_color;
// Build thinking dots HTML - skip inline colors if AI theme is active
var dotStyle = skipColors ? '' : ' style="background-color: ' + botMessageFontColor + ';"';
var thinkingHtml = '' +
'
' +
'' +
'' +
'' +
'
' +
'
';
// Append the thinking dots to this bot's chat container - skip inline colors if AI theme is active
var messageStyle = skipColors ? '' : ' style="background-color: ' + botMessageBgColor + '; color: ' + botMessageFontColor + ';"';
$chatBox.append('' + thinkingHtml + '
');
scrollToBottom(botId);
}
function removeThinkingDots(botId) {
botId = botId || 'default';
var $chatBox = getElement(botId, 'chat-box');
// Remove by temporary-message class first, then fall back to any bot-message containing thinking dots
$chatBox.find('.thinking-dots').closest('.temporary-message').remove();
$chatBox.find('.bot-message .thinking-dots').closest('.bot-message').remove();
}
// ====================================
// TEXT FORMATTING & PROCESSING
// ====================================
function linkify(inputText) {
if (!inputText) {
return '';
}
// Helper function to check if URL is already encoded
function isUrlEncoded(url) {
// Check for % followed by exactly 2 hex digits
return /%[0-9a-fA-F]{2}/.test(url);
}
// Helper function to safely encode URLs only if needed
function safeEncodeUrl(url) {
// If URL already contains encoded characters, return as-is
if (isUrlEncoded(url)) {
return url;
}
// Otherwise, encode it
return encodeURI(url);
}
// Process markdown headers FIRST
let processedText = formatMarkdownHeaders(inputText);
// Process text styling (bold, italic, strikethrough)
processedText = formatTextStyling(processedText);
// Process code blocks BEFORE processing links
processedText = formatCodeBlocks(processedText);
// Process markdown tables BEFORE converting newlines to paragraphs
processedText = formatMarkdownTables(processedText);
// NOW convert to paragraphs
processedText = convertNewlinesToBreaks(processedText);
// IMPORTANT: Handle citation-style brackets FIRST [URL]
// This prevents them from being processed as markdown links
// Match [URL] where URL is a complete URL in square brackets (common in AI citations)
processedText = processedText.replace(/\[(https?:\/\/[^\]]+)\]/g, (match, url) => {
// Clean the URL of any trailing punctuation
let cleanUrl = url.replace(/[.,;!?]+$/, '');
const safeUrl = safeEncodeUrl(cleanUrl);
// Return as a proper link without the brackets
return `${cleanUrl}`;
});
// Process markdown links: [text](url) and [](url)
// Uses balanced parenthesis matching to handle URLs containing parens
// (e.g. PDF filenames with dates like (2025-08-28).pdf)
processedText = (function(input) {
var result = '';
var i = 0;
while (i < input.length) {
// Look for [ at current position
if (input[i] === '[') {
// Find closing ]
var closeBracket = input.indexOf(']', i + 1);
if (closeBracket === -1 || closeBracket + 1 >= input.length || input[closeBracket + 1] !== '(') {
result += input[i];
i++;
continue;
}
var linkText = input.substring(i + 1, closeBracket);
// Check if URL starts with http
var urlStart = closeBracket + 2;
if (!input.substring(urlStart).match(/^https?:\/\//)) {
result += input[i];
i++;
continue;
}
// Find balanced closing paren
var depth = 1;
var j = urlStart;
while (j < input.length && depth > 0) {
if (input[j] === '(') depth++;
else if (input[j] === ')') depth--;
if (depth > 0) j++;
}
if (depth !== 0) {
result += input[i];
i++;
continue;
}
var url = input.substring(urlStart, j);
var cleanUrl = url.replace(/[\].,;!?]+$/, '');
var encodedUrl = safeEncodeUrl(cleanUrl);
if (!linkText || !linkText.trim()) {
result += '
' + cleanUrl + '';
} else {
var safeText = sanitizeUserInput(linkText);
result += '
' + safeText + '';
}
i = j + 1; // Skip past the closing )
} else {
result += input[i];
i++;
}
}
return result;
})(processedText);
// Process phone numbers: [text](tel:number)
const phonePattern = /\[([^\]]+)\]\((tel:[\d+\-\s()]+)\)/g;
processedText = processedText.replace(phonePattern, (match, text, phone) => {
const safePhone = safeEncodeUrl(phone);
const safeText = sanitizeUserInput(text);
return `
${safeText}`;
});
// Process mailto links: [text](mailto:email)
const mailtoPattern = /\[([^\]]+)\]\((mailto:[^\)]+)\)/g;
processedText = processedText.replace(mailtoPattern, (match, text, mailto) => {
const safeMailto = safeEncodeUrl(mailto);
const safeText = sanitizeUserInput(text);
return `
${safeText}`;
});
// Process standalone URLs - but NOT if they're already in
tags or brackets
// Updated pattern to be more careful about what it matches
const urlPattern = /(^|[^">=\[\]])(https?:\/\/[^\s<"\[\]]+)(?![^<]*<\/a>)(?!\])/gim;
processedText = processedText.replace(urlPattern, (match, prefix, url) => {
// Extra check: make sure this isn't already linked
if (match.includes('href=') || match.includes('')) {
return match;
}
// Clean trailing punctuation
let cleanUrl = url.replace(/[.,;!?)]+$/, '');
const safeUrl = safeEncodeUrl(cleanUrl);
return `${prefix}
${cleanUrl}`;
});
// Process www. URLs - but NOT if they're already in
tags or brackets
const wwwPattern = /(^|[^">/\[\]])(www\.[\S]+)(?![^<]*<\/a>)(?!\])/gim;
processedText = processedText.replace(wwwPattern, (match, prefix, url) => {
// Extra check: make sure this isn't already linked
if (match.includes('href=') || match.includes('')) {
return match;
}
// Clean trailing punctuation
let cleanUrl = url.replace(/[.,;!?)]+$/, '');
const safeUrl = safeEncodeUrl(`http://${cleanUrl}`);
return `${prefix}
${cleanUrl}`;
});
return processedText;
}
function formatMarkdownHeaders(text) {
// Handle h1 to h6 headers
return text.replace(/^(#{1,6})\s+(.+)$/gm, function(match, hashes, content) {
const level = hashes.length;
return `
${content.trim()}`;
});
}
function formatTextStyling(text) {
// IMPORTANT: Protect BOTH HTML href and Markdown URLs from formatting
const protectedSegments = [];
let protectedText = text;
// Step 1a: Protect HTML href="..." attributes
protectedText = protectedText.replace(/href\s*=\s*["']([^"']+)["']/gi, function(match) {
const placeholder = `__PROTECTED_${protectedSegments.length}__`;
protectedSegments.push(match);
return placeholder;
});
// Step 1b: Protect Markdown links [text](url)
// This is crucial - we need to protect the URLs in markdown format
protectedText = protectedText.replace(/\[([^\]]*)\]\(([^)]+)\)/g, function(match) {
const placeholder = `__PROTECTED_${protectedSegments.length}__`;
protectedSegments.push(match);
return placeholder;
});
// Step 1c: Also protect bare URLs that might exist
protectedText = protectedText.replace(/(https?:\/\/[^\s<>"]+)/gi, function(match) {
const placeholder = `__PROTECTED_${protectedSegments.length}__`;
protectedSegments.push(match);
return placeholder;
});
// Step 2: Now apply text styling to the protected text
// Handle bold text (**text**)
protectedText = protectedText.replace(/\*\*(.*?)\*\*/g, '
$1');
// Handle italic text (*text* or _text_) - Safari-compatible (no lookbehind)
// Match single asterisks that aren't part of bold (**) by checking they're not followed/preceded by another *
protectedText = protectedText.replace(/(?!\*\*)\*([^*\n]+)\*(?!\*)/g, '
$1');
// Handle underscores for italic - Safari-compatible (no lookbehind)
// Exclude __PROTECTED_N__ placeholders by checking the content doesn't contain PROTECTED
protectedText = protectedText.replace(/(?!__)_((?!PROTECTED)[^_\n]+)_(?!_)/g, '
$1');
// Handle strikethrough (~~text~~)
protectedText = protectedText.replace(/~~(.*?)~~/g, '
$1');
// Step 3: Restore all protected segments
protectedSegments.forEach((original, index) => {
const placeholder = `__PROTECTED_${index}__`;
protectedText = protectedText.replace(placeholder, original);
});
return protectedText;
}
function formatBoldText(text) {
// This function is kept for compatibility but now uses formatTextStyling
return formatTextStyling(text);
}
function convertNewlinesToBreaks(text) {
// Split the text into paragraphs (marked by double newlines or multiple
tags)
const paragraphs = text.split(/(?:\n\n|\
\s*\
)/g);
// Filter out empty paragraphs and wrap each paragraph in
tags
return paragraphs
.map(para => para.trim())
.filter(para => para.length > 0) // Remove empty paragraphs
.map(para => `
${para}
`)
.join('');
}
function formatCodeBlocks(text) {
// Handle fenced code blocks with language specification (```language)
text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, language, code) => {
const lang = language || 'text';
const escapedCode = escapeHtml(code.trim());
return `
`;
});
// Handle inline code with single backticks
text = text.replace(/`([^`\n]+)`/g, '
$1');
// Handle raw PHP tags (legacy support)
text = text.replace(/(<\?php[\s\S]*?\?>)/g, (match) => {
const escapedCode = escapeHtml(match);
return `
`;
});
return text;
}
function formatMarkdownTables(text) {
var lines = text.split('\n');
var result = [];
var i = 0;
while (i < lines.length) {
// Check for a table: current line has pipes AND next line is a separator row
if (i + 1 < lines.length &&
lines[i].indexOf('|') !== -1 &&
/^\s*\|?[\s\-:]+(\|[\s\-:]+)+\|?\s*$/.test(lines[i + 1])) {
var tableLines = [];
var headerLine = lines[i];
var separatorLine = lines[i + 1];
tableLines.push(headerLine);
tableLines.push(separatorLine);
// Collect remaining table rows
var j = i + 2;
while (j < lines.length && lines[j].indexOf('|') !== -1 && lines[j].trim() !== '') {
tableLines.push(lines[j]);
j++;
}
// Parse alignment from separator row
var sepCells = separatorLine.split('|').filter(function(c) { return c.trim() !== ''; });
var alignments = sepCells.map(function(cell) {
var trimmed = cell.trim();
if (trimmed.charAt(0) === ':' && trimmed.charAt(trimmed.length - 1) === ':') return 'center';
if (trimmed.charAt(trimmed.length - 1) === ':') return 'right';
return 'left';
});
// Build HTML table
var html = '
';
// Header row
var headerCells = tableLines[0].split('|').filter(function(c) { return c.trim() !== ''; });
html += '';
headerCells.forEach(function(cell, idx) {
var align = alignments[idx] || 'left';
html += '| ' + cell.trim() + ' | ';
});
html += '
';
// Body rows
html += '';
for (var r = 2; r < tableLines.length; r++) {
var rowCells = tableLines[r].split('|').filter(function(c) { return c.trim() !== ''; });
html += '';
rowCells.forEach(function(cell, idx) {
var align = alignments[idx] || 'left';
html += '| ' + cell.trim() + ' | ';
});
html += '
';
}
html += '
';
result.push(html);
i = j;
} else {
result.push(lines[i]);
i++;
}
}
return result.join('\n');
}
function sanitizeUserInput(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeHtml(unsafe) {
// Skip escaping if it's already escaped or contains HTML code block markup
if (unsafe.includes('<') || unsafe.includes('>') ||
unsafe.includes('
')) {
return unsafe;
}
return unsafe
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function decodeHTMLEntities(text) {
var textArea = document.createElement('textarea');
textArea.innerHTML = text;
return textArea.value;
}
// ====================================
// UI & SCROLLING CONTROLS
// ====================================
function scrollToBottom(botIdOrInstant, instant) {
// Handle backward compatibility: scrollToBottom() or scrollToBottom(true/false)
var botId = 'default';
if (typeof botIdOrInstant === 'string') {
botId = botIdOrInstant;
instant = instant || false;
} else if (typeof botIdOrInstant === 'boolean') {
instant = botIdOrInstant;
} else {
instant = false;
}
var chatBox = getElement(botId, 'chat-box');
if (instant) {
// Instantly set the scroll position to the bottom
chatBox.scrollTop(chatBox.prop("scrollHeight"));
} else {
// Use requestAnimationFrame for smoother scrolling if needed
let start = null;
const scrollHeight = chatBox.prop("scrollHeight");
const initialScroll = chatBox.scrollTop();
const distance = scrollHeight - initialScroll;
const duration = 500; // Duration in ms
function smoothScroll(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
const currentScroll = initialScroll + (distance * (progress / duration));
chatBox.scrollTop(currentScroll);
if (progress < duration) {
requestAnimationFrame(smoothScroll);
} else {
chatBox.scrollTop(scrollHeight); // Ensure it's exactly at the bottom
}
}
requestAnimationFrame(smoothScroll);
}
}
function scrollElementToTop(element, botId, topOffset) {
botId = botId || 'default';
topOffset = (typeof topOffset === 'number') ? topOffset : 2;
var chatBox = getElement(botId, 'chat-box');
var elementTop = element.position().top + chatBox.scrollTop();
chatBox.animate({ scrollTop: Math.max(0, elementTop - topOffset) }, 500);
}
function showChatWidget(botId) {
botId = botId || 'default';
var $button = getElement(botId, 'floating-chatbot-button');
// First ensure display is set
$button.css('display', 'flex');
// Then handle the fade
$button.fadeTo(500, 1);
// Force visibility
$button.removeClass('hidden');
}
function hideChatWidget(botId) {
botId = botId || 'default';
var $button = getElement(botId, 'floating-chatbot-button');
$button.css('display', 'none');
$button.addClass('hidden');
}
function disableScroll() {
if (isMobile()) {
$('body').css('overflow', 'hidden');
}
}
function enableScroll() {
if (isMobile()) {
$('body').css('overflow', '');
}
}
function isMobile() {
// This can be a simple check, or more sophisticated detection of mobile devices
return window.innerWidth <= 768; // Example threshold for mobile devices
}
function setFullHeight() {
var vh = $(window).innerHeight() * 0.01;
$(':root').css('--vh', vh + 'px');
}
// ====================================
// NOTIFICATION SYSTEM
// ====================================
function createNotificationBadge() {
const chatButton = document.getElementById('floating-chatbot-button');
if (!chatButton) return;
// Remove any existing badge first
const existingBadge = chatButton.querySelector('.chat-notification-badge');
if (existingBadge) {
existingBadge.remove();
}
notificationBadge = document.createElement('div');
notificationBadge.className = 'chat-notification-badge';
notificationBadge.style.cssText = `
display: none;
position: absolute;
top: -5px;
right: -5px;
background-color: red;
color: white;
border-radius: 50%;
padding: 4px 8px;
font-size: 12px;
font-weight: bold;
z-index: 10001;
`;
chatButton.style.position = 'relative';
chatButton.appendChild(notificationBadge);
}
function showNotification(botId) {
botId = botId || 'default';
const badge = getElementDOM(botId, 'chat-notification-badge');
var $floatingChatbot = getElement(botId, 'floating-chatbot');
if (badge && $floatingChatbot.hasClass('hidden')) {
badge.style.display = 'block';
badge.textContent = '1';
}
}
function hideNotification(botId) {
botId = botId || 'default';
const badge = getElementDOM(botId, 'chat-notification-badge');
if (badge) {
badge.style.display = 'none';
}
}
function startNotificationChecking(botId) {
botId = botId || 'default';
const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
if (!chatPersistenceEnabled) return;
createNotificationBadge(botId);
var instance = MxChatInstances.get(botId);
instance.notificationCheckInterval = setInterval(function() {
checkForNewMessages(botId);
}, 30000); // Check every 30 seconds
}
function stopNotificationChecking(botId) {
botId = botId || 'default';
var instance = MxChatInstances.get(botId);
if (instance.notificationCheckInterval) {
clearInterval(instance.notificationCheckInterval);
}
}
function checkForNewMessages() {
const sessionId = getChatSession();
const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
if (!chatPersistenceEnabled) return;
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
data: {
action: 'mxchat_check_new_messages',
session_id: sessionId,
last_seen_id: lastSeenMessageId,
nonce: mxchatChat.nonce
},
success: function(response) {
if (response.success && response.data.hasNewMessages) {
showNotification();
}
}
});
}
// ====================================
// LIVE AGENT FUNCTIONALITY
// ====================================
function startPolling(botId) {
botId = botId || 'default';
var instance = MxChatInstances.get(botId);
// Clear any existing interval first
stopPolling(botId);
instance.pollingInterval = setInterval(function() {
checkForAgentMessages(botId);
}, 5000);
}
function stopPolling(botId) {
botId = botId || 'default';
var instance = MxChatInstances.get(botId);
if (instance.pollingInterval) {
clearInterval(instance.pollingInterval);
instance.pollingInterval = null;
}
}
function checkForAgentMessages(botId) {
botId = botId || 'default';
var instance = MxChatInstances.get(botId);
const sessionId = getChatSession(botId);
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
dataType: 'json',
data: {
action: 'mxchat_fetch_new_messages',
session_id: sessionId,
last_seen_id: instance.lastSeenMessageId,
persistence_enabled: 'true',
nonce: mxchatChat.nonce
},
success: function (response) {
if (response.success && response.data?.new_messages) {
let hasNewMessage = false;
response.data.new_messages.forEach(function (message) {
if (message.role === "agent" && !instance.processedMessageIds.has(message.id)) {
hasNewMessage = true;
appendMessage("agent", message.content, '', [], false, botId);
instance.lastSeenMessageId = message.id;
instance.processedMessageIds.add(message.id);
}
});
if (hasNewMessage) {
enableChatInput(botId);
}
var $floatingChatbot = getElement(botId, 'floating-chatbot');
if (hasNewMessage && $floatingChatbot.hasClass('hidden')) {
showNotification(botId);
}
scrollToBottom(botId, true);
}
// Handle chat mode transitions (e.g. agent ended chat via !endchat)
if (response.success && response.data?.chat_mode) {
updateChatModeIndicator(response.data.chat_mode, botId);
}
},
error: function (xhr, status, error) {
// Polling error - silently continue
}
});
}
// ====================================
// CHAT HISTORY & PERSISTENCE
// ====================================
function loadChatHistory(botId, onComplete) {
botId = botId || 'default';
var instance = MxChatInstances.get(botId);
// Prevent duplicate loading
if (instance.chatHistoryLoaded) {
if (onComplete) onComplete();
return;
}
// Use getChatSession which returns null if no session exists (does NOT create one)
var sessionId = getChatSession(botId);
var chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
// No session yet — nothing to load. History will load after first message via ensureSession.
if (!sessionId) {
instance.chatHistoryLoaded = true;
if (onComplete) onComplete();
return;
}
if (chatPersistenceEnabled && sessionId) {
$.ajax({
url: mxchatChat.ajax_url,
type: 'POST',
dataType: 'json',
data: {
action: 'mxchat_fetch_conversation_history',
session_id: sessionId
},
success: function(response) {
// Handle session reset (IP changed while user was away)
if (response.success === false && response.data && response.data.action === 'reset_session') {
// Silent reset — new session but don't clear UI
MxChatInstances.silentResetSession(botId);
instance.chatHistoryLoaded = true; // Prevent retry loop
if (onComplete) onComplete();
return;
}
// Check if the response indicates success
if (response.success) {
// Handle case where conversation data exists and is an array
if (response.data && Array.isArray(response.data.conversation)) {
var $chatBox = getElement(botId, 'chat-box');
var $fragment = $(document.createDocumentFragment());
let highestMessageId = instance.lastSeenMessageId;
// Update chat mode if provided
if (response.data.chat_mode) {
updateChatModeIndicator(response.data.chat_mode, botId);
}
// Only process if there are actual messages
if (response.data.conversation.length > 0) {
// IMPORTANT: Clear existing messages before loading history
$chatBox.empty();
$.each(response.data.conversation, function(index, message) {
// Skip agent messages if persistence is off
if (!chatPersistenceEnabled && message.role === 'agent') {
return;
}
var messageClass, messageBgColor, messageFontColor;
switch (message.role) {
case 'user':
messageClass = 'user-message';
messageBgColor = userMessageBgColor;
messageFontColor = userMessageFontColor;
break;
case 'agent':
messageClass = 'agent-message';
messageBgColor = liveAgentMessageBgColor;
messageFontColor = liveAgentMessageFontColor;
break;
default:
messageClass = 'bot-message';
messageBgColor = botMessageBgColor;
messageFontColor = botMessageFontColor;
break;
}
var messageElement = $('
').addClass(messageClass)
.css({
'background': messageBgColor,
'color': messageFontColor
});
var content = message.content;
content = content.replace(/\\'/g, "'").replace(/\\"/g, '"');
content = decodeHTMLEntities(content);
// Skip linkify for messages containing structured HTML
// (forms, product cards, galleries, etc.) to avoid
// markdown formatting corrupting HTML attributes
// (e.g. underscores in name="field_name" becoming
tags)
if (content.includes("mxchat-product-card") ||
content.includes("mxchat-image-gallery") ||
content.includes("mxchat-featured-products") ||
content.includes("