All checks were successful
Build Container / Build Container (push) Successful in 4m25s
458 lines
No EOL
14 KiB
TypeScript
458 lines
No EOL
14 KiB
TypeScript
import {Bot, createIcons, UserRound, Send, Stars} from "lucide"
|
|
import {useTranslations} from "../i18n/utils.ts"
|
|
import removeDiacritics from "./removeDiacritics.ts"
|
|
|
|
export type ResponseDictType = {
|
|
trigger?: Array<string>;
|
|
startsWith?: Array<string>;
|
|
endsWith?: Array<string>;
|
|
response: string;
|
|
errors?: Record<string, string>
|
|
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 = `
|
|
<div class="chat-image avatar">
|
|
<div class="w-10 rounded-full ${lang === 'uwu' ? 'bg-transparent' : 'bg-primary'} flex items-center justify-center">
|
|
${lang === 'uwu' ? '<span class="text-4xl">🥺</span>' : '<i data-lucide="bot"></i>'}
|
|
</div>
|
|
</div>
|
|
`
|
|
|
|
const chatHeader = `
|
|
<div class="chat-header">
|
|
${t("chat.dooris")}
|
|
</div>
|
|
`
|
|
|
|
const chatAvatarUser = `
|
|
<div class="chat-image avatar">
|
|
<div class="w-10 rounded-full ${lang === 'uwu' ? 'bg-transparent' : 'bg-secondary'} flex items-center justify-center">
|
|
${lang === 'uwu' ? '<span class="text-4xl">🥺</span>' : '<i data-lucide="user-round"></i>'}
|
|
</div>
|
|
</div>
|
|
`
|
|
|
|
const chatHeaderUser = `
|
|
<div class="chat-header">
|
|
${t("chat.user")}
|
|
</div>
|
|
`
|
|
|
|
const chatWait = `
|
|
<div class="chat chat-start w-full prose" data-waiting>
|
|
${chatAvatar}
|
|
${chatHeader}
|
|
<div class="chat-bubble chat-bubble-primary whitespace-normal min-h-10">
|
|
<span class="loading loading-dots loading-md"></span>
|
|
</div>
|
|
</div>
|
|
`
|
|
|
|
const templateInjectChat = `
|
|
<details class="dropdown dropdown-top dropdown-end absolute right-2 bottom-2" id="chat-root">
|
|
<summary class="btn btn-lg btn-circle btn-primary me-2 mb-2">
|
|
<i data-lucide="stars"></i>
|
|
</summary>
|
|
<div class="dropdown-content card h-140 w-90 bg-base-300 p-4 mb-4" style="max-height: min(calc(100vh - 10rem), 50rem); max-width: calc(100vw - 1rem)">
|
|
<div class="w-full overflow-y-auto h-full prose leading-5 grow pb-4" id="chat"></div>
|
|
<div class="join mt-auto w-full">
|
|
<div class="w-full">
|
|
<label class="input join-item">
|
|
<input type="text" id="chat-text" />
|
|
</label>
|
|
</div>
|
|
<button class="btn btn-secondary join-item" id="send-chat">
|
|
<i data-lucide="send"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
`
|
|
|
|
type ChatMessage = {
|
|
type: "bot" | "user",
|
|
message: string,
|
|
special?: boolean;
|
|
id?: string;
|
|
fresh?: boolean;
|
|
}
|
|
|
|
const chat: Array<ChatMessage> = []
|
|
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) => `
|
|
<div class="chat chat-end w-full ps-10">
|
|
${chatAvatarUser}
|
|
${chatHeaderUser}
|
|
<div class="chat-bubble chat-bubble-secondary whitespace-normal" style="word-break: break-word;">${message}</div>
|
|
</div>`
|
|
|
|
const chatBubbleIds: Array<string> = []
|
|
|
|
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<string>) {
|
|
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<string> = []
|
|
|
|
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 = `<ul>`
|
|
for (let door of doors) {
|
|
if (msg.includes(door.label.toLowerCase())) {
|
|
matchedDoors.push(door)
|
|
}
|
|
doorListElement += `<li>${door.label}</li>`
|
|
}
|
|
doorListElement += `</ul>`
|
|
|
|
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 => `<span class="badge">${door.label}</span>`)
|
|
.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)
|
|
}
|
|
}) |