257 lines
No EOL
7.7 KiB
TypeScript
257 lines
No EOL
7.7 KiB
TypeScript
import {createIcons, Send, Stars} from "lucide"
|
|
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 responses =
|
|
lang === "de"
|
|
? (await import("./chatDict_de")).default
|
|
: (await import("./chatDict_en")).default
|
|
|
|
|
|
const templateInjectChat = `
|
|
<div class="fab">
|
|
<div tabindex="0" role="button" class="btn btn-lg btn-circle btn-primary">
|
|
<i data-lucide="stars"></i>
|
|
</div>
|
|
|
|
<div class="card min-h-120 max-h-200 w-80 bg-base-300 p-4">
|
|
<div class="w-full overflow-auto h-full" id="chat"></div>
|
|
<div class="chat chat-start w-full hidden" id="chat-waiting">
|
|
<div class="chat-bubble chat-bubble-primary whitespace-normal">
|
|
<span class="loading loading-dots loading-md"></span>
|
|
</div>
|
|
</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>
|
|
</div>`
|
|
|
|
type ChatMessage = {
|
|
type: "bot" | "user",
|
|
message: string,
|
|
special?: boolean;
|
|
}
|
|
|
|
const chat: Array<ChatMessage> = []
|
|
const chatWaiting = {
|
|
value: false,
|
|
}
|
|
const timeouts: {
|
|
wait: number | null;
|
|
message: number | null;
|
|
} = {
|
|
wait: null,
|
|
message: null,
|
|
}
|
|
|
|
function refreshChat() {
|
|
const templateBot = (message: string) => `
|
|
<div class="chat chat-start w-full prose">
|
|
<div class="chat-bubble chat-bubble-primary whitespace-normal">${message}</div>
|
|
</div>`
|
|
|
|
const templateUser = (message: string) => `
|
|
<div class="chat chat-end w-full prose">
|
|
<div class="chat-bubble chat-bubble-secondary whitespace-normal">${message}</div>
|
|
</div>`
|
|
|
|
const chatElement: HTMLDivElement = document.querySelector("#chat")!
|
|
const chatWaitElement: HTMLDivElement = document.querySelector("#chat-waiting")!
|
|
|
|
chatElement.innerHTML = ""
|
|
chat.forEach(chatItem => {
|
|
switch (chatItem.type) {
|
|
case "bot":
|
|
chatElement?.insertAdjacentHTML("beforeend", templateBot(chatItem.message))
|
|
break
|
|
case "user":
|
|
chatElement?.insertAdjacentHTML("beforeend", templateUser(chatItem.message))
|
|
break
|
|
}
|
|
})
|
|
chatWaitElement.classList.toggle("hidden", !chatWaiting.value)
|
|
chatElement.scrollTo({
|
|
top: chatElement.scrollHeight,
|
|
})
|
|
}
|
|
|
|
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 sortedResponses = responses.sort(
|
|
(a, b) => b.priority - a.priority)
|
|
|
|
const msg = removeDiacritics(message.trim().normalize()).toLowerCase()
|
|
|
|
const matchingResponses: Array<{
|
|
message: string,
|
|
priority: number,
|
|
}> = []
|
|
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 (!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,
|
|
})
|
|
}
|
|
|
|
|
|
// 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)
|
|
.map(response => response.message)
|
|
|
|
const chatMessages = matchingMessages.map(message => {
|
|
return {
|
|
type: "bot" as ChatMessage["type"],
|
|
message: message,
|
|
}
|
|
})
|
|
|
|
chat.push(...chatMessages)
|
|
} else {
|
|
chat.push({
|
|
type: "bot",
|
|
message: sortedResponses.at(-1)?.response ?? "",
|
|
})
|
|
}
|
|
}
|
|
|
|
document.querySelector("body")?.insertAdjacentHTML("beforeend", templateInjectChat)
|
|
|
|
createIcons({
|
|
nameAttr: "data-lucide",
|
|
root: document.querySelector(".fab")!,
|
|
icons: {
|
|
Stars,
|
|
Send,
|
|
},
|
|
})
|
|
|
|
document.querySelector("#send-chat")!.addEventListener("click", sendMessage)
|
|
document.querySelector("#chat-text")!.addEventListener("keydown", (e: Event) => {
|
|
if ((e as KeyboardEvent).key === "Enter") {
|
|
sendMessage()
|
|
}
|
|
}) |