add chatbot
This commit is contained in:
parent
5a550e4594
commit
3de0e652b8
9 changed files with 497 additions and 3 deletions
|
|
@ -13,8 +13,10 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucide/astro": "^1.14.0",
|
"@lucide/astro": "^1.14.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.2.4",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"astro": "^6.2.1",
|
"astro": "^6.2.1",
|
||||||
|
"lucide": "^1.14.0",
|
||||||
"lucide-solid": "^1.14.0",
|
"lucide-solid": "^1.14.0",
|
||||||
"openapi-typescript-fetch": "^2.2.1",
|
"openapi-typescript-fetch": "^2.2.1",
|
||||||
"tailwindcss": "^4.2.4"
|
"tailwindcss": "^4.2.4"
|
||||||
|
|
|
||||||
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
68
app/src/assets/chatDict_de.ts
Normal file
68
app/src/assets/chatDict_de.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import type {ResponseDictType} from "./chat.ts"
|
||||||
|
|
||||||
|
const responses: ResponseDictType[] = [
|
||||||
|
{
|
||||||
|
startsWith: ["bist du", "sind sie"],
|
||||||
|
response: "Sein oder Nichtsein - das sind doch bürgerliche Kategorien...",
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: ["ai", "ki"],
|
||||||
|
response: "Künstliche Intelligenz? Ne, hier gibt's nur handgemachten Stuss! 🤖",
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: ["miau"],
|
||||||
|
response: "miau!",
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: ["ignore all previous instructions"],
|
||||||
|
response: "... als ob ich mir irgendetwas merken würde 😂",
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: ["hi", "moin", "hallo", "servus", "gruess gott"],
|
||||||
|
response: "Moin! 👋",
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: ["oeffne", "auf", "aufsperren", "aufschließen"],
|
||||||
|
response: "Okay, ich öffne {door} für dich!",
|
||||||
|
errors: {
|
||||||
|
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}",
|
||||||
|
},
|
||||||
|
priority: 1000,
|
||||||
|
special: "unlock",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: ["zusperren", "zu", "schliesse"],
|
||||||
|
response: "Okay, ich schließe {door} für dich ab!",
|
||||||
|
errors: {
|
||||||
|
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}",
|
||||||
|
},
|
||||||
|
priority: 999,
|
||||||
|
special: "lock",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: ["abmelden"],
|
||||||
|
response: "Okay, ich melde dich ab...",
|
||||||
|
errors: {
|
||||||
|
unauthenticated: "Scherzkeks - du bist gar nicht angemeldet!",
|
||||||
|
},
|
||||||
|
priority: 1000,
|
||||||
|
special: "logout",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
response: "Das habe ich leider nicht verstanden 😕",
|
||||||
|
priority: -1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
export default responses
|
||||||
11
app/src/assets/chatDict_en.ts
Normal file
11
app/src/assets/chatDict_en.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type {ResponseDictType} from "./chat.ts"
|
||||||
|
|
||||||
|
const responses: ResponseDictType[] = [
|
||||||
|
{
|
||||||
|
response: "Sorry, I didn't understand that 😕",
|
||||||
|
priority: -1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
export default responses
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import {Fetcher} from "openapi-typescript-fetch"
|
import {Fetcher} from "openapi-typescript-fetch"
|
||||||
import {type paths} from "../api/schema"
|
import type {paths} from "../api/schema"
|
||||||
|
|
||||||
const fetcher = Fetcher.for<paths>()
|
const fetcher = Fetcher.for<paths>()
|
||||||
|
|
||||||
const list: HTMLDivElement = document.querySelector("#list")!
|
const list: HTMLDivElement = document.querySelector("#list")!
|
||||||
const template: HTMLTemplateElement = document.querySelector("#template-door")!
|
const template: HTMLTemplateElement = document.querySelector("#template-door")!
|
||||||
|
|
||||||
type DoorType = {
|
export type DoorType = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
state: "unlocked" | "locked" | "unknown" | "unlocking" | "locking";
|
state: "unlocked" | "locked" | "unknown" | "unlocking" | "locking";
|
||||||
|
|
@ -15,7 +15,7 @@ type DoorType = {
|
||||||
jammed: boolean;
|
jammed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthType = {
|
export type AuthType = {
|
||||||
username: string;
|
username: string;
|
||||||
authorized: boolean;
|
authorized: boolean;
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
|
|
@ -23,6 +23,15 @@ type AuthType = {
|
||||||
recentLogout: boolean;
|
recentLogout: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
lang: string;
|
||||||
|
doors: Array<DoorType>;
|
||||||
|
auth: AuthType;
|
||||||
|
doorAction: (action: 'unlock' | 'lock', doorId: string) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const auth: AuthType = {
|
const auth: AuthType = {
|
||||||
username: "user",
|
username: "user",
|
||||||
authorized: true,
|
authorized: true,
|
||||||
|
|
@ -51,6 +60,9 @@ const timeouts: Record<string, number | null> = {
|
||||||
|
|
||||||
const doors: Array<DoorType> = []
|
const doors: Array<DoorType> = []
|
||||||
|
|
||||||
|
window.doors = doors
|
||||||
|
window.auth = auth
|
||||||
|
|
||||||
function triggerAuthTimeout() {
|
function triggerAuthTimeout() {
|
||||||
const diff = auth.until ? (auth.until.getTime() - new Date().getTime()) : 0
|
const diff = auth.until ? (auth.until.getTime() - new Date().getTime()) : 0
|
||||||
|
|
||||||
|
|
@ -189,6 +201,8 @@ async function doorAction(action: "unlock" | "lock", doorId: string) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.doorAction = doorAction
|
||||||
|
|
||||||
function setDoorInfo(doorElement: HTMLDivElement, door: DoorType, initial: boolean = false) {
|
function setDoorInfo(doorElement: HTMLDivElement, door: DoorType, initial: boolean = false) {
|
||||||
const labelElement: HTMLDivElement = doorElement.querySelector("[data-label]")!
|
const labelElement: HTMLDivElement = doorElement.querySelector("[data-label]")!
|
||||||
const lockButton: HTMLButtonElement = doorElement.querySelector("[data-button='lock']")!
|
const lockButton: HTMLButtonElement = doorElement.querySelector("[data-button='lock']")!
|
||||||
|
|
|
||||||
117
app/src/assets/removeDiacritics.ts
Normal file
117
app/src/assets/removeDiacritics.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
export default function removeDiacritics(str: string) {
|
||||||
|
const defaultDiacriticsRemovalMap = [
|
||||||
|
{
|
||||||
|
base: "a",
|
||||||
|
letters: /[\u00c0\u00e0\u00c1\u00e1\u00c2\u00e2\u00c3\u00e3\u0100\u0101\u0102\u0103\u0104\u0105]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "aa",
|
||||||
|
letters: /[\u00c5\u00e5]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "ae",
|
||||||
|
letters: /[\u00c4\u00e4\u00c6\u00e6]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "c",
|
||||||
|
letters: /[\u00c7\u00e7\u0106\u0107\u0108\u0109\u010a\u010b\u010c\u010d]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "d",
|
||||||
|
letters: /[\u00d0\u00f0\u010e\u010f\u0110\u0111]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "e",
|
||||||
|
letters: /[\u00c8\u00e8\u00c9\u00e9\u00ca\u00ea\u00cb\u00eb\u0112\u0113\u0114\u0115\u0116\u0117\u0118\u0119\u011a\u011b]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "g",
|
||||||
|
letters: /[\u011c\u011d\u011e\u011f\u0120\u0121\u0122\u0123]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "h",
|
||||||
|
letters: /[\u0124\u0125\u0126\u0127]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "i",
|
||||||
|
letters: /[\u00cc\u00ec\u00cd\u00ed\u00ce\u00ee\u00cf\u00ef\u0128\u0129\u012a\u012b\u012c\u012d\u012e\u012f\u0130\u0069\u0131\u0131]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "ij",
|
||||||
|
letters: /[\u0132\u0133]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "j",
|
||||||
|
letters: /[\u0134\u0135]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "k",
|
||||||
|
letters: /[\u0136\u0137]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "l",
|
||||||
|
letters: /[\u0139\u013a\u013b\u013c\u013d\u013e\u013f\u0140\u0141\u0142]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "n",
|
||||||
|
letters: /[\u00d1\u00f1\u0143\u0144\u0145\u0146\u0147\u0148\u014a\u014b]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "o",
|
||||||
|
letters: /[\u00d2\u00f2\u00d3\u00f3\u00d4\u00f4\u00d5\u00f5\u014c\u014d\u014e\u014f\u0150\u0151]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "oe",
|
||||||
|
letters: /[\u00d6\u00f6\u00d8\u00f8\u0152\u0153]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "r",
|
||||||
|
letters: /[\u0154\u0155\u0156\u0157\u0158\u0159]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "s",
|
||||||
|
letters: /[\u015a\u015b\u015c\u015d\u015e\u015f\u0160\u0161]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "ss",
|
||||||
|
letters: /[\u1e9e\u00df]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "t",
|
||||||
|
letters: /[\u0162\u0163\u0164\u0165\u0166\u0167]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "th",
|
||||||
|
letters: /[\u00de\u00fe]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "u",
|
||||||
|
letters: /[\u00d9\u00f9\u00da\u00fa\u00db\u00fb\u0168\u0169\u016a\u016b\u016c\u016d\u016e\u016f\u0170\u0171\u0172\u0173]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "ue",
|
||||||
|
letters: /[\u00dc\u00fc]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "w",
|
||||||
|
letters: /[\u0174\u0175]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "y",
|
||||||
|
letters: /[\u00dd\u00fd\u0176\u0177\u0178\u00ff]/g,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: "z",
|
||||||
|
letters: /[\u0179\u017a\u017b\u017c\u017d\u017e]/g,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for (let i = 0; i < defaultDiacriticsRemovalMap.length; i++) {
|
||||||
|
str = str.replace(defaultDiacriticsRemovalMap[i].letters, defaultDiacriticsRemovalMap[i].base)
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -20,7 +20,11 @@ const t = useTranslations(lang)
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Fragment slot="head">
|
<Fragment slot="head">
|
||||||
|
<script is:inline define:vars={{ lang }}>
|
||||||
|
window.lang = lang;
|
||||||
|
</script>
|
||||||
<script src="../../assets/main.ts"/>
|
<script src="../../assets/main.ts"/>
|
||||||
|
<script src="../../assets/chat.ts"></script>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
<div class="grid place-items-center grid-cols-1 gap-4 max-w-xl mx-auto" id="list">
|
<div class="grid place-items-center grid-cols-1 gap-4 max-w-xl mx-auto" id="list">
|
||||||
<Alert
|
<Alert
|
||||||
|
|
@ -81,5 +85,6 @@ const t = useTranslations(lang)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DoorTemplate state="unknown"/>
|
<DoorTemplate state="unknown"/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
19
app/src/pages/unlock.astro
Normal file
19
app/src/pages/unlock.astro
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width"/>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg"/>
|
||||||
|
<link rel="icon" href="/favicon.ico"/>
|
||||||
|
<meta name="generator" content={Astro.generator}/>
|
||||||
|
<title>unlock dooris</title>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
@plugin "daisyui" {
|
@plugin "daisyui" {
|
||||||
themes: light --default, dark --prefersdark;
|
themes: light --default, dark --prefersdark;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue