add chatbot
This commit is contained in:
parent
1dab2f7f4e
commit
c11eca7b05
9 changed files with 497 additions and 3 deletions
257
app/src/assets/chat.ts
Normal file
257
app/src/assets/chat.ts
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
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()
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue