feat(web): Tailwind-Redesign-UI + Doku (Nachtrag zu 3470c0e)
Eigentliche UI-Dateien: neues Tailwind-Layout (index.html), Theme-Umschalter + Bubble-Stile + Mic-Status (app.js); README-Abschnitt aktualisiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3470c0e262
commit
7ebba4e755
3 changed files with 91 additions and 40 deletions
|
|
@ -392,9 +392,11 @@ PORT=8003 docker compose up # mit Docker
|
||||||
|
|
||||||
## Web-UI & Remote-Zugang
|
## Web-UI & Remote-Zugang
|
||||||
|
|
||||||
Das Gateway liefert unter `/` eine minimale **Web-Oberfläche** aus (`app/web/`, vanilla,
|
Das Gateway liefert unter `/` eine **Web-Oberfläche** aus (`app/web/`, Tailwind via CDN,
|
||||||
kein Build): Text-Eingabe + **Mikrofon-Button** (Aufnahme im Browser → `/ws/voice` →
|
kein Build): responsives, modernes Layout mit **Tag-/Nacht-Umschalter** (folgt automatisch
|
||||||
Antwort wird vorgelesen) und links ein Menü mit Identität/Logout/Admin.
|
dem System), Text-Eingabe + **Mikrofon-Button** (Aufnahme im Browser → `/ws/voice` → Antwort
|
||||||
|
wird vorgelesen), **Stimmen-Auswahl** (piper/chatterbox/cloud) sowie Identität/Logout und
|
||||||
|
(für Admins) eine Nutzerliste.
|
||||||
|
|
||||||
### Zugriff aus dem lokalen Netz (LAN)
|
### Zugriff aus dem lokalen Netz (LAN)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ function loadMe() {
|
||||||
logout.classList.remove("hidden");
|
logout.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
if (me.is_admin) {
|
if (me.is_admin) {
|
||||||
$("#admin").classList.remove("hidden");
|
$("#admin-toggle").classList.remove("hidden");
|
||||||
}
|
}
|
||||||
return me;
|
return me;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -83,9 +83,16 @@ function scrollToBottom() {
|
||||||
requestAnimationFrame(() => { messagesEl.scrollTop = messagesEl.scrollHeight; });
|
requestAnimationFrame(() => { messagesEl.scrollTop = messagesEl.scrollHeight; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BUBBLE = {
|
||||||
|
user: "ml-auto max-w-[80%] rounded-2xl rounded-br-sm bg-blue-600 px-4 py-2 text-white whitespace-pre-wrap",
|
||||||
|
assistant: "mr-auto max-w-[80%] rounded-2xl rounded-bl-sm border border-slate-200 bg-white px-4 py-2 whitespace-pre-wrap dark:border-slate-700 dark:bg-slate-800",
|
||||||
|
system: "mx-auto max-w-[90%] rounded-lg bg-amber-100 px-3 py-1.5 text-xs text-amber-800 dark:bg-amber-900/40 dark:text-amber-200",
|
||||||
|
emergency: "mx-auto max-w-[90%] rounded-lg bg-red-100 px-3 py-1.5 text-sm font-semibold text-red-800 dark:bg-red-900/40 dark:text-red-200",
|
||||||
|
};
|
||||||
|
|
||||||
function addMessage(role, text) {
|
function addMessage(role, text) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "msg " + role;
|
div.className = BUBBLE[role] || BUBBLE.assistant;
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
messagesEl.appendChild(div);
|
messagesEl.appendChild(div);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
@ -211,13 +218,15 @@ async function startRecording() {
|
||||||
sendVoice(bytes);
|
sendVoice(bytes);
|
||||||
};
|
};
|
||||||
mediaRecorder.start();
|
mediaRecorder.start();
|
||||||
micBtn.classList.add("recording");
|
micBtn.classList.remove("bg-emerald-600");
|
||||||
|
micBtn.classList.add("bg-red-600", "animate-pulse");
|
||||||
statusEl.textContent = "Aufnahme … (zum Stoppen erneut tippen)";
|
statusEl.textContent = "Aufnahme … (zum Stoppen erneut tippen)";
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopRecording() {
|
function stopRecording() {
|
||||||
if (mediaRecorder && mediaRecorder.state !== "inactive") mediaRecorder.stop();
|
if (mediaRecorder && mediaRecorder.state !== "inactive") mediaRecorder.stop();
|
||||||
micBtn.classList.remove("recording");
|
micBtn.classList.remove("bg-red-600", "animate-pulse");
|
||||||
|
micBtn.classList.add("bg-emerald-600");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendVoice(bytes) {
|
async function sendVoice(bytes) {
|
||||||
|
|
@ -239,6 +248,24 @@ micBtn.addEventListener("click", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#reload-users").addEventListener("click", loadUsers);
|
$("#reload-users").addEventListener("click", loadUsers);
|
||||||
|
$("#admin-toggle").addEventListener("click", () => $("#admin").classList.toggle("hidden"));
|
||||||
|
|
||||||
|
// Tag/Nacht: Umschalter (manuell) + automatisch dem System folgen, solange nichts gewählt.
|
||||||
|
const themeBtn = $("#theme-toggle");
|
||||||
|
const setThemeIcon = () =>
|
||||||
|
(themeBtn.textContent = document.documentElement.classList.contains("dark") ? "☀️" : "🌙");
|
||||||
|
themeBtn.addEventListener("click", () => {
|
||||||
|
const dark = document.documentElement.classList.toggle("dark");
|
||||||
|
localStorage.setItem("theme", dark ? "dark" : "light");
|
||||||
|
setThemeIcon();
|
||||||
|
});
|
||||||
|
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
||||||
|
if (!localStorage.getItem("theme")) {
|
||||||
|
document.documentElement.classList.toggle("dark", e.matches);
|
||||||
|
setThemeIcon();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setThemeIcon();
|
||||||
|
|
||||||
// Wenn die Bildschirmtastatur die sichtbare Hoehe aendert (iOS), unten bleiben.
|
// Wenn die Bildschirmtastatur die sichtbare Hoehe aendert (iOS), unten bleiben.
|
||||||
if (window.visualViewport) {
|
if (window.visualViewport) {
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,65 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="de">
|
<html lang="de" class="h-full">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<title>Voice Assistant</title>
|
<title>Voice Assistant</title>
|
||||||
<link rel="stylesheet" href="/style.css?v=5" />
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>tailwind.config = { darkMode: "class" };</script>
|
||||||
|
<script>
|
||||||
|
// Theme vor dem Rendern setzen (kein Flackern): manuell > System.
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var t = localStorage.getItem("theme");
|
||||||
|
if (t === "dark" || (!t && matchMedia("(prefers-color-scheme: dark)").matches))
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="h-[100dvh] flex flex-col bg-slate-50 text-slate-800 dark:bg-slate-900 dark:text-slate-100">
|
||||||
<div id="layout">
|
|
||||||
<aside id="menu">
|
<header class="shrink-0 h-14 flex items-center gap-3 px-4 border-b border-slate-200 dark:border-slate-700 bg-white/80 dark:bg-slate-800/80 backdrop-blur">
|
||||||
<h2>Voice Assistant</h2>
|
<h1 class="flex-1 truncate font-semibold text-base">Voice Assistant</h1>
|
||||||
<div id="identity" class="muted">lade …</div>
|
<select id="tts" title="Stimme"
|
||||||
<label id="tts-row">Stimme:
|
class="text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1.5">
|
||||||
<select id="tts">
|
<option value="">Schnell</option>
|
||||||
<option value="">Schnell (piper)</option>
|
<option value="chatterbox">Hohe Qualität</option>
|
||||||
<option value="chatterbox">Hohe Qualität (chatterbox)</option>
|
<option value="openrouter">Cloud</option>
|
||||||
<option value="openrouter">Cloud (OpenRouter)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
<button id="admin-toggle" title="Nutzer"
|
||||||
<div id="admin" class="hidden">
|
class="hidden h-9 w-9 grid place-items-center rounded-lg border border-slate-300 dark:border-slate-600">👥</button>
|
||||||
<h3>Admin</h3>
|
<button id="theme-toggle" title="Tag/Nacht"
|
||||||
<button id="reload-users">Nutzer laden</button>
|
class="h-9 w-9 grid place-items-center rounded-lg border border-slate-300 dark:border-slate-600">🌙</button>
|
||||||
<ul id="user-list"></ul>
|
</header>
|
||||||
<a href="/api/metrics" target="_blank" rel="noopener">Metriken</a>
|
|
||||||
|
<div class="shrink-0 flex items-center justify-between px-4 py-1 text-xs text-slate-500 dark:text-slate-400 border-b border-slate-200 dark:border-slate-700">
|
||||||
|
<span id="identity">lade …</span>
|
||||||
|
<a id="logout" class="hidden hover:underline" href="#">Abmelden</a>
|
||||||
</div>
|
</div>
|
||||||
<a id="logout" class="hidden" href="#">Abmelden</a>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main id="chat">
|
<div id="admin" class="hidden shrink-0 px-4 py-2 border-b border-slate-200 dark:border-slate-700 bg-slate-100/60 dark:bg-slate-800/60">
|
||||||
<div id="messages"></div>
|
<div class="flex items-center gap-2 mb-1 text-sm">
|
||||||
|
<span class="font-medium">Nutzer</span>
|
||||||
|
<button id="reload-users" class="text-xs rounded border border-slate-300 dark:border-slate-600 px-2 py-0.5">aktualisieren</button>
|
||||||
|
</div>
|
||||||
|
<ul id="user-list" class="text-xs text-slate-600 dark:text-slate-300 space-y-0.5 max-h-32 overflow-auto"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form id="prompt-form">
|
<main id="messages" class="flex-1 overflow-y-auto px-4 py-4 space-y-3 w-full max-w-3xl mx-auto"></main>
|
||||||
<button type="button" id="mic" title="Mikrofon" aria-label="Mikrofon">🎤</button>
|
|
||||||
<input id="prompt" type="text" placeholder="Nachricht eingeben …" autocomplete="off" />
|
<footer class="shrink-0 border-t border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-3 pt-3 pb-[calc(env(safe-area-inset-bottom)+0.6rem)]">
|
||||||
<button type="submit" id="send">Senden</button>
|
<form id="prompt-form" class="flex items-center gap-2 w-full max-w-3xl mx-auto">
|
||||||
|
<button type="button" id="mic" title="Mikrofon"
|
||||||
|
class="shrink-0 h-11 w-11 grid place-items-center rounded-full bg-emerald-600 text-white text-lg">🎤</button>
|
||||||
|
<input id="prompt" type="text" autocomplete="off" placeholder="Nachricht …"
|
||||||
|
class="flex-1 rounded-xl border border-slate-300 dark:border-slate-600 bg-transparent px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||||
|
<button type="submit" id="send"
|
||||||
|
class="shrink-0 rounded-xl bg-blue-600 hover:bg-blue-700 text-white px-4 py-2.5 font-medium">Senden</button>
|
||||||
</form>
|
</form>
|
||||||
<div id="status" class="muted"></div>
|
<div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div>
|
||||||
</main>
|
</footer>
|
||||||
</div>
|
|
||||||
<script src="/app.js?v=5"></script>
|
<script src="/app.js?v=6"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue