Compare commits
2 commits
3f6bf53a7f
...
487c8aa3c1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
487c8aa3c1 | ||
|
|
35fd3a5d15 |
3 changed files with 173 additions and 50 deletions
|
|
@ -1761,15 +1761,29 @@ Vollständige Anleitung: [deploy/README.md](deploy/README.md).
|
||||||
|
|
||||||
> 🔧 Admin — erreichbar über den **⚙️-Button** im Web-Interface (nur für Admin-Nutzer sichtbar)
|
> 🔧 Admin — erreichbar über den **⚙️-Button** im Web-Interface (nur für Admin-Nutzer sichtbar)
|
||||||
|
|
||||||
Das Admin-Panel öffnet sich als Vollbild-Overlay über dem Chat. Es enthält sieben Tabs:
|
Das Admin-Panel öffnet sich als Vollbild-Overlay über dem Chat und gliedert sich in
|
||||||
|
**fünf Bereiche**. Bereiche mit mehreren Ansichten zeigen darunter eine Sub-Navigation:
|
||||||
|
|
||||||
#### Nutzer
|
| Bereich | Inhalt |
|
||||||
|
|---------|--------|
|
||||||
|
| **📊 Übersicht** | Start-Dashboard: Kennzahlen (Nutzer, Anfragen gesamt, Notfälle) + LLM-Backend/GPU; Direktsprünge |
|
||||||
|
| **👥 Nutzer** | Sub-Tabs *Verwalten* (anlegen/umbenennen/Token/löschen, Erinnerungen) und *Gespräche* (Transkripte) |
|
||||||
|
| **🚨 Notfälle** | protokollierte Notfall-Ereignisse |
|
||||||
|
| **🖥 System** | Sub-Tabs *Status* (inkl. LLM-Steuerung), *Metriken*, *Log* |
|
||||||
|
| **⚙ Konfiguration** | Sub-Tabs *Einstellungen* (Laufzeit-Config) und *Wörterbuch* (Aussprache) |
|
||||||
|
|
||||||
|
#### Übersicht
|
||||||
|
|
||||||
|
Beim Öffnen sichtbar: Kacheln mit Nutzerzahl, Anfragen gesamt, Notfall-Anzahl und
|
||||||
|
aktivem LLM-Backend/Modell, dazu die GPU-Auslastung und Schnell-Sprünge in die Bereiche.
|
||||||
|
|
||||||
|
#### Nutzer › Verwalten
|
||||||
|
|
||||||
Nutzer anlegen (Name eingeben → „Anlegen" → Token erscheint **einmalig** — sofort kopieren!),
|
Nutzer anlegen (Name eingeben → „Anlegen" → Token erscheint **einmalig** — sofort kopieren!),
|
||||||
umbenennen, Token zurücksetzen und löschen. Erinnerungen je Nutzer auf- und zuklappen,
|
umbenennen, Token zurücksetzen und löschen. Erinnerungen je Nutzer auf- und zuklappen,
|
||||||
neue Erinnerungen hinzufügen oder vorhandene löschen.
|
neue Erinnerungen hinzufügen oder vorhandene löschen.
|
||||||
|
|
||||||
#### Gespräche
|
#### Nutzer › Gespräche
|
||||||
|
|
||||||
Nutzerliste links → Session auswählen → Gesprächs-Transkript als Chat-Bubbles ansehen.
|
Nutzerliste links → Session auswählen → Gesprächs-Transkript als Chat-Bubbles ansehen.
|
||||||
|
|
||||||
|
|
@ -1778,7 +1792,7 @@ Nutzerliste links → Session auswählen → Gesprächs-Transkript als Chat-Bubb
|
||||||
Tabellarische Übersicht aller protokollierten Notfall-Ereignisse (Zeitpunkt, Nutzer,
|
Tabellarische Übersicht aller protokollierten Notfall-Ereignisse (Zeitpunkt, Nutzer,
|
||||||
Kategorie, Textausschnitt).
|
Kategorie, Textausschnitt).
|
||||||
|
|
||||||
#### Status
|
#### System › Status
|
||||||
|
|
||||||
Zeigt aktives Profil, Provider-Konfiguration, Laufzeit-Metriken und verfügbare Provider.
|
Zeigt aktives Profil, Provider-Konfiguration, Laufzeit-Metriken und verfügbare Provider.
|
||||||
Zusätzlich eine **LLM-Backend-Karte**: aktives Backend (Ollama/llama.cpp), Modell, ob
|
Zusätzlich eine **LLM-Backend-Karte**: aktives Backend (Ollama/llama.cpp), Modell, ob
|
||||||
|
|
@ -1795,27 +1809,24 @@ wechseln"** und **„Gateway neu starten"**.
|
||||||
|
|
||||||
Am Ende: **⬇ voice-assistant.db herunterladen** — lädt die SQLite-Datenbank als Backup.
|
Am Ende: **⬇ voice-assistant.db herunterladen** — lädt die SQLite-Datenbank als Backup.
|
||||||
|
|
||||||
#### Metriken
|
#### System › Metriken
|
||||||
|
|
||||||
Nutzungsstatistik je Nutzer (Anfragen, Einheiten, letzte Aktivität) als Tabelle
|
Nutzungsstatistik je Nutzer (Anfragen, Einheiten, letzte Aktivität) als Tabelle
|
||||||
und CSS-Balkendiagramm.
|
und CSS-Balkendiagramm.
|
||||||
|
|
||||||
#### Wörterbuch
|
#### Konfiguration › Wörterbuch
|
||||||
|
|
||||||
Aussprache-Lexikon direkt im Browser bearbeiten — kein Kommandozeilen-Skript nötig:
|
Aussprache-Lexikon direkt im Browser bearbeiten — kein Kommandozeilen-Skript nötig:
|
||||||
|
|
||||||
1. Sprache wählen (Dropdown: **Deutsch** / **Englisch**).
|
1. Sprache wählen (alle 8 Sprachen: de, en, fr, es, it, nl, ru, zh).
|
||||||
2. Sektion wählen: **Abkürzungen**, **Einheiten**, **Begriffe / Aussprache**.
|
2. Sektion wählen: **Abkürzungen**, **Einheiten**, **Begriffe / Aussprache**.
|
||||||
3. Vorhandene Einträge: Maus drüber → **✕** erscheint → löschen.
|
3. Eintrag bearbeiten: Zeile anklicken → lädt in die Felder unten (Button wird zu „Speichern").
|
||||||
4. Neuer Eintrag: Schlüssel + Ersetzung eingeben → **+ Hinzufügen**.
|
4. Eintrag löschen: Maus drüber → **✕**.
|
||||||
Die Änderung greift sofort (Server-Cache wird automatisch geleert).
|
5. Neuer Eintrag: Schlüssel + Ersetzung → **+ Hinzufügen**.
|
||||||
|
Die Liste wird nach jedem Speichern alphabetisch sortiert; Änderungen greifen sofort
|
||||||
|
(Server-Cache wird automatisch geleert). Vollständige Anleitung → § 6.5.4.
|
||||||
|
|
||||||
> **Andere Sprachen (fr, es, it, nl, ru, zh):** Das Wörterbuch-Tab zeigt aktuell nur
|
#### System › Log
|
||||||
> Deutsch und Englisch. Für andere Sprachen die YAML-Datei direkt editieren
|
|
||||||
> (`config/pronunciation.<lang>.yaml`) oder die REST-API nutzen — beides ohne Neustart
|
|
||||||
> möglich (API) bzw. mit Neustart (YAML direkt). Vollständige Anleitung → § 6.5.4.
|
|
||||||
|
|
||||||
#### Log
|
|
||||||
|
|
||||||
Zeigt den systemd-Journal-Log des `voice-assistant.service` live im Browser:
|
Zeigt den systemd-Journal-Log des `voice-assistant.service` live im Browser:
|
||||||
|
|
||||||
|
|
|
||||||
126
app/web/app.js
126
app/web/app.js
|
|
@ -673,43 +673,137 @@ async function adminFetch(url, method = "GET", body = null) {
|
||||||
// ---------- Panel öffnen / schließen ----------
|
// ---------- Panel öffnen / schließen ----------
|
||||||
function openAdminPanel() {
|
function openAdminPanel() {
|
||||||
$("#admin-panel").classList.remove("hidden");
|
$("#admin-panel").classList.remove("hidden");
|
||||||
switchAdminTab("users");
|
switchAdminTab("overview");
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#admin-close").addEventListener("click", () => $("#admin-panel").classList.add("hidden"));
|
$("#admin-close").addEventListener("click", () => $("#admin-panel").classList.add("hidden"));
|
||||||
|
|
||||||
// ---------- Tab-Umschalter ----------
|
// ---------- Tab-Struktur: 5 Bereiche, je mit 1+ Abschnitten ----------
|
||||||
function switchAdminTab(name) {
|
// section = vorhandener Content-Block (#admin-tab-<section>); Bereiche mit mehreren
|
||||||
document.querySelectorAll('[id^="admin-tab-"]').forEach((el) => el.classList.add("hidden"));
|
// Abschnitten bekommen eine Sub-Navigation.
|
||||||
$("#admin-tab-" + name).classList.remove("hidden");
|
const ADMIN_TABS = {
|
||||||
|
overview: [["overview", "Übersicht"]],
|
||||||
|
users: [["users", "Verwalten"], ["chat", "Gespräche"]],
|
||||||
|
emergency: [["emergency", "Notfälle"]],
|
||||||
|
system: [["status", "Status"], ["metrics", "Metriken"], ["log", "Log"]],
|
||||||
|
config: [["settings", "Einstellungen"], ["words", "Wörterbuch"]],
|
||||||
|
};
|
||||||
|
const SECTION_LOADER = {
|
||||||
|
overview: () => loadOverview(),
|
||||||
|
users: () => loadAdminUsers(),
|
||||||
|
chat: () => loadChatUsers(),
|
||||||
|
emergency: () => loadEmergencyEvents(),
|
||||||
|
status: () => loadStatus(),
|
||||||
|
metrics: () => loadMetrics(),
|
||||||
|
words: () => loadWoerterbuch(),
|
||||||
|
settings: () => loadSettings(),
|
||||||
|
log: () => {}, // Log: Nutzer verbindet manuell
|
||||||
|
};
|
||||||
|
|
||||||
document.querySelectorAll(".admin-tab").forEach((btn) => {
|
function _setTabActive(btn, active) {
|
||||||
const active = btn.dataset.tab === name;
|
|
||||||
btn.classList.toggle("border-blue-600", active);
|
btn.classList.toggle("border-blue-600", active);
|
||||||
btn.classList.toggle("text-blue-600", active);
|
btn.classList.toggle("text-blue-600", active);
|
||||||
btn.classList.toggle("dark:border-blue-400", active);
|
btn.classList.toggle("dark:border-blue-400", active);
|
||||||
btn.classList.toggle("dark:text-blue-400", active);
|
btn.classList.toggle("dark:text-blue-400", active);
|
||||||
btn.classList.toggle("border-transparent", !active);
|
btn.classList.toggle("border-transparent", !active);
|
||||||
btn.classList.toggle("text-slate-500", !active);
|
btn.classList.toggle("text-slate-500", !active);
|
||||||
});
|
}
|
||||||
|
|
||||||
if (name === "users") loadAdminUsers();
|
function showAdminSection(section, top) {
|
||||||
if (name === "chat") loadChatUsers();
|
document.querySelectorAll('[id^="admin-tab-"]').forEach((el) => el.classList.add("hidden"));
|
||||||
if (name === "emergency") loadEmergencyEvents();
|
const panel = $("#admin-tab-" + section);
|
||||||
if (name === "status") loadStatus();
|
if (panel) panel.classList.remove("hidden");
|
||||||
if (name === "metrics") loadMetrics();
|
// Sub-Tab markieren
|
||||||
if (name === "words") loadWoerterbuch();
|
document.querySelectorAll(".admin-subtab").forEach((b) => {
|
||||||
if (name === "log") { /* User muss manuell verbinden */ }
|
const active = b.dataset.sec === section;
|
||||||
if (name === "settings") loadSettings();
|
b.classList.toggle("bg-blue-600", active);
|
||||||
|
b.classList.toggle("text-white", active);
|
||||||
|
b.classList.toggle("border-blue-600", active);
|
||||||
|
b.classList.toggle("text-slate-600", !active);
|
||||||
|
b.classList.toggle("dark:text-slate-300", !active);
|
||||||
|
b.classList.toggle("border-slate-300", !active);
|
||||||
|
b.classList.toggle("dark:border-slate-600", !active);
|
||||||
|
});
|
||||||
|
const loader = SECTION_LOADER[section];
|
||||||
|
if (loader) loader();
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchAdminTab(top) {
|
||||||
|
const sections = ADMIN_TABS[top] || [[top, top]];
|
||||||
|
document.querySelectorAll(".admin-tab").forEach((btn) => _setTabActive(btn, btn.dataset.tab === top));
|
||||||
|
|
||||||
|
const subnav = $("#admin-subnav");
|
||||||
|
if (sections.length <= 1) {
|
||||||
|
subnav.classList.add("hidden");
|
||||||
|
subnav.innerHTML = "";
|
||||||
|
showAdminSection(sections[0][0], top);
|
||||||
|
} else {
|
||||||
|
subnav.classList.remove("hidden");
|
||||||
|
subnav.innerHTML = sections.map(([sec, label]) =>
|
||||||
|
`<button class="admin-subtab text-xs rounded-lg border px-3 py-1.5 whitespace-nowrap transition-colors border-slate-300 dark:border-slate-600 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700" data-sec="${sec}">${escHtml(label)}</button>`
|
||||||
|
).join("");
|
||||||
|
subnav.querySelectorAll(".admin-subtab").forEach((b) =>
|
||||||
|
b.addEventListener("click", () => showAdminSection(b.dataset.sec, top))
|
||||||
|
);
|
||||||
|
showAdminSection(sections[0][0], top);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll(".admin-tab").forEach((btn) =>
|
document.querySelectorAll(".admin-tab").forEach((btn) =>
|
||||||
btn.addEventListener("click", () => switchAdminTab(btn.dataset.tab))
|
btn.addEventListener("click", () => switchAdminTab(btn.dataset.tab))
|
||||||
);
|
);
|
||||||
|
$("#load-overview").addEventListener("click", loadOverview);
|
||||||
$("#load-emergency").addEventListener("click", loadEmergencyEvents);
|
$("#load-emergency").addEventListener("click", loadEmergencyEvents);
|
||||||
$("#load-status").addEventListener("click", loadStatus);
|
$("#load-status").addEventListener("click", loadStatus);
|
||||||
$("#load-metrics").addEventListener("click", loadMetrics);
|
$("#load-metrics").addEventListener("click", loadMetrics);
|
||||||
|
|
||||||
|
// ════════════════════════════════════════
|
||||||
|
// TAB: ÜBERSICHT (Dashboard)
|
||||||
|
// ════════════════════════════════════════
|
||||||
|
async function loadOverview() {
|
||||||
|
const c = $("#overview-content");
|
||||||
|
c.innerHTML = '<p class="text-sm text-slate-400">lade …</p>';
|
||||||
|
const [users, usage, emergencies, llm] = await Promise.all([
|
||||||
|
adminFetch("/api/admin/users"),
|
||||||
|
adminFetch("/api/admin/usage"),
|
||||||
|
adminFetch("/api/admin/emergency-events"),
|
||||||
|
adminFetch("/api/admin/llm/status"),
|
||||||
|
]);
|
||||||
|
const userCount = users ? users.length : "—";
|
||||||
|
const reqTotal = usage ? usage.reduce((s, u) => s + (u.total_requests || 0), 0) : "—";
|
||||||
|
const emCount = emergencies ? emergencies.length : "—";
|
||||||
|
const backend = llm ? ({ ollama: "Ollama", llamacpp: "llama.cpp" }[llm.backend] || llm.backend) : "—";
|
||||||
|
const gpu = (llm && llm.gpus && llm.gpus.length)
|
||||||
|
? llm.gpus.map((g) => `GPU ${g.index}: ${g.percent}%`).join(" · ") : "—";
|
||||||
|
|
||||||
|
const stat = (label, value, sub) => `
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400">${escHtml(label)}</p>
|
||||||
|
<p class="text-2xl font-semibold mt-1">${escHtml(value)}</p>
|
||||||
|
${sub ? `<p class="text-xs text-slate-400 mt-0.5 truncate">${escHtml(sub)}</p>` : ""}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const jump = (to, label) =>
|
||||||
|
`<button class="ov-jump text-sm rounded-lg border border-slate-300 dark:border-slate-600 px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors" data-jump="${to}">${escHtml(label)}</button>`;
|
||||||
|
|
||||||
|
c.innerHTML = `
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
${stat("Nutzer", userCount)}
|
||||||
|
${stat("Anfragen gesamt", reqTotal)}
|
||||||
|
${stat("Notfälle", emCount, (typeof emCount === "number" && emCount > 0) ? "protokolliert" : "")}
|
||||||
|
${stat("LLM-Backend", backend, llm ? llm.model : "")}
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
|
||||||
|
<h4 class="font-semibold text-sm mb-2">GPU-Auslastung</h4>
|
||||||
|
<p class="text-xs font-mono text-slate-600 dark:text-slate-300">${escHtml(gpu)}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
${jump("users", "→ Nutzer")} ${jump("system", "→ System")} ${jump("emergency", "→ Notfälle")}
|
||||||
|
</div>`;
|
||||||
|
c.querySelectorAll(".ov-jump").forEach((b) =>
|
||||||
|
b.addEventListener("click", () => switchAdminTab(b.dataset.jump)));
|
||||||
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
// TAB: NUTZER
|
// TAB: NUTZER
|
||||||
// ════════════════════════════════════════
|
// ════════════════════════════════════════
|
||||||
|
|
|
||||||
|
|
@ -38,21 +38,35 @@
|
||||||
class="h-9 w-9 grid place-items-center rounded-lg text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 text-xl leading-none transition-colors">✕</button>
|
class="h-9 w-9 grid place-items-center rounded-lg text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 text-xl leading-none transition-colors">✕</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation (5 logische Bereiche) -->
|
||||||
<nav class="shrink-0 flex border-b border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 overflow-x-auto">
|
<nav class="shrink-0 flex border-b border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 overflow-x-auto">
|
||||||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400" data-tab="users">👥 Nutzer</button>
|
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="overview">📊 Übersicht</button>
|
||||||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="chat">💬 Gespräche</button>
|
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="users">👥 Nutzer</button>
|
||||||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="emergency">🚨 Notfälle</button>
|
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="emergency">🚨 Notfälle</button>
|
||||||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="status">📡 Status</button>
|
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="system">🖥 System</button>
|
||||||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="metrics">📊 Metriken</button>
|
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="config">⚙ Konfiguration</button>
|
||||||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="words">🔤 Wörterbuch</button>
|
|
||||||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="log">📋 Log</button>
|
|
||||||
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="settings">⚙ Einstellungen</button>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Sub-Navigation (dynamisch, nur bei mehreren Abschnitten) -->
|
||||||
|
<div id="admin-subnav" class="hidden shrink-0 flex gap-1.5 px-3 py-2 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/40 overflow-x-auto"></div>
|
||||||
|
|
||||||
<!-- Tab Content Container -->
|
<!-- Tab Content Container -->
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
|
|
||||||
|
<!-- ── Übersicht (Dashboard) ── -->
|
||||||
|
<div id="admin-tab-overview" class="hidden h-full overflow-y-auto p-4">
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="font-semibold text-base">Übersicht</h3>
|
||||||
|
<button id="load-overview"
|
||||||
|
class="rounded-lg border border-slate-300 dark:border-slate-600 text-sm px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Aktualisieren</button>
|
||||||
|
</div>
|
||||||
|
<div id="overview-content" class="space-y-4">
|
||||||
|
<p class="text-sm text-slate-400">lade …</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── Tab: Nutzer ── -->
|
<!-- ── Tab: Nutzer ── -->
|
||||||
<div id="admin-tab-users" class="h-full overflow-y-auto p-4">
|
<div id="admin-tab-users" class="h-full overflow-y-auto p-4">
|
||||||
<div class="max-w-3xl mx-auto space-y-4">
|
<div class="max-w-3xl mx-auto space-y-4">
|
||||||
|
|
@ -290,16 +304,20 @@
|
||||||
|
|
||||||
<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)]">
|
<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)]">
|
||||||
<form id="prompt-form" class="flex items-center gap-2 w-full max-w-3xl mx-auto">
|
<form id="prompt-form" class="flex items-center gap-2 w-full max-w-3xl mx-auto">
|
||||||
<button type="button" id="mic" title="Mikrofon"
|
<button type="button" id="mic" title="Mikrofon" aria-label="Mikrofon"
|
||||||
class="shrink-0 h-11 w-11 grid place-items-center rounded-full bg-emerald-600 text-white text-lg">🎤</button>
|
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 …"
|
<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" />
|
class="flex-1 min-w-0 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"
|
<button type="submit" id="send" title="Senden" aria-label="Senden"
|
||||||
class="shrink-0 rounded-xl bg-blue-600 hover:bg-blue-700 text-white px-4 py-2.5 font-medium transition-colors">Senden</button>
|
class="shrink-0 h-11 w-11 grid place-items-center rounded-full bg-blue-600 hover:bg-blue-700 text-white transition-colors">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.27 4.36a.6.6 0 0 1 .82-.74l16.2 7.83a.6.6 0 0 1 0 1.08l-16.2 7.83a.6.6 0 0 1-.82-.74L6 12Zm0 0h7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<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>
|
<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>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/app.js?v=35"></script>
|
<script src="/app.js?v=37"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue