/*eslint no-unused-vars: ["error", {"varsIgnorePattern": "setupAiChatbot"}]*/
/* global showdown */
let ws = null;
let request_id = Math.floor(Math.random() * 100);;
const chatbotElementId = "ai-chatbot-element";
const chatbotInputElementId = "ai-chatbot-conversation-input-text";
const chatbotConversationElementId = "ai-chatbot-conversation-messages";
const chatbotMessageElementId = "ai-chatbot-message-bubble";
const chatbotAiMessageClassName = "chatbot-conversation-message flex quartergap";
const chatbotUserMessageClassName = "user-conversation-message";
const chatbotSystemMessageClassName = "chatbot-conversation-message flex quartergap";
const chatbotButtonElementId = "ai-chatbot-conversation-button";
const chatbotProgressIconId = "ai-chatbot-message-bubble-progress-icon";
let mdConverter = null;
let aiMessage = '';
function showAiLoader() {
const progressIcon = document.getElementById(chatbotProgressIconId);
if (progressIcon) {
progressIcon.style.display = 'block';
}
const chatbotInputElement = document.getElementById(chatbotInputElementId);
if (chatbotInputElement) {
chatbotInputElement.value = '';
chatbotInputElement.disabled = true;
chatbotInputElement.focus();
}
}
function hideAiLoader() {
const progressIcon = document.getElementById(chatbotProgressIconId);
if (progressIcon) {
progressIcon.style.display = 'none';
}
const chatbotInputElement = document.getElementById(chatbotInputElementId);
if (chatbotInputElement) {
chatbotInputElement.disabled = false;
chatbotInputElement.focus();
}
}
function isAtBottom(el, threshold = 8) {
return el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
}
function scrollToBottomSmooth(el) {
// rešpektuj prefers-reduced-motion
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const behavior = prefersReduced ? 'auto' : 'smooth';
el.scrollTo({ top: el.scrollHeight, behavior });
}
// pri pridávaní bubliny:
const chat = document.querySelector('.chatbot-conversation-messages');
// …po appendnutí novej správy:
if (chat && isAtBottom(chat)) {
scrollToBottomSmooth(chat); // animuje, len keď bol užívateľ „pri dne“
} else {
// voliteľne zobraz „Jump to latest“ button
}
function setupAiChatbot() {
const buttonSend = document.getElementById(chatbotButtonElementId);
const inputText = document.getElementById(chatbotInputElementId);
const chatbotElement = document.getElementById(chatbotElementId);
if (!buttonSend || !inputText || !chatbotElement) return;
mdConverter = ((Object.prototype.hasOwnProperty.call(window, 'showdown')) ? new showdown.Converter() : null);
if (!mdConverter) {
console.error("Showdown library not found, markdown conversion will not work.");
return;
}
buttonSend.addEventListener('click', () => {
if (inputText.value && (inputText.value.trim().length > 0)) {
sendChatbotRequest();
}
});
inputText.addEventListener('input', () => {
connectWebSocket();
}, { once: true });
inputText.addEventListener('keyup', (event) => {
if (event.defaultPrevented) {
return; // Do nothing if the event was already processed
}
if (event.key === "Enter") {
if (inputText.value && (inputText.value.trim().length > 0)) {
sendChatbotRequest();
event.preventDefault();
}
}
});
chatbotElement.addEventListener('pagehide', () => {
if (ws && ((ws.readyState === WebSocket.OPEN) || (ws.readyState === WebSocket.CONNECTING))) {
ws.close();
}
});
}
function appendChatbotMessage(message, msg_type = 'user') {
const chatbotConversationElement = document.getElementById(chatbotConversationElementId);
if (!chatbotConversationElement) {
console.error(`Element with ID ${chatbotConversationElementId} not found.`);
return;
}
let messageBubble = null;
let text = "";
if (msg_type === 'ai') {
const message_json = JSON.parse(message);
text = message_json.response;
let request_id_msg = message_json.request_id;
messageBubble = document.getElementById(`${chatbotMessageElementId}-${request_id_msg}`);
if (!messageBubble) {
messageBubble = document.createElement('div');
messageBubble.id = `${chatbotMessageElementId}-${request_id_msg}`;
messageBubble.className = chatbotAiMessageClassName;
let messageBubbleP = document.createElement('p');
messageBubble.appendChild(messageBubbleP);
chatbotConversationElement.appendChild(messageBubble);
}
} else {
text = message;
messageBubble = document.createElement('div');
}
if (!messageBubble) {
console.error(`Element with ID ${messageBubble} not found.`);
return;
}
if (msg_type === 'user') {
messageBubble.className = chatbotUserMessageClassName;
let messageBubbleP = document.createElement('p');
messageBubble.appendChild(messageBubbleP);
messageBubbleP.textContent = text;
chatbotConversationElement.appendChild(messageBubble);
} else if (msg_type === 'ai') {
let messageBubbleP = messageBubble.querySelector('p');
if (!messageBubbleP) {
messageBubbleP = document.createElement('p');
messageBubble.appendChild(messageBubbleP);
}
aiMessage += text;
messageBubbleP.innerHTML = `Boost: ${mdConverter.makeHtml(aiMessage)}`;
} else {
messageBubble.className = chatbotSystemMessageClassName;
let messageBubbleP = document.createElement('p');
messageBubble.appendChild(messageBubbleP);
messageBubbleP.textContent = text;
chatbotConversationElement.appendChild(messageBubble);
}
scrollToBottomSmooth(chatbotConversationElement);
}
// Function to create and manage the WebSocket connection
function connectWebSocket() {
const host = window.location.host;
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = `${protocol}://${host}/api/chatbot`;
if (!ws || ((ws.readyState !== WebSocket.OPEN) && (ws.readyState !== WebSocket.CONNECTING))) {
console.log(`Attempting to connect to ${wsUrl}...`);
ws = new WebSocket(wsUrl);
} else {
console.log(`WebSocket to ${wsUrl} is already connected.`);
return;
}
ws.onopen = () => {
console.log("WebSocket connection established.");
const chatbotButtonElement = document.getElementById(chatbotButtonElementId);
if (chatbotButtonElement) {
chatbotButtonElement.disabled = false;
}
};
ws.onmessage = (event) => {
console.log("AI message received:", event.data);
hideAiLoader();
try {
appendChatbotMessage(`${event.data}`, "ai");
} catch (e) {
console.error("Failed to parse message:", e);
appendChatbotMessage("Error: Invalid message received.", "system");
}
};
ws.onclose = () => {
const chatbotButtonElement = document.getElementById(chatbotButtonElementId);
if (chatbotButtonElement) {
chatbotButtonElement.disabled = true;
}
console.log("WebSocket connection closed.");
};
ws.onerror = (error) => {
const chatbotButtonElement = document.getElementById(chatbotButtonElementId);
if (chatbotButtonElement) {
chatbotButtonElement.disabled = true;
}
console.error("WebSocket error:", error);
appendChatbotMessage("Error: Connection with AI failed, please try to reload the page.", "system");
if (ws && ((ws.readyState === WebSocket.OPEN) || (ws.readyState === WebSocket.CONNECTING))) {
ws.close();
}
};
}
//Asynchronously waits for the WebSocket to open
function waitForWsOpen(socket) {
return new Promise((resolve, reject) => {
//If it's already open, resolve immediately
if (socket.readyState === WebSocket.OPEN) {
resolve(socket);
return;
}
//Set up the event listeners
socket.onopen = () => {
//Remove the listeners to prevent memory leaks
socket.onopen = null;
socket.onerror = null;
resolve(socket);
};
socket.onerror = (error) => {
//Remove the listeners
socket.onopen = null;
socket.onerror = null;
reject(new Error(`WebSocket failed to connect: ${error}`));
};
});
}
async function sendChatbotRequest() {
const chatbotConversationElement = document.getElementById(chatbotConversationElementId);
if (!chatbotConversationElement) {
console.error(`Element with ID ${chatbotConversationElementId} not found.`);
return;
}
const chatbotButtonElement = document.getElementById(chatbotButtonElementId);
if (!chatbotButtonElement) {
console.error(`Element with ID ${chatbotButtonElementId} not found.`);
return;
}
const inputText = document.getElementById(chatbotInputElementId);
if (!inputText || !inputText.value || (inputText.value.trim().length === 0)) {
console.warn("Invalid input text, can't be empty.");
inputText.focus();
return;
}
chatbotButtonElement.disabled = true;
if (!ws || (ws.readyState !== WebSocket.OPEN)) {
connectWebSocket();
try {
console.log('Awaiting WebSocket connection...');
//This is the unblocked wait using async/await
await waitForWsOpen(ws);
console.log('WebSocket is OPEN! ReadyState:', ws.readyState);
} catch (error) {
console.error('Failed to initialize connection:', error.message);
}
}
//Construct the message
request_id++;
const message = JSON.stringify({
input_text: inputText.value,
request_id: request_id,
});
let error_msg = "";
if (ws && (ws.readyState === WebSocket.OPEN)) {
ws.send(message);
} else {
error_msg = "not ready, please try to repeate your question.";
}
appendChatbotMessage(`${inputText.value}`, "user");
inputText.value = '';
//Show AI bubble immediately
let messageBubble = document.getElementById(`${chatbotMessageElementId}-${request_id}`);
if (!messageBubble) {
messageBubble = document.createElement('div');
messageBubble.id = `${chatbotMessageElementId}-${request_id}`;
messageBubble.className = chatbotAiMessageClassName;
let messageBubbleP = document.createElement('p');
let progressIcon = document.getElementById(chatbotProgressIconId);
progressIcon?.remove();
progressIcon = document.createElement('div');
progressIcon.id = chatbotProgressIconId;
progressIcon.className = "chatbot-progress-icon";
progressIcon.innerHTML = `
`;
messageBubble.appendChild(progressIcon);
messageBubble.appendChild(messageBubbleP);
aiMessage = error_msg;
messageBubbleP.innerHTML = `Boost: ${aiMessage}`;
chatbotConversationElement.appendChild(messageBubble);
showAiLoader();
scrollToBottomSmooth(chatbotConversationElement);
}
chatbotButtonElement.disabled = false;
}