Add Chat feature #1
2 changed files with 131 additions and 25 deletions
improved chat action handling, typing animation for chatbot
commit
a2f9913150
|
|
@ -53,6 +53,8 @@ type ChatMessage = {
|
||||||
type: "bot" | "user",
|
type: "bot" | "user",
|
||||||
message: string,
|
message: string,
|
||||||
special?: boolean;
|
special?: boolean;
|
||||||
|
id?: string;
|
||||||
|
fresh?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chat: Array<ChatMessage> = []
|
const chat: Array<ChatMessage> = []
|
||||||
|
|
@ -67,28 +69,46 @@ const timeouts: {
|
||||||
message: null,
|
message: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let chatId: number = 0
|
||||||
|
|
||||||
function refreshChat() {
|
function refreshChat() {
|
||||||
const templateBot = (message: string) => `
|
const templateBot = (id: string, message: string, special: boolean) => {
|
||||||
<div class="chat chat-start w-full prose">
|
const chatContainer = document.createElement("div")
|
||||||
<div class="chat-bubble chat-bubble-primary whitespace-normal">${message}</div>
|
const chatBubble = document.createElement("div")
|
||||||
</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) => `
|
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 class="chat-bubble chat-bubble-secondary whitespace-normal">${message}</div>
|
||||||
</div>`
|
</div>`
|
||||||
|
|
||||||
const chatElement: HTMLDivElement = document.querySelector("#chat")!
|
|
||||||
const chatWaitElement: HTMLDivElement = document.querySelector("#chat-waiting")!
|
|
||||||
|
|
||||||
chatElement.innerHTML = ""
|
|
||||||
chat.forEach(chatItem => {
|
chat.forEach(chatItem => {
|
||||||
switch (chatItem.type) {
|
switch (chatItem.type) {
|
||||||
case "bot":
|
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
|
break
|
||||||
case "user":
|
case "user":
|
||||||
chatElement?.insertAdjacentHTML("beforeend", templateUser(chatItem.message))
|
if (!chatItem.fresh) {
|
||||||
|
chatItem.fresh = true
|
||||||
|
chatElement?.insertAdjacentHTML("beforeend", templateUser(chatItem.message))
|
||||||
|
}
|
||||||
|
|
||||||
break
|
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() {
|
function sendMessage() {
|
||||||
const chatTextElement: HTMLTextAreaElement = document.querySelector("#chat-text")!
|
const chatTextElement: HTMLTextAreaElement = document.querySelector("#chat-text")!
|
||||||
const text = chatTextElement.value
|
const text = chatTextElement.value
|
||||||
|
|
@ -128,14 +186,12 @@ function sendMessage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResponse(message: string) {
|
function getResponse(message: string) {
|
||||||
const sortedResponses = responses.sort(
|
|
||||||
(a, b) => b.priority - a.priority)
|
|
||||||
|
|
||||||
const msg = removeDiacritics(message.trim().normalize()).toLowerCase()
|
const msg = removeDiacritics(message.trim().normalize()).toLowerCase()
|
||||||
|
|
||||||
const matchingResponses: Array<{
|
const matchingResponses: Array<{
|
||||||
message: string,
|
message: string;
|
||||||
priority: number,
|
priority: number;
|
||||||
|
special?: boolean;
|
||||||
}> = []
|
}> = []
|
||||||
let highestMatchingPriority = 0
|
let highestMatchingPriority = 0
|
||||||
for (const response of sortedResponses) {
|
for (const response of sortedResponses) {
|
||||||
|
|
@ -182,6 +238,10 @@ function getResponse(message: string) {
|
||||||
message = response.errors ? response.errors["unknownDoor"].replace("{doors}", doorListElement) : ""
|
message = response.errors ? response.errors["unknownDoor"].replace("{doors}", doorListElement) : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (doors.length == 0) {
|
||||||
|
message = response.errors ? response.errors["noDoors"] : ""
|
||||||
|
}
|
||||||
|
|
||||||
if (!auth.authorized) {
|
if (!auth.authorized) {
|
||||||
message = response.errors ? response.errors["unauthorized"] : ""
|
message = response.errors ? response.errors["unauthorized"] : ""
|
||||||
}
|
}
|
||||||
|
|
@ -194,8 +254,7 @@ function getResponse(message: string) {
|
||||||
doorAction(action, door.id)
|
doorAction(action, door.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
message = response.response.
|
message = response.response.replace("{door}", matchedDoors
|
||||||
replace("{door}", matchedDoors
|
|
||||||
.map(door => `<span class="badge">${door.label}</span>`)
|
.map(door => `<span class="badge">${door.label}</span>`)
|
||||||
.join())
|
.join())
|
||||||
}
|
}
|
||||||
|
|
@ -203,9 +262,29 @@ function getResponse(message: string) {
|
||||||
matchingResponses.push({
|
matchingResponses.push({
|
||||||
message: message,
|
message: message,
|
||||||
priority: response.priority,
|
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
|
// stop searching for more matches
|
||||||
break
|
break
|
||||||
|
|
@ -221,12 +300,12 @@ function getResponse(message: string) {
|
||||||
if (matchingResponses.length > 0) {
|
if (matchingResponses.length > 0) {
|
||||||
const matchingMessages = matchingResponses
|
const matchingMessages = matchingResponses
|
||||||
.filter(response => response.priority == highestMatchingPriority)
|
.filter(response => response.priority == highestMatchingPriority)
|
||||||
.map(response => response.message)
|
|
||||||
|
|
||||||
const chatMessages = matchingMessages.map(message => {
|
const chatMessages = matchingMessages.map(response => {
|
||||||
return {
|
return {
|
||||||
type: "bot" as ChatMessage["type"],
|
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)
|
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({
|
createIcons({
|
||||||
nameAttr: "data-lucide",
|
nameAttr: "data-lucide",
|
||||||
root: document.querySelector(".fab")!,
|
root: document.querySelector("#chat-root")!,
|
||||||
icons: {
|
icons: {
|
||||||
Stars,
|
Stars,
|
||||||
Send,
|
Send,
|
||||||
|
|
@ -255,4 +346,17 @@ document.querySelector("#chat-text")!.addEventListener("keydown", (e: Event) =>
|
||||||
if ((e as KeyboardEvent).key === "Enter") {
|
if ((e as KeyboardEvent).key === "Enter") {
|
||||||
sendMessage()
|
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,
|
priority: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
trigger: ["ai", "ki"],
|
trigger: ["ai", "ki", "llm"],
|
||||||
response: "Künstliche Intelligenz? Ne, hier gibt's nur handgemachten Stuss! 🤖",
|
response: "Künstliche Intelligenz? Ne, hier gibt's nur handgemachten Stuss! 🤖",
|
||||||
priority: 0,
|
priority: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
trigger: ["miau"],
|
trigger: ["miau"],
|
||||||
response: "miau!",
|
response: "miau! 😸",
|
||||||
priority: 0,
|
priority: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -22,7 +22,7 @@ const responses: ResponseDictType[] = [
|
||||||
priority: 0,
|
priority: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
trigger: ["hi", "moin", "hallo", "servus", "gruess gott", "uwu"],
|
trigger: ["hi", "moin", "hallo", "servus", "gruess gott"],
|
||||||
response: "Moin! 👋",
|
response: "Moin! 👋",
|
||||||
priority: 0,
|
priority: 0,
|
||||||
},
|
},
|
||||||
|
|
@ -33,6 +33,7 @@ const responses: ResponseDictType[] = [
|
||||||
unauthenticated: "Ich darf nur angemeldeten Wesen die Türen aufschließen 😕",
|
unauthenticated: "Ich darf nur angemeldeten Wesen die Türen aufschließen 😕",
|
||||||
unauthorized: "Ich darf nur berechtigten 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}",
|
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,
|
priority: 1000,
|
||||||
special: "unlock",
|
special: "unlock",
|
||||||
|
|
@ -44,6 +45,7 @@ const responses: ResponseDictType[] = [
|
||||||
unauthenticated: "Ich darf nur angemeldeten Wesen die Türen zusperren 😕",
|
unauthenticated: "Ich darf nur angemeldeten Wesen die Türen zusperren 😕",
|
||||||
unauthorized: "Ich darf nur berechtigten 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}",
|
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,
|
priority: 999,
|
||||||
special: "lock",
|
special: "lock",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue