From 3de0e652b825dd7ed3344c65553b80b741657b49 Mon Sep 17 00:00:00 2001 From: kritzl Date: Thu, 14 May 2026 15:45:28 +0200 Subject: [PATCH 1/9] add chatbot --- app/package.json | 2 + app/src/assets/chat.ts | 257 +++++++++++++++++++++++++++++ app/src/assets/chatDict_de.ts | 68 ++++++++ app/src/assets/chatDict_en.ts | 11 ++ app/src/assets/main.ts | 20 ++- app/src/assets/removeDiacritics.ts | 117 +++++++++++++ app/src/pages/[lang]/index.astro | 5 + app/src/pages/unlock.astro | 19 +++ app/src/styles/global.css | 1 + 9 files changed, 497 insertions(+), 3 deletions(-) create mode 100644 app/src/assets/chat.ts create mode 100644 app/src/assets/chatDict_de.ts create mode 100644 app/src/assets/chatDict_en.ts create mode 100644 app/src/assets/removeDiacritics.ts create mode 100644 app/src/pages/unlock.astro diff --git a/app/package.json b/app/package.json index cbb027f..1c4ee0b 100644 --- a/app/package.json +++ b/app/package.json @@ -13,8 +13,10 @@ }, "dependencies": { "@lucide/astro": "^1.14.0", + "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.2.4", "astro": "^6.2.1", + "lucide": "^1.14.0", "lucide-solid": "^1.14.0", "openapi-typescript-fetch": "^2.2.1", "tailwindcss": "^4.2.4" diff --git a/app/src/assets/chat.ts b/app/src/assets/chat.ts new file mode 100644 index 0000000..5ed94f2 --- /dev/null +++ b/app/src/assets/chat.ts @@ -0,0 +1,257 @@ +import {createIcons, Send, Stars} from "lucide" +import removeDiacritics from "./removeDiacritics.ts" + +export type ResponseDictType = { + trigger?: Array; + startsWith?: Array; + endsWith?: Array; + response: string; + errors?: Record + 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 = ` +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+
` + +type ChatMessage = { + type: "bot" | "user", + message: string, + special?: boolean; +} + +const chat: Array = [] +const chatWaiting = { + value: false, +} +const timeouts: { + wait: number | null; + message: number | null; +} = { + wait: null, + message: null, +} + +function refreshChat() { + const templateBot = (message: string) => ` +
+
${message}
+
` + + const templateUser = (message: string) => ` +
+
${message}
+
` + + 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 = `
    ` + for (let door of doors) { + if (msg.includes(door.label.toLowerCase())) { + matchedDoors.push(door) + } + doorListElement += `
  • ${door.label}
  • ` + } + doorListElement += `
` + + 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 => `${door.label}`) + .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() + } +}) \ No newline at end of file diff --git a/app/src/assets/chatDict_de.ts b/app/src/assets/chatDict_de.ts new file mode 100644 index 0000000..e88b17e --- /dev/null +++ b/app/src/assets/chatDict_de.ts @@ -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 😕
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 😕
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 \ No newline at end of file diff --git a/app/src/assets/chatDict_en.ts b/app/src/assets/chatDict_en.ts new file mode 100644 index 0000000..43cc35d --- /dev/null +++ b/app/src/assets/chatDict_en.ts @@ -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 \ No newline at end of file diff --git a/app/src/assets/main.ts b/app/src/assets/main.ts index 5f5b5e3..f6fe9fe 100644 --- a/app/src/assets/main.ts +++ b/app/src/assets/main.ts @@ -1,12 +1,12 @@ import {Fetcher} from "openapi-typescript-fetch" -import {type paths} from "../api/schema" +import type {paths} from "../api/schema" const fetcher = Fetcher.for() const list: HTMLDivElement = document.querySelector("#list")! const template: HTMLTemplateElement = document.querySelector("#template-door")! -type DoorType = { +export type DoorType = { id: string; label: string; state: "unlocked" | "locked" | "unknown" | "unlocking" | "locking"; @@ -15,7 +15,7 @@ type DoorType = { jammed: boolean; } -type AuthType = { +export type AuthType = { username: string; authorized: boolean; authenticated: boolean; @@ -23,6 +23,15 @@ type AuthType = { recentLogout: boolean; } +declare global { + interface Window { + lang: string; + doors: Array; + auth: AuthType; + doorAction: (action: 'unlock' | 'lock', doorId: string) => void; + } +} + const auth: AuthType = { username: "user", authorized: true, @@ -51,6 +60,9 @@ const timeouts: Record = { const doors: Array = [] +window.doors = doors +window.auth = auth + function triggerAuthTimeout() { 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) { const labelElement: HTMLDivElement = doorElement.querySelector("[data-label]")! const lockButton: HTMLButtonElement = doorElement.querySelector("[data-button='lock']")! diff --git a/app/src/assets/removeDiacritics.ts b/app/src/assets/removeDiacritics.ts new file mode 100644 index 0000000..fe578d7 --- /dev/null +++ b/app/src/assets/removeDiacritics.ts @@ -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 +} + + + diff --git a/app/src/pages/[lang]/index.astro b/app/src/pages/[lang]/index.astro index b4cf578..273eaf9 100644 --- a/app/src/pages/[lang]/index.astro +++ b/app/src/pages/[lang]/index.astro @@ -20,7 +20,11 @@ const t = useTranslations(lang) +
+
diff --git a/app/src/pages/unlock.astro b/app/src/pages/unlock.astro new file mode 100644 index 0000000..4fd9f47 --- /dev/null +++ b/app/src/pages/unlock.astro @@ -0,0 +1,19 @@ + + + + + + + + + + unlock dooris + + + + + + + diff --git a/app/src/styles/global.css b/app/src/styles/global.css index 8ab655c..456d557 100644 --- a/app/src/styles/global.css +++ b/app/src/styles/global.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@plugin "@tailwindcss/typography"; @plugin "daisyui" { themes: light --default, dark --prefersdark; } -- 2.51.2 From 9f6535ace59bca7df00e4770d220f2053707f636 Mon Sep 17 00:00:00 2001 From: kritzl Date: Thu, 14 May 2026 17:06:18 +0200 Subject: [PATCH 2/9] update dict --- app/pnpm-lock.yaml | 42 +++++++++++++++++++++++++++++++++++ app/src/assets/chatDict_de.ts | 9 ++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index f4942c3..b99dec1 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -11,12 +11,18 @@ importers: '@lucide/astro': specifier: ^1.14.0 version: 1.14.0(astro@6.2.1(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@6.0.3)) + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.2.4) '@tailwindcss/vite': specifier: ^4.2.4 version: 4.2.4(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)) astro: specifier: ^6.2.1 version: 6.2.1(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.2)(typescript@6.0.3) + lucide: + specifier: ^1.14.0 + version: 1.16.0 lucide-solid: specifier: ^1.14.0 version: 1.14.0(solid-js@1.9.12) @@ -669,6 +675,11 @@ packages: resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} engines: {node: '>= 20'} + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.2.4': resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==} peerDependencies: @@ -807,6 +818,11 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -1125,6 +1141,9 @@ packages: peerDependencies: solid-js: ^1.4.7 + lucide@1.16.0: + resolution: {integrity: sha512-20QvduCJTB7e7K9WVvoLBuKPsYZ8d6ptwe9PIdTFiZmjVTPdFWQreBNyNCM9QQtGnqHR0PLiEQdxyhYsL1LdoA==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1367,6 +1386,10 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss@8.5.12: resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} @@ -1656,6 +1679,9 @@ packages: uri-js-replace@1.0.1: resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -2238,6 +2264,11 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 + '@tailwindcss/typography@0.5.19(tailwindcss@4.2.4)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.2.4 + '@tailwindcss/vite@4.2.4(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0))': dependencies: '@tailwindcss/node': 4.2.4 @@ -2443,6 +2474,8 @@ snapshots: css-what@6.2.2: {} + cssesc@3.0.0: {} + csso@5.0.5: dependencies: css-tree: 2.2.1 @@ -2779,6 +2812,8 @@ snapshots: dependencies: solid-js: 1.9.12 + lucide@1.16.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3204,6 +3239,11 @@ snapshots: pluralize@8.0.0: {} + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss@8.5.12: dependencies: nanoid: 3.3.11 @@ -3543,6 +3583,8 @@ snapshots: uri-js-replace@1.0.1: {} + util-deprecate@1.0.2: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/app/src/assets/chatDict_de.ts b/app/src/assets/chatDict_de.ts index e88b17e..b651efb 100644 --- a/app/src/assets/chatDict_de.ts +++ b/app/src/assets/chatDict_de.ts @@ -22,7 +22,7 @@ const responses: ResponseDictType[] = [ priority: 0, }, { - trigger: ["hi", "moin", "hallo", "servus", "gruess gott"], + trigger: ["hi", "moin", "hallo", "servus", "gruess gott", "uwu"], response: "Moin! 👋", priority: 0, }, @@ -58,9 +58,14 @@ const responses: ResponseDictType[] = [ special: "logout", }, + + { + response: "Moin!
Wie kann ich dir behilflich sein? 🤓", + priority: -1, + }, { response: "Das habe ich leider nicht verstanden 😕", - priority: -1, + priority: -2, }, ] -- 2.51.2 From ff05a8c2ee3c1d8f2da9eea2451ba34c3474ca02 Mon Sep 17 00:00:00 2001 From: kritzl Date: Fri, 15 May 2026 11:36:15 +0200 Subject: [PATCH 3/9] use details tag for chat --- app/src/assets/chat.ts | 43 +++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/app/src/assets/chat.ts b/app/src/assets/chat.ts index 5ed94f2..e4dc2ba 100644 --- a/app/src/assets/chat.ts +++ b/app/src/assets/chat.ts @@ -23,30 +23,31 @@ const responses = const templateInjectChat = ` -
-
- -
- -
-
-