`
const input = shadowRoot.getElementById("cbuserInput");
const chatFooter = shadowRoot.querySelector(".chat-footer");
const chatContainer = shadowRoot.getElementById('chatContainer');
const inputContainer = shadowRoot.getElementById("inputContainer");
const sendButton = shadowRoot.getElementById("cb-sendmessage-button");
const mainButton = shadowRoot.getElementById("mainButton");
const tooltipContainer = shadowRoot.getElementById("tooltipContainer");
const closeButton = shadowRoot.getElementById("closeButton");
const messagesContainer = shadowRoot.querySelector(".messages-container");
const formElement = document.createElement("div");
function showMainButtonAndTooltip() {
mainButton.classList.remove("fade-out");
mainButton.classList.add("fade-in");
tooltipContainer.classList.remove("fade-out");
tooltipContainer.classList.add("fade-in");
}
function hideMainButtonAndTooltip() {
mainButton.classList.remove("fade-in");
mainButton.classList.add("fade-out");
tooltipContainer.classList.remove("fade-in");
tooltipContainer.classList.add("fade-out");
}
closeButton.addEventListener("click", () => {
chatContainer.classList.remove("chat-open");
chatContainer.classList.add("chat-close");
setTimeout(() => {
chatContainer.classList.add("chat-hidden");
chatContainer.classList.remove("chat-close");
mainButton.style.display = "flex";
showMainButtonAndTooltip();
}, 300);
});
function getCookie(name) {
const cookies = document.cookie.split(";");
for (const cookie of cookies) {
const [cookieName, cookieValue] = cookie.trim().split("=");
if (cookieName === name) {
return cookieValue;
}
}
return undefined;
}
function setCookie(name, value) {
document.cookie = name + "=" + value + "; path=/";
}
function fetchChatId() {
try {
const xhr = new XMLHttpRequest();
xhr.open(
"GET",
`${serverUrl}/get_chat_id?chatbot_id=${CHATBOT_ID}&workspace_id=${WORKSPACE_ID}`,
false,
);
xhr.withCredentials = true;
xhr.send();
if (xhr.status !== 200) {
throw new Error(`HTTP error! status: ${xhr.status}`);
}
return xhr.responseText;
} catch (error) {
console.error("Error fetching getting chat id:", error);
return null;
}
}
function getChatID() {
let chatID = getCookie("chatID");
if (chatID) {
input.removeAttribute("disabled");
chatFooter.classList.remove("disabled");
// formElement.classList.add("chat-hidden");
return chatID;
} else {
return null;
}
}
async function createChat() {
await fetch(
`${serverUrl}/loadChat?chat_id=${chatID}&userLanguage=${chatLanguage}&chatbot_id=${CHATBOT_ID}&workspace_id=${WORKSPACE_ID}`,
);
return Promise.resolve('Created Chat');
}
var firstclick = true;
var chatID = getChatID();
const LOGGED_IN = chatID;
const sendRating = (rating, ticketId) => {
const xhr = new XMLHttpRequest();
xhr.open(
"POST",
`${serverUrl}/rate_support?chatbot_id=${CHATBOT_ID}&workspace_id=${WORKSPACE_ID}`,
true,
);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(
JSON.stringify({ chat_id: chatID, ticket_id: ticketId, rating: rating }),
);
};
const parseCookies = () => {
result = []
for (const key in VALUES_TO_RPARSE) {
result.push({key: getCookie(key)})
}
return result
}
const sendCookies = (rating, ticketId) => {
const parsedCookies = parseCookies()
const xhr = new XMLHttpRequest();
xhr.open(
"POST",
`${serverUrl}/rate_support?chatbot_id=${CHATBOT_ID}&workspace_id=${WORKSPACE_ID}`,
true,
);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(
JSON.stringify({ chat_id: chatID, cookies: parsedCookies }),
);
};
const addRatingMessageHandlers = (ticket_id) => {
const starRating = document.getElementById(`rating-${ticket_id}`);
const starRatingOverlay = document.getElementById(`rating-container-${ticket_id}`);
const closeStarRatingOverlay = document.getElementById(`close-rating-container-${ticket_id}`);
const text = document.getElementById(`text-${ticket_id}`);
const stars = [];
for (let i = 0; i < 5; i++) {
stars.push(document.getElementById(`star-${i}-${ticket_id}`));
}
const handleMouseOver = (e) => {
if (e.target.classList.contains("star")) {
const value = e.target.getAttribute("data-value");
highlightStars(value);
}
};
const handleMouseOut = () => {
highlightStars(0);
};
const handleClick = (e) => {
if (e.target.classList.contains("star")) {
const value = e.target.getAttribute("data-value");
selectStars(value);
sendRating(value, ticket_id);
starRating.removeEventListener("click", handleClick);
starRating.removeEventListener("mouseover", handleMouseOver);
starRating.removeEventListener("mouseout", handleMouseOut);
text.classList.add('fade-out');
starRating.style.opacity = '0';
// Change the content after the fade-out animation
setTimeout(() => {
text.setAttribute('data-content', `${UIElements.review}`);
// Start fade-in effect
text.classList.remove('fade-out');
text.classList.add('fade-in');
}, 750); // Match the transition duration (0.5s)
setTimeout(() => {
closeRating();
}, 3000);
}
};
starRating.addEventListener("mouseover", handleMouseOver);
starRating.addEventListener("mouseout", handleMouseOut);
starRating.addEventListener("click", handleClick);
closeStarRatingOverlay.addEventListener("click", closeRating);
function highlightStars(count) {
for (let i = 0; i < stars.length; i++) {
if (i < count) {
stars[i].classList.add("hover");
} else {
stars[i].classList.remove("hover");
}
}
}
function selectStars(count) {
for (let i = 0; i < stars.length; i++) {
if (i < count) {
stars[i].classList.add("selected");
} else {
stars[i].classList.remove("selected");
}
}
}
function closeRating(){
sendRating("0", ticket_id);
starRatingOverlay.classList.add('hidden');
}
};
function addTimezoneOffset(time, offset) {
// Parse the time (assumes "HH:MM" format)
const [hours, minutes] = time.split(":").map(Number);
// Create a Date object for today with the given time
const now = new Date();
const dateWithTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes);
// Add the timezone offset (in minutes)
dateWithTime.setMinutes(dateWithTime.getMinutes() + offset);
// Format the updated time back to "HH:MM"
const updatedHours = dateWithTime.getHours().toString().padStart(2, "0");
const updatedMinutes = dateWithTime.getMinutes().toString().padStart(2, "0");
return `${updatedHours}:${updatedMinutes}`;
}
function createInfo(sender, time, status) {
if (!time){
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
time = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
} else if (time.includes(" ")){
time = time.split(" ")[1]; // Split by space and take the second part
time = addTimezoneOffset(time,timezoneOffset);
}
// Determine the SVG based on conditions
let svg = "";
if (sender === "user") {
if (status === "read") {
svg = `
`;
} else {
svg = `
`;
}
}
// Return the full HTML
return `
${time}
${svg}
`;
}
function addNewMessage({sender, text, time, status, rating = null, ticket_id = null}) {
if (sender === "rating") {
console.log(sender, text, time, status, rating, ticket_id);
const overlayContainer = document.createElement("div");
overlayContainer.classList.add("stars-rating-container", "hidden");
overlayContainer.id = `rating-container-${ticket_id}`;
overlayContainer.innerHTML = `
`;
const ratingContainer = document.createElement("div");
ratingContainer.className = "stars-rating";
ratingContainer.id = `rating-${ticket_id}`;
for (let i = 0; i < 5; i++) {
const star = document.createElement("span");
star.id = `star-${i}-${ticket_id}`;
if (rating !== null && rating > i) {
star.className = "star selected";
return;
} else {
star.className = "star";
star.setAttribute("data-value", i + 1);
}
ratingContainer.appendChild(star);
}
chatContainer.appendChild(overlayContainer);
overlayContainer.appendChild(ratingContainer);
if (rating === null) {
addRatingMessageHandlers(ticket_id);
setTimeout(() => {
overlayContainer.classList.remove("hidden");
}, 0);
}
return;
}
const messageWrapper = document.createElement("div");
messageWrapper.className = `message-wrapper new-message ${sender}`;
const messageDiv = document.createElement("div");
messageDiv.className = `message ${sender}`;
const innerDiv = document.createElement("div");
messageDiv.appendChild(innerDiv);
messageWrapper.appendChild(messageDiv);
messagesContainer.appendChild(messageWrapper);
if (sender === "system"){
innerDiv.style.display = 'flex';
innerDiv.style.alignItems = 'center';
innerDiv.innerHTML = ``+ marked.parse(text) + ``;
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return;
} else {
innerDiv.innerHTML = marked.parse(text) + createInfo(sender, time, status);
}
const links = innerDiv.getElementsByTagName("a");
for (let link of links) {
link.style.color = "darkblue";
link.style.textDecoration = "underline";
link.style.fontStyle = "italic";
link.target="_blank";
link.rel="noopener noreferrer";
}
function extractTwoEmojis(text) {
// Regex to find emojis (Unicode-aware)
const emojiRegex = /[\p{Emoji}]/gu;
// Extract emojis from the text
const emojis = text.match(emojiRegex) || [];
// Check if the number of emojis is exactly 1 or 2
if (emojis.length === 0 || emojis.length > 2) {
// Return the original text if there are 0 or more than 2 emojis
return {
textWithoutEmojis: text,
emojis: []
};
}
// Ensure exactly 2 emojis (duplicate the first if only 1 found)
const resultEmojis = emojis.length === 1
? [emojis[0], emojis[0]]
: emojis;
// Remove the emojis from the text
const textWithoutEmojis = text.replace(emojiRegex, '').trim();
return {
textWithoutEmojis,
emojis: resultEmojis
};
}
function placeIconsNearHeading(heading, emoji1, emoji2) {
if (!heading || !emoji1 || !emoji2) {
//console.error("Invalid heading or emojis.");
return;
}
//console.log("Original heading text:", heading.textContent);
// Generate unique IDs for the two emojis
const uniqueId1 = `emoji1-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
const uniqueId2 = `emoji2-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
const originalText = heading.textContent.trim();
const words = originalText.split(/\s+/);
if (words.length < 2) {
console.error("Heading must have at least two words for proper emoji placement.");
return;
}
// STEP 1: Insert the first emoji at the start of the first word, wrapped in a
// We add both an id AND a common class "heading-emoji" to this span.
words[0] =
`${emoji1}` + words[0];
// Helper to build the heading HTML at a given insertion index for the second emoji
function buildHeadingText(wordsArray, secondEmojiInsertIndex) {
const tempWords = [...wordsArray];
// Insert the second emoji (also with uniqueId2 and the same "heading-emoji" class)
tempWords[secondEmojiInsertIndex] += `${emoji2}`;
return tempWords.join(" ");
}
// Check if both emojis are on the same line (compare their .top in getBoundingClientRect)
function areBothEmojisOnSameLine() {
const e1 = heading.querySelector(`#${uniqueId1}`);
const e2 = heading.querySelector(`#${uniqueId2}`);
if (!e1 || !e2) return false;
const e1Top = e1.getBoundingClientRect().top;
const e2Top = e2.getBoundingClientRect().top;
//console.log(`Emoji1 top: ${e1Top}, Emoji2 top: ${e2Top}`);
// If they're on the same line, their .top is nearly identical
return Math.abs(e1Top - e2Top) < 1;
}
// STEP 2: Try placing the second emoji from the last word backward
// until we find a position that keeps both emojis on the same first line.
let foundValidPosition = false;
for (let i = words.length - 1; i >= 0; i--) {
heading.innerHTML = buildHeadingText(words, i);
//console.log(`Trying to place second emoji after word index ${i}:`, heading.textContent);
// Force a reflow (so getBoundingClientRect() is accurate)
heading.getBoundingClientRect();
if (areBothEmojisOnSameLine()) {
foundValidPosition = true;
//console.log("✅ Both emojis fit on the same (first) line at this position.");
break;
} else {
// Optionally clear innerHTML before trying the next index
heading.innerHTML = "";
}
}
// STEP 3: If no valid position was found, restore original text
if (!foundValidPosition) {
heading.textContent = originalText;
console.error("❌ Unable to fit both emojis on the first line. Restoring original text.");
}
}
async function fitHeadingToContainer(heading) {
const container = innerDiv;
container.style.width = '85%';
const container_width = container.getBoundingClientRect().width;
container.style.width = null;
// Start from a base font size (e.g., 1.2em) or whatever you prefer
let fontSize = 1.25;
let minFontSize = 1.135
let letterSpacing = 0;
heading.style.fontSize = fontSize + 'em';
const oneline = 24;
// Measure text width vs container width
function textHeight() {
return heading.getBoundingClientRect().height;
}
function textWidth() {
return heading.getBoundingClientRect().width;
}
// Decrease font size until it fits
while (textWidth() >= container_width && textHeight() > oneline) {
if (fontSize > minFontSize) {
fontSize -= 0.01; // adjust decrement step as needed
heading.style.fontSize = fontSize.toFixed(2) + 'em';
} else if (letterSpacing > -0.055) {
letterSpacing -= 0.001;
heading.style.letterSpacing = letterSpacing.toFixed(3) + 'em';
}
if (letterSpacing > -0.025) {
letterSpacing -= 0.001;
heading.style.letterSpacing = letterSpacing.toFixed(3) + 'em';
}
if (fontSize <= minFontSize && letterSpacing <= -0.055) {
//console.log('Failed to optimize Font');
heading.style.fontSize = '1.25em';
heading.style.letterSpacing = '0em';
const extracted = extractTwoEmojis(heading.textContent);
heading.textContent = extracted.textWithoutEmojis;
placeIconsNearHeading(heading, extracted.emojis[0], extracted.emojis[1]);
break;
}
}
await new Promise((resolve) => requestAnimationFrame(resolve));
}
for (let i = 1; i <= 6; i++) {
const headers = innerDiv.getElementsByTagName(`h${i}`);
for (let header of headers) {
if (i === 1) {
header.style.fontWeight = "700";
fitHeadingToContainer(header);
} else {
header.style.fontWeight = "600";
header.style.fontSize = `${1.25 - (i - 1) * 0.08}em`;
}
}
}
const listItems = innerDiv.getElementsByTagName("li");
for (let item of listItems) {
// Set styles for the
element
item.style.listStyleType = item.parentNode.nodeName === "OL" ? "decimal" : "disc";
}
const preTags = innerDiv.getElementsByTagName("pre");
for (let pre of preTags) {
pre.style.whiteSpace = "break-spaces";
}
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
function markAllMessagesRead(){
const infos = messagesContainer.querySelectorAll(".message.user .msginfo");
infos.forEach(info => {
const time_paragraph = info.getElementsByTagName("p")[0];
if (time_paragraph) {
info.innerHTML = createInfo("user", time_paragraph.innerText, "read");
}
});
}
const formHTML = `
${UIElements.form.heading}
`;
function addFormToChat() {
formElement.innerHTML = formHTML;
formElement.classList.add('overlay-container');
chatContainer.appendChild(formElement);
}
// NETWORK //
scriptSocket.addEventListener("load", () => {
const socket = io(serverUrl , {
reconnection: true, // default is true, enables auto-reconnection
reconnectionAttempts: 5, // Number of tries before giving up
reconnectionDelay: 1000, // Initial delay between attempts (in ms)
reconnectionDelayMax: 5000, // Max delay between attempts (in ms)
});
socket.on("connect", () => {
console.log("Connected to the server:", socket.id);
});
// Listen for the disconnect event
socket.on("disconnect", (reason) => {
console.log("Disconnected from the server:", reason);
// Optionally handle custom logic here
});
// Listen for the connect_error event
socket.on("connect_error", (error) => {
console.error("Connection error:", error);
});
// Listen for the reconnect event
socket.on("reconnect", (attempt) => {
console.log(`Reconnected successfully on attempt ${attempt}`);
});
// Listen for the reconnect_attempt event
socket.on("reconnect_attempt", (attempt) => {
console.log(`Attempting to reconnect, attempt number: ${attempt}`);
});
// Listen for the reconnect_error event
socket.on("reconnect_error", (error) => {
console.error("Reconnection attempt failed:", error);
});
// Listen for the reconnect_failed event
socket.on("reconnect_failed", () => {
console.error("Reconnection failed. Giving up.");
});
function connectToRoom() {
socket.emit("connect_to_room", { user_id: chatID });
socket.emit("message", {
data: "SYS_MESSAGE_CONNECTION",
user_iD: chatID,
name: "user",
});
}
function sendSocketIOMessage(text) {
socket.emit("message", {
data: text,
user_iD: chatID,
name: "user",
chatbot_id: CHATBOT_ID,
});
}
function newMessageHandler(addNewMessage, setManagerConnected) {
socket.on("message", (data) => {
console.log(data);
if (data.name === "system") {
if (data.data === "MANAGER_CALLED") {
return;
} else if (data.data === "MANAGER_CONNECTED") {
console.log("Manager connected");
setManagerConnected(true);
return;
} else if (data.data === "SYS_MESSAGE_DISCONNECT") {
console.log("Manager disconnected");
setManagerConnected(false);
return;
} else if (data.data === "READ_USER_MSGS") {
markAllMessagesRead();
return;
}
}
if (data.name !== "user") {
addNewMessage({
sender: data.name,
text: data.data,
ticket_id: data.ticket_id
});
markAllMessagesRead();
}
});
}
async function getChatHistory() {
const response = await fetch(
`${serverUrl}/loadChat?chat_id=${chatID}&userLanguage=${chatLanguage}&chatbot_id=${CHATBOT_ID}&workspace_id=${WORKSPACE_ID}`,
);
const messages = await response.json();
console.log(messages);
if (Array.isArray(messages) && messages.length === 0) {
const welcomeResponse = await fetch(
`${serverUrl}/getWelcomeMessage?chat_id=${chatID}&chatbot_id=${CHATBOT_ID}&workspace_id=${WORKSPACE_ID}`,
);
const welcomeMessage = await welcomeResponse.json();
addNewMessage({
sender: "bot",
text: welcomeMessage
});
} else {
messages.forEach((message) =>
addNewMessage({
sender: message.sender,
text: message.text,
time: message.time,
status: message.status,
rating: message.rating,
ticket_id: message.ticket_id
}),
);
}
}
async function sendMessage(userText) {
const response = await fetch(
`${serverUrl}/ask?chat_id=${chatID}&chatbot_id=${CHATBOT_ID}&workspace_id=${WORKSPACE_ID}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ messageText: userText }),
},
);
const data = await response.json();
if (data.not_final) {
return null;
}
if (response.status == 401){
return "Please fill out the form before trying to send a message!";
}
return data.text;
}
const getManagerConnectedSync = () => {
try {
const xhr = new XMLHttpRequest();
xhr.open(
"GET",
`${serverUrl}/contacting_human?chat_id=${getChatID()}&chatbot_id=${CHATBOT_ID}&workspace_id=${WORKSPACE_ID}`,
false,
);
xhr.withCredentials = true;
xhr.send(); // Send the request
if (xhr.status !== 200) {
throw new Error(`HTTP error! status: ${xhr.status}`);
}
const response = JSON.parse(xhr.responseText);
console.log("getManagerConnectedSync: ");
console.log(response);
return response.value;
} catch (error) {
console.error("Error fetching contacting human status:", error);
return null;
}
};
let managerConnected = getManagerConnectedSync();
function setManagerConnected(value) {
managerConnected = value;
}
function handleFormSubmit(event) {
var button = shadowRoot.getElementById('formSubmitButton');
button.setAttribute('disabled', '');
// Предотвращаем стандартное поведение формы
event.preventDefault();
const formXhr = new XMLHttpRequest();
formXhr.open(
"POST",
`${serverUrl}/chat_details?chatbot_id=${CHATBOT_ID}&workspace_id=${WORKSPACE_ID}`,
true,
);
formXhr.setRequestHeader("Content-Type", "application/json");
formXhr.onload = function () {
if (formXhr.status === 200) {
// On successful form submission
input.removeAttribute("disabled");
setTimeout(() => {
closeButton.classList.add('fade-out');
setTimeout(() => {
closeButton.classList.remove('fade-out');
closeButton.classList.remove('overlay');
}, 500);
}, 100);
chatFooter.classList.remove("disabled", "cursor-not-allowed");
formElement.classList.add('hidden');
setTimeout(() => {
input.focus();
}, 4850);
setCookie("chatID", chatID);
getChatHistory();
connectToRoom();
newMessageHandler(addNewMessage, setManagerConnected);
} else {
console.error("Error sending form:", formXhr.status);
alert("Error sending form. Please try again.");
button.removeAttribute('disabled');
}
};
formXhr.onerror = function () {
console.error("Network error occurred");
alert("Network error occurred. Please check your connection.");
button.removeAttribute('disabled');
};
try {
chatID = fetchChatId();
const formData = {
chat_id: chatID,
details: {
firstName: event.target.firstName.value,
lastName: event.target.lastName.value,
email: event.target.email.value,
phoneNumber: event.target.phoneNumber.value,
},
};
createChat().then((result) => {
formXhr.send(JSON.stringify(formData));
});
} catch (error) {
console.error("Error sending form:", error);
alert("Error sending form. Please try again.");
button.removeAttribute('disabled');
}
}
function sendMessageButtonOnClick() {
if (![... sendButton.classList].includes("disabled")) {
const userMessage = input.value.trim();
input.value = "";
if (userMessage) {
addNewMessage({
sender: "user",
text: userMessage
});
if (managerConnected) {
sendSocketIOMessage(userMessage);
} else {
sendButton.classList.add("loading" , "disabled");
sendMessage(userMessage).then((botMessage) => {
if (botMessage) {
addNewMessage({
sender: "bot",
text: botMessage
});
markAllMessagesRead();
sendButton.classList.remove("loading" , "disabled");
if (!input.value.length > 0 ) {
setTimeout(() => {
sendButton.classList.remove('black');
}, 400);
}
}
});
}
}
}
}
sendButton.addEventListener("click", sendMessageButtonOnClick);
input.addEventListener("keydown", (event) => {
if (![... sendButton.classList].includes("disabled")){
if (event.key === "Enter") {
sendMessageButtonOnClick();
} else {
setTimeout(() => {
if (input.value.length > 0 ) {
sendButton.classList.add('black');
} else if (![... sendButton.classList].includes("disabled")){
sendButton.classList.remove('black');
}
}, 10);
}
}
});
mainButton.addEventListener("click", () => {
hideMainButtonAndTooltip();
if (firstclick) {
if (!LOGGED_IN) {
input.setAttribute("disabled", "");
chatFooter.classList.add("cursor-not-allowed");
chatFooter.classList.add("disabled");
closeButton.classList.add('overlay');
addFormToChat();
shadowRoot
.getElementById("userForm")
.addEventListener("submit", handleFormSubmit);
} else {
getChatHistory();
connectToRoom();
newMessageHandler(addNewMessage, setManagerConnected);
}
firstclick = false;
}
setTimeout(() => {
mainButton.style.display = "none";
console.log('mainButton was hidden');
chatContainer.classList.remove("chat-hidden");
console.log('chatContainer made visible');
chatContainer.classList.add("chat-open");
console.log('chatContainer opening animation was added');
tooltipContainer.style.setProperty("opacity", "0");
}, 300);
input.focus();
const chatBox = shadowRoot.querySelector(".messages-container");
chatBox.scrollTop = chatBox.scrollHeight;
});
});
mainButton.addEventListener("mouseenter", () => {
tooltipContainer.classList.add("fade-in");
tooltipContainer.classList.remove("fade-out");
tooltipContainer.style.setProperty("opacity", "1");
tooltipContainer.style.setProperty("transform", "translateY(0)");
});
mainButton.addEventListener("mouseleave", () => {
tooltipContainer.classList.remove("fade-in");
tooltipContainer.classList.add("fade-out");
tooltipContainer.style.setProperty("transform", "translateY(300%)");
tooltipContainer.style.setProperty("opacity", "0");
});