Hi Max, thanks for this. It works great and the solution is fixed.
Note: We already declared ‘const waitForElement’ in JS code to keep users on the same tab when clicking a link inside the chatbot (I included this code in my original post).
With the help of Claude AI, I was able to find a workaround to maintain the same-tab function with the CTA buttons injected. I’ve shared it below for future reference:
// Consolidated code - no duplicate declarations
const WIDGET_ID = 'e6b5fefa-bada-4235-b3bc-7e9d941415f0';
// Single waitForElement function for both features
const waitForChatElement = (selector, root = document, maxAttempts = 500) =>
new Promise((resolve) => {
let attempts = 0;
const check = () => {
const element = root.querySelector(selector);
if (element) {
resolve(element);
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(check, 100);
}
};
check();
});
// CTA Button Configuration
const CALL_BUTTON_TEXT = 'Call Us';
const CALL_BUTTON_LINK = 'tel:8887298812';
const SMS_BUTTON_TEXT = 'Text Us';
const SMS_BUTTON_LINK = 'sms:8542239313';
let chatContainer = null;
// Initialize both features
waitForChatElement(`.eapps-ai-chatbot-${WIDGET_ID}-custom-css-root .es-window-body`).then((container) => {
chatContainer = container;
// Feature 1: Fix links to open in same tab
const fixLinks = () => {
const links = chatContainer.querySelectorAll("a[target='_blank']");
links.forEach((link) => {
link.target = "_self";
});
};
const linkObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
fixLinks();
}
});
});
linkObserver.observe(chatContainer, {
childList: true,
subtree: true,
});
fixLinks();
});
// Feature 2: Add CTA buttons
const observeChildNodes = (element, callback) => {
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
mutation.addedNodes.forEach(callback);
}
});
observer.observe(element, { childList: true });
return observer;
};
const createCTAButton = (text, link, className) =>
Object.assign(document.createElement('a'), {
href: link,
textContent: text,
className: `es-btn ${className}`,
target: '_blank',
});
const addButtons = (btns, portal) => {
const lastAssistantMessage = [
...portal.querySelectorAll('.es-message-group-assistant'),
].pop();
if (lastAssistantMessage) {
lastAssistantMessage.after(btns);
}
};
const firstAssistantMessageSelector =
'.es-message-group-assistant:not(:first-child):has(.es-message-content-container)';
waitForChatElement(firstAssistantMessageSelector).then((assistantFirstMsg) => {
const portal = assistantFirstMsg.closest('#__EAAPS_PORTAL');
let messageContainer = portal.querySelector(
'[class*="widget-window__MessagesContainer-sc"]'
);
if (!portal || !messageContainer) return;
const wrapper = document.createElement('div');
wrapper.className = 'es-cta-buttons';
// Call button
const callBtn = createCTAButton(
CALL_BUTTON_TEXT,
CALL_BUTTON_LINK,
'es-btn-call'
);
// SMS button
const smsBtn = createCTAButton(
SMS_BUTTON_TEXT,
SMS_BUTTON_LINK,
'es-btn-sms'
);
wrapper.append(callBtn, smsBtn);
addButtons(wrapper, portal);
// Track new assistant messages
const observeNewMessages = () => {
return observeChildNodes(messageContainer, (el) => {
if (el.classList && el.classList.contains('es-message-group-assistant')) {
addButtons(wrapper, portal);
}
});
};
let messagesObserver = observeNewMessages();
// Track when chat window opens
observeChildNodes(portal, (el) => {
if (el.classList && el.classList.contains('es-window-container')) {
addButtons(wrapper, portal);
messagesObserver.disconnect();
messageContainer = portal.querySelector(
'[class*="widget-window__MessagesContainer-sc"]'
);
messagesObserver = observeNewMessages();
}
});
});
Thanks so much!