import {Bot, createIcons, UserRound, Send, Stars} from "lucide" import {useTranslations} from "../i18n/utils.ts" import removeDiacritics from "./removeDiacritics.ts" export type ResponseDictType = { trigger?: Array; startsWith?: Array; endsWith?: Array; response: string; errors?: Record priority: number; special?: string; } const lang = window.lang const doors = window.doors const auth = window.auth const doorAction = window.doorAction const t = useTranslations(lang) const responses = lang === "de" ? (await import("./chatDict_de")).default : lang === "uwu" ? (await import("./chatDict_uwu")).default : lang === "meow" ? (await import("./chatDict_meow")).default : (await import("./chatDict_en")).default const sortedResponses = responses.sort( (a, b) => b.priority - a.priority) const chatAvatar = `
${lang === 'uwu' ? '🥺' : ''}
` const chatHeader = `
${t("chat.dooris")}
` const chatAvatarUser = `
${lang === 'uwu' ? '🥺' : ''}
` const chatHeaderUser = `
${t("chat.user")}
` const chatWait = `
${chatAvatar} ${chatHeader}
` const templateInjectChat = ` ` type ChatMessage = { type: "bot" | "user", message: string, special?: boolean; id?: string; fresh?: boolean; } const chat: Array = [] const chatWaiting = { value: false, } const timeouts: { wait: number | null; message: number | null; } = { wait: null, message: null, } let chatId: number = 0 function refreshChat() { const templateBot = (id: string, message: string, special: boolean) => { const chatContainer = document.createElement("div") const chatBubble = document.createElement("div") chatBubble.classList.add("chat-bubble", "chat-bubble-primary", "whitespace-normal", "min-h-10", "break-[break-word]") chatBubble.style.wordBreak = "break-word" chatBubble.classList.toggle("shimmer", special) chatBubble.dataset.content = message chatContainer.classList.add("chat", "chat-start", "w-full", "overflow-visible", "hidden", "pe-10") chatContainer.id = id chatContainer.insertAdjacentHTML("afterbegin", chatAvatar) chatContainer.insertAdjacentHTML("afterbegin", chatHeader) chatContainer.append(chatBubble) return chatContainer } const templateUser = (message: string) => `
${chatAvatarUser} ${chatHeaderUser}
${message}
` const chatBubbleIds: Array = [] console.log(chat) chat.forEach(chatItem => { switch (chatItem.type) { case "bot": if (!chatItem.id) { chatItem.id = `chat-${chatId}` chatId++ chatElement?.appendChild(templateBot(chatItem.id, chatItem.message, chatItem.special ?? false)) chatBubbleIds.push(chatItem.id) } break case "user": if (!chatItem.fresh) { chatItem.fresh = true chatElement?.insertAdjacentHTML("beforeend", templateUser(chatItem.message)) } break } }) typeBubble(chatBubbleIds) chatElement.querySelector("[data-waiting]")?.remove() if (chatWaiting.value) { chatElement.insertAdjacentHTML("beforeend", chatWait) } chatElement.scrollTo({ top: chatElement.scrollHeight, }) refreshIcons() } function typeBubble(ids: Array) { if (!ids.length) return console.log(ids) const firstId = ids.slice(0, 1)[0] const restIds = ids.slice(1)! const chatContainer = document.getElementById(firstId)! chatContainer.classList.remove('hidden') const chatBubble: HTMLDivElement = chatContainer.querySelector('.chat-bubble')! const tmpDiv = document.createElement("div") tmpDiv.innerHTML = chatBubble.getAttribute("data-content") || "" const typingArray: Array = [] tmpDiv.childNodes.forEach(child => { // check if textNode if (child.nodeType === 3) { typingArray.push(...(child.textContent?.split("")) ?? []) } else { typingArray.push((child as Element).outerHTML) } }) let text = "" let charIndex = 0 const typingSpeed = 25 function typeChar() { if (charIndex < typingArray.length) { text += typingArray.at(charIndex) chatBubble.innerHTML = text charIndex++ setTimeout(typeChar, typingSpeed) chatElement.scrollTo({ top: chatElement.scrollHeight, }) } else if (restIds.length > 0) { typeBubble(restIds) } } typeChar() } function sendMessage() { const chatTextElement: HTMLTextAreaElement = document.querySelector("#chat-text")! const text = chatTextElement.value chatTextElement.value = "" if (text.trim() === "") return chat.push({ type: "user", message: text, }) refreshChat() timeouts.wait !== null && clearTimeout(timeouts.wait) timeouts.message !== null && clearTimeout(timeouts.message) timeouts.wait = setTimeout(() => { chatWaiting.value = true refreshChat() }, 500) timeouts.message = setTimeout(() => { chatWaiting.value = false getResponse(text) refreshChat() }, 2000) } function getResponse(message: string) { const msg = removeDiacritics(message.trim().normalize()).toLowerCase() const matchingResponses: Array<{ message: string; priority: number; special?: boolean; }> = [] let highestMatchingPriority = 0 for (const response of sortedResponses) { let match = false for (const trigger of response.trigger ?? []) { if (msg.includes(trigger)) match = true } for (const start of response.startsWith ?? []) { if (msg.startsWith(start)) match = true } for (const end of response.endsWith ?? []) { if (msg.endsWith(end)) match = true } if (match) { if (response.priority > highestMatchingPriority) highestMatchingPriority = response.priority if (response.special) { // clear previous matches while (matchingResponses.length) { matchingResponses.pop() } if (response.special === "unlock" || response.special === "lock") { let matchedDoors = [] const action = response.special let doorListElement = `
    ` for (let door of doors) { if (msg.includes(door.label.toLowerCase())) { matchedDoors.push(door) } doorListElement += `
  • ${door.label}
  • ` } doorListElement += `
` let message = "" if (matchedDoors.length == 0 || !auth.authorized || !auth.authenticated) { if (matchedDoors.length == 0) { message = response.errors ? response.errors["unknownDoor"].replace("{doors}", doorListElement) : "" } if (doors.length == 0) { message = response.errors ? response.errors["noDoors"] : "" } if (!auth.authorized) { message = response.errors ? response.errors["unauthorized"] : "" } if (!auth.authenticated) { message = response.errors ? response.errors["unauthenticated"] : "" } } else { matchedDoors.forEach(door => { doorAction(action, door.id) }) message = response.response.replace("{door}", matchedDoors .map(door => `${door.label}`) .join()) } matchingResponses.push({ message: message, priority: response.priority, special: true, }) } if (response.special === "uwu") { setTimeout(() => { window.location.href = "/uwu" }, 1000) matchingResponses.push({ message: response.response, priority: response.priority, special: true, }) } if (response.special === "meow") { setTimeout(() => { window.location.href = "/meow" }, 1000) matchingResponses.push({ message: response.response, priority: response.priority, special: true, }) } if (response.special === "logout") { if (auth.authenticated) { setTimeout(() => { window.location.href = "/auth/logout?next=/" }, 1000) matchingResponses.push({ message: response.response, priority: response.priority, special: true, }) } else { matchingResponses.push({ message: response.errors ? response.errors["unauthenticated"] : "", priority: response.priority, special: true, }) } } // stop searching for more matches break } else { matchingResponses.push({ message: response.response, priority: response.priority, }) } } } if (matchingResponses.length > 0) { const matchingMessages = matchingResponses .filter(response => response.priority == highestMatchingPriority) const chatMessages = matchingMessages.map(response => { return { type: "bot" as ChatMessage["type"], message: response.message, special: response.special, } }) chat.push(...chatMessages) } else { chat.push({ type: "bot", message: sortedResponses.at(-1)?.response ?? "", }) } } document.querySelector("body")?.insertAdjacentHTML("beforeend", templateInjectChat) const chatElement: HTMLDivElement = document.querySelector("#chat")! function initChat() { chat.push({ type: "bot", message: sortedResponses.at(-2)?.response ?? "", }) refreshChat() } let firstOpen = true function refreshIcons() { createIcons({ nameAttr: "data-lucide", root: document.querySelector("#chat-root")!, icons: { Stars, Send, UserRound, Bot, }, }) } refreshIcons() document.querySelector("#send-chat")!.addEventListener("click", sendMessage) document.querySelector("#chat-text")!.addEventListener("keydown", (e: Event) => { if ((e as KeyboardEvent).key === "Enter") { sendMessage() } }) document.querySelector("#chat-root")!.addEventListener("click", () => { if (firstOpen) { firstOpen = false chatWaiting.value = true refreshChat() timeouts.message = setTimeout(() => { chatWaiting.value = false initChat() }, 1000) } })