improved chat action handling, typing animation for chatbot
This commit is contained in:
parent
232d385787
commit
eaf0a7b7a4
2 changed files with 131 additions and 25 deletions
|
|
@ -53,6 +53,8 @@ type ChatMessage = {
|
|||
type: "bot" | "user",
|
||||
message: string,
|
||||
special?: boolean;
|
||||
id?: string;
|
||||
fresh?: boolean;
|
||||
}
|
||||
|
||||
const chat: Array<ChatMessage> = []
|
||||
|
|
@ -67,28 +69,46 @@ const timeouts: {
|
|||
message: null,
|
||||
}
|
||||
|
||||
let chatId: number = 0
|
||||
|
||||
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 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")
|
||||
chatBubble.classList.toggle("shimmer", special)
|
||||
chatBubble.id = id
|
||||
chatBubble.dataset.content = message
|
||||
chatContainer.classList.add("chat", "chat-start", "w-full")
|
||||
chatContainer.append(chatBubble)
|
||||
|
||||
return chatContainer
|
||||
}
|
||||
|
||||
|
||||
const templateUser = (message: string) => `
|
||||
<div class="chat chat-end w-full prose">
|
||||
<div class="chat chat-end w-full">
|
||||
<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))
|
||||
if (!chatItem.id) {
|
||||
chatItem.id = `chat-${chatId}`
|
||||
chatId++
|
||||
chatElement?.appendChild(templateBot(chatItem.id, chatItem.message, chatItem.special ?? false))
|
||||
typeBubble(chatItem.id, () => {
|
||||
})
|
||||
}
|
||||
break
|
||||
case "user":
|
||||
chatElement?.insertAdjacentHTML("beforeend", templateUser(chatItem.message))
|
||||
if (!chatItem.fresh) {
|
||||
chatItem.fresh = true
|
||||
chatElement?.insertAdjacentHTML("beforeend", templateUser(chatItem.message))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
})
|
||||
|
|
@ -98,6 +118,44 @@ function refreshChat() {
|
|||
})
|
||||
}
|
||||
|
||||
function typeBubble(id: string, callback: Function) {
|
||||
const element = document.getElementById(id)!
|
||||
const tmpDiv = document.createElement("div")
|
||||
tmpDiv.innerHTML = element.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)
|
||||
element.innerHTML = text
|
||||
charIndex++
|
||||
setTimeout(typeChar, typingSpeed)
|
||||
chatElement.scrollTo({
|
||||
top: chatElement.scrollHeight,
|
||||
})
|
||||
} else if (callback) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
typeChar()
|
||||
}
|
||||
|
||||
|
||||
function sendMessage() {
|
||||
const chatTextElement: HTMLTextAreaElement = document.querySelector("#chat-text")!
|
||||
const text = chatTextElement.value
|
||||
|
|
@ -128,14 +186,12 @@ function sendMessage() {
|
|||
}
|
||||
|
||||
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,
|
||||
message: string;
|
||||
priority: number;
|
||||
special?: boolean;
|
||||
}> = []
|
||||
let highestMatchingPriority = 0
|
||||
for (const response of sortedResponses) {
|
||||
|
|
@ -182,6 +238,10 @@ function getResponse(message: string) {
|
|||
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"] : ""
|
||||
}
|
||||
|
|
@ -194,8 +254,7 @@ function getResponse(message: string) {
|
|||
doorAction(action, door.id)
|
||||
})
|
||||
|
||||
message = response.response.
|
||||
replace("{door}", matchedDoors
|
||||
message = response.response.replace("{door}", matchedDoors
|
||||
.map(door => `<span class="badge">${door.label}</span>`)
|
||||
.join())
|
||||
}
|
||||
|
|
@ -203,9 +262,29 @@ function getResponse(message: string) {
|
|||
matchingResponses.push({
|
||||
message: message,
|
||||
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
|
||||
|
|
@ -221,12 +300,12 @@ function getResponse(message: string) {
|
|||
if (matchingResponses.length > 0) {
|
||||
const matchingMessages = matchingResponses
|
||||
.filter(response => response.priority == highestMatchingPriority)
|
||||
.map(response => response.message)
|
||||
|
||||
const chatMessages = matchingMessages.map(message => {
|
||||
const chatMessages = matchingMessages.map(response => {
|
||||
return {
|
||||
type: "bot" as ChatMessage["type"],
|
||||
message: message,
|
||||
message: response.message,
|
||||
special: response.special
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -240,10 +319,22 @@ function getResponse(message: string) {
|
|||
}
|
||||
|
||||
document.querySelector("body")?.insertAdjacentHTML("beforeend", templateInjectChat)
|
||||
const chatElement: HTMLDivElement = document.querySelector("#chat")!
|
||||
const chatWaitElement: HTMLDivElement = document.querySelector("#chat-waiting")!
|
||||
|
||||
function initChat() {
|
||||
chat.push({
|
||||
type: "bot",
|
||||
message: sortedResponses.at(-2)?.response ?? "",
|
||||
})
|
||||
refreshChat()
|
||||
}
|
||||
|
||||
let firstOpen = true
|
||||
|
||||
createIcons({
|
||||
nameAttr: "data-lucide",
|
||||
root: document.querySelector(".fab")!,
|
||||
root: document.querySelector("#chat-root")!,
|
||||
icons: {
|
||||
Stars,
|
||||
Send,
|
||||
|
|
@ -255,4 +346,17 @@ 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)
|
||||
}
|
||||
})
|
||||
|
|
@ -7,13 +7,13 @@ const responses: ResponseDictType[] = [
|
|||
priority: 0,
|
||||
},
|
||||
{
|
||||
trigger: ["ai", "ki"],
|
||||
trigger: ["ai", "ki", "llm"],
|
||||
response: "Künstliche Intelligenz? Ne, hier gibt's nur handgemachten Stuss! 🤖",
|
||||
priority: 0,
|
||||
},
|
||||
{
|
||||
trigger: ["miau"],
|
||||
response: "miau!",
|
||||
response: "miau! 😸",
|
||||
priority: 0,
|
||||
},
|
||||
{
|
||||
|
|
@ -22,7 +22,7 @@ const responses: ResponseDictType[] = [
|
|||
priority: 0,
|
||||
},
|
||||
{
|
||||
trigger: ["hi", "moin", "hallo", "servus", "gruess gott", "uwu"],
|
||||
trigger: ["hi", "moin", "hallo", "servus", "gruess gott"],
|
||||
response: "Moin! 👋",
|
||||
priority: 0,
|
||||
},
|
||||
|
|
@ -33,6 +33,7 @@ const responses: ResponseDictType[] = [
|
|||
unauthenticated: "Ich darf nur angemeldeten Wesen die Türen aufschließen 😕",
|
||||
unauthorized: "Ich darf nur berechtigten Wesen die Türen aufschließen 😕",
|
||||
unknownDoor: "Um eine Tür zu öffnen, musst du mir sagen, welche Tür du meinst 😕<br> Ich kenne folgende Türen: {doors}",
|
||||
noDoors: "Es sind gerade keine Türen verfügbar, die aufgeschlossen werden können 😕",
|
||||
},
|
||||
priority: 1000,
|
||||
special: "unlock",
|
||||
|
|
@ -44,6 +45,7 @@ const responses: ResponseDictType[] = [
|
|||
unauthenticated: "Ich darf nur angemeldeten Wesen die Türen zusperren 😕",
|
||||
unauthorized: "Ich darf nur berechtigten Wesen die Türen zusperren 😕",
|
||||
unknownDoor: "Um eine Tür zu schließen, musst du mir sagen, welche Tür du meinst 😕<br> Ich kenne folgende Türen: {doors}",
|
||||
noDoors: "Es sind gerade keine Türen verfügbar, die zugeschlossen werden können 😕",
|
||||
},
|
||||
priority: 999,
|
||||
special: "lock",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue