Neue Karte in Admin › System › Status: ein Dropdown kuratierter, erprobter
Modelle, das Provider + Modell in einem Klick als Laufzeit-Override umstellt
(wirkt sofort, kein Neustart).
- Presets in config/models.yaml (Label, optional note, set:{key:value} aus
RUNTIME_SETTABLE, default:true = Config-Standard). Loader app/model_presets.py
(nur erlaubte LLM-Keys: default_llm_provider, openrouter_llm_model,
local_llm_model).
- Endpunkte: GET /api/admin/models (Presets + aktuell aktives LLM + active_index),
POST /api/admin/models/select (Default -> Overrides löschen, sonst Override-Keys
setzen) — nutzt set_/delete_config_override + invalidate_cache, mit Audit-Log.
- Frontend: Karte zeigt Dropdown + „Aktiv: <provider> · <modell>"; Auswahl wirkt
sofort und lädt den Status neu. Bump app.js v=52 -> v=53.
- Startliste: Mistral Small 24B (Standard), DeepSeek V4 Flash/Pro, Ollama Gemma 3.
Tests: Laden/Default-aktiv/Select-setzt-Override/Default-löscht/404/Auth.
263 Offline-Tests grün. Live via Headless-Chrome verifiziert (Auswahl ->
Override gesetzt, Provider-Karte spiegelt das Modell).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
416 lines
26 KiB
HTML
416 lines
26 KiB
HTML
<!doctype html>
|
|
<html lang="de" class="h-full">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
<title>Voice Assistant</title>
|
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=1" />
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png?v=1" />
|
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png?v=1" />
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>tailwind.config = { darkMode: "class" };</script>
|
|
<style>
|
|
/* Dropdown-Popup (<option>) hat sonst Browser-Default-Weiß -> im Dark-Mode
|
|
helle Schrift auf hellem Grund = unlesbar. Farben explizit setzen. */
|
|
select option { background-color: #ffffff; color: #1e293b; } /* slate-800 */
|
|
.dark select option { background-color: #1e293b; color: #f1f5f9; } /* slate-100 */
|
|
</style>
|
|
<script>
|
|
(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>
|
|
<body class="h-[100dvh] flex flex-col bg-slate-50 text-slate-800 dark:bg-slate-900 dark:text-slate-100">
|
|
|
|
<!-- ═══════════════════ ADMIN PANEL OVERLAY ═══════════════════ -->
|
|
<div id="admin-panel" class="hidden fixed inset-0 z-50 flex flex-col bg-slate-50 dark:bg-slate-900">
|
|
|
|
<!-- Admin Header -->
|
|
<header class="shrink-0 h-14 flex items-center gap-3 px-4 border-b border-slate-200 dark:border-slate-700 bg-white/90 dark:bg-slate-800/90 backdrop-blur">
|
|
<span class="text-lg">⚙️</span>
|
|
<h2 class="flex-1 font-semibold text-base">Admin-Bereich</h2>
|
|
<button id="admin-close" title="Schließen"
|
|
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>
|
|
|
|
<!-- 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">
|
|
<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="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="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="config">⚙ Konfiguration</button>
|
|
</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 -->
|
|
<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 ── -->
|
|
<div id="admin-tab-users" class="h-full overflow-y-auto p-4">
|
|
<div class="max-w-3xl mx-auto space-y-4">
|
|
|
|
<!-- Nutzer anlegen -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
|
|
<h3 class="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">Nutzer anlegen</h3>
|
|
<form id="create-user-form" class="flex gap-2">
|
|
<input id="new-user-name" type="text" placeholder="Anzeigename" required
|
|
class="flex-1 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
|
<button type="submit"
|
|
class="rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm px-4 py-2 font-medium transition-colors">Anlegen</button>
|
|
</form>
|
|
<div id="new-token-box" class="hidden mt-3 rounded-lg bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800 p-3">
|
|
<p class="text-xs text-emerald-700 dark:text-emerald-400 mb-1 font-medium">Login-Link (nur einmal sichtbar — jetzt kopieren & an den Nutzer geben!):</p>
|
|
<code id="new-token-value" class="text-xs font-mono break-all text-emerald-800 dark:text-emerald-200 select-all"></code>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nutzerliste -->
|
|
<div id="admin-user-cards" class="space-y-3">
|
|
<p class="text-sm text-slate-400 text-center py-4">lade …</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Tab: Gespräche ── -->
|
|
<div id="admin-tab-chat" class="hidden h-full flex overflow-hidden">
|
|
<!-- Linke Spalte: Nutzerliste -->
|
|
<div class="w-48 md:w-60 shrink-0 border-r border-slate-200 dark:border-slate-700 flex flex-col">
|
|
<div class="px-3 py-2.5 border-b border-slate-200 dark:border-slate-700 bg-slate-100/60 dark:bg-slate-800/60">
|
|
<p class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">Nutzer</p>
|
|
</div>
|
|
<ul id="chat-user-list" class="flex-1 overflow-y-auto divide-y divide-slate-100 dark:divide-slate-800/60"></ul>
|
|
</div>
|
|
<!-- Rechte Spalte: Sessions & Transkript -->
|
|
<div id="chat-right-panel" class="flex-1 overflow-y-auto">
|
|
<p class="p-6 text-sm text-slate-400">← Nutzer auswählen</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Tab: Notfälle ── -->
|
|
<div id="admin-tab-emergency" class="hidden h-full overflow-y-auto p-4">
|
|
<div class="max-w-4xl mx-auto">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="font-semibold text-base">Notrufe (vom Nutzer ausgelöst)</h3>
|
|
<button id="load-emergency"
|
|
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="emergency-content">
|
|
<p class="text-sm text-slate-400">lade …</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Tab: Status ── -->
|
|
<div id="admin-tab-status" 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">System-Status</h3>
|
|
<button id="load-status"
|
|
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="status-content" class="space-y-4">
|
|
<p class="text-sm text-slate-400">lade …</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Tab: Metriken ── -->
|
|
<div id="admin-tab-metrics" 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">Nutzungsstatistik</h3>
|
|
<button id="load-metrics"
|
|
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="metrics-content">
|
|
<p class="text-sm text-slate-400">lade …</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Tab: Wörterbuch ── -->
|
|
<div id="admin-tab-words" 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 flex-wrap gap-2">
|
|
<h3 class="font-semibold text-base">Aussprache-Lexikon</h3>
|
|
<div class="flex items-center gap-2">
|
|
<select id="words-lang"
|
|
class="text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1.5">
|
|
<option value="de">Deutsch (de)</option>
|
|
<option value="en">Englisch (en)</option>
|
|
<option value="fr">Französisch (fr)</option>
|
|
<option value="es">Spanisch (es)</option>
|
|
<option value="it">Italienisch (it)</option>
|
|
<option value="pt">Portugiesisch (pt)</option>
|
|
<option value="pl">Polnisch (pl)</option>
|
|
<option value="ar">Arabisch (ar)</option>
|
|
<option value="ru">Russisch (ru)</option>
|
|
<option value="zh">Chinesisch (zh)</option>
|
|
</select>
|
|
<button id="load-words"
|
|
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>
|
|
<p class="text-xs text-slate-400 mb-4">Wirkt nur bei lokalem TTS (Piper, Stufe „full"). <strong>Abkürzungen</strong> ersetzen ganze Token. <strong>Einheiten</strong> wirken nur direkt nach einer Zahl. <strong>Begriffe</strong> für Eigennamen, Fachwörter, Aussprache-Korrekturen.</p>
|
|
<div id="words-content" class="space-y-6">
|
|
<p class="text-sm text-slate-400">lade …</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Tab: Wartung ── -->
|
|
<div id="admin-tab-maintenance" class="hidden h-full overflow-y-auto p-4">
|
|
<div class="max-w-3xl mx-auto">
|
|
<h3 class="font-semibold text-base mb-4">Wartung</h3>
|
|
<div id="maintenance-content" class="space-y-4">
|
|
<p class="text-sm text-slate-400">lade …</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Tab: Log ── -->
|
|
<div id="admin-tab-log" class="hidden h-full flex flex-col">
|
|
<div class="shrink-0 flex items-center gap-3 px-4 py-2.5 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/60">
|
|
<button id="log-connect"
|
|
class="rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white text-xs px-3 py-1.5 font-medium transition-colors">▶ Verbinden</button>
|
|
<button id="log-disconnect" disabled
|
|
class="rounded-lg border border-slate-300 dark:border-slate-600 text-xs px-3 py-1.5 opacity-40 transition-colors">■ Trennen</button>
|
|
<span id="log-status" class="text-xs text-slate-400">getrennt</span>
|
|
<button id="log-clear" class="ml-auto text-xs text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">Leeren</button>
|
|
</div>
|
|
<div id="log-output"
|
|
class="flex-1 overflow-y-auto bg-slate-950 text-emerald-300 font-mono text-xs p-4 leading-relaxed whitespace-pre-wrap">
|
|
<span class="text-slate-500">— Verbinden um Log zu starten —</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Tab: Einstellungen ── -->
|
|
<div id="admin-tab-settings" class="hidden h-full overflow-y-auto p-4">
|
|
<div class="max-w-3xl mx-auto">
|
|
<div class="flex items-center justify-between mb-2 flex-wrap gap-2">
|
|
<h3 class="font-semibold text-base">Laufzeit-Einstellungen</h3>
|
|
<button id="load-settings"
|
|
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>
|
|
<p class="text-xs text-slate-400 mb-4">Änderungen wirken sofort — kein Server-Neustart nötig. „Zurücksetzen" entfernt den Override und der .env-Wert gilt wieder.</p>
|
|
<div id="settings-content" class="space-y-2">
|
|
<p class="text-sm text-slate-400">lade …</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /Tab Content Container -->
|
|
</div><!-- /Admin Panel -->
|
|
|
|
|
|
<!-- ═══════════════════ CHAT INTERFACE ═══════════════════ -->
|
|
<header class="shrink-0 h-14 flex items-center gap-2 px-3 border-b border-slate-200 dark:border-slate-700 bg-white/80 dark:bg-slate-800/80 backdrop-blur">
|
|
<img src="/favicon.svg?v=1" alt="" class="w-7 h-7 shrink-0" />
|
|
<h1 class="flex-1 truncate font-semibold text-base">Voice Assistant</h1>
|
|
<button type="button" id="emergency-btn" title="Hilfe rufen" aria-label="Hilfe rufen"
|
|
class="shrink-0 h-8 w-8 grid place-items-center rounded-lg bg-red-600 hover:bg-red-700 text-white font-extrabold text-sm leading-none shadow-sm transition-colors disabled:opacity-100">SOS</button>
|
|
<select id="lang-sel" title="Sprache"
|
|
class="text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1.5">
|
|
<option value="de">🇩🇪 DE</option>
|
|
<option value="en">🇬🇧 EN</option>
|
|
<option value="fr">🇫🇷 FR</option>
|
|
<option value="es">🇪🇸 ES</option>
|
|
<option value="it">🇮🇹 IT</option>
|
|
<option value="pt">🇧🇷 PT</option>
|
|
<option value="pl">🇵🇱 PL</option>
|
|
<option value="ar">🇸🇦 AR</option>
|
|
<option value="ru">🇷🇺 RU</option>
|
|
<option value="zh">🇨🇳 ZH</option>
|
|
</select>
|
|
<!-- „Neues Gespräch" in der Kopfzeile: ab 384px Viewport sichtbar, darunter
|
|
(sehr schmale Phones, z.B. iPhone SE) ausgeblendet, damit der Titel Platz
|
|
behält. Das Menü enthält „Neues Gespräch" dauerhaft als Rückfall. -->
|
|
<button id="new-chat-top" type="button" title="Neues Gespräch" aria-label="Neues Gespräch"
|
|
class="shrink-0 h-9 w-9 hidden min-[384px]:grid place-items-center rounded-lg border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
|
|
</button>
|
|
<button id="menu-toggle" title="Menü" aria-label="Menü"
|
|
class="h-9 w-9 grid place-items-center rounded-lg border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700 text-xl leading-none transition-colors">⋮</button>
|
|
</header>
|
|
|
|
<!-- Identitätsleiste: angemeldeter Nutzer (links) + Abmelden (rechts) -->
|
|
<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>
|
|
|
|
<!-- Verstecktes Quell-Select: speist die Ton-Presets (bewahrt die bestehende Logik) -->
|
|
<select id="tts" class="hidden">
|
|
<option value="device">Im Gerät</option>
|
|
<option value="piper">Server</option>
|
|
<option value="chatterbox">Beste Qualität</option>
|
|
<option value="cartesia">Cartesia</option>
|
|
<option value="openrouter">Cloud</option>
|
|
</select>
|
|
|
|
<!-- ═══════════════════ EINSTELLUNGS-SHEET ═══════════════════ -->
|
|
<div id="settings-backdrop" class="hidden fixed inset-0 z-40 bg-black/30"></div>
|
|
<div id="settings-sheet"
|
|
class="hidden fixed top-14 right-2 z-50 w-[min(20rem,calc(100vw-1rem))] max-h-[80vh] overflow-y-auto
|
|
rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 shadow-xl p-3 space-y-3">
|
|
|
|
<!-- Vorlesen (TTS-Presets) -->
|
|
<div>
|
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-1.5 px-1">Vorlesen</p>
|
|
<div class="space-y-1.5">
|
|
<button type="button" data-tts="device"
|
|
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
|
|
<span class="text-lg shrink-0">📱</span>
|
|
<span class="flex-1 min-w-0"><span class="block text-sm font-medium">Im Gerät</span>
|
|
<span class="tts-sub block text-xs text-slate-500 dark:text-slate-400">Das Gerät liest vor · spart Daten</span></span>
|
|
<span class="check text-blue-600 dark:text-blue-400 font-bold opacity-0">✓</span>
|
|
</button>
|
|
<button type="button" data-tts="piper"
|
|
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
|
|
<span class="text-lg shrink-0">⚡</span>
|
|
<span class="flex-1 min-w-0"><span class="block text-sm font-medium">Schnell</span>
|
|
<span class="block text-xs text-slate-500 dark:text-slate-400">Piper · lokal, zuverlässig</span></span>
|
|
<span class="check text-blue-600 dark:text-blue-400 font-bold opacity-0">✓</span>
|
|
</button>
|
|
<button type="button" data-tts="chatterbox"
|
|
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
|
|
<span class="text-lg shrink-0">✨</span>
|
|
<span class="flex-1 min-w-0"><span class="block text-sm font-medium">Hohe Qualität</span>
|
|
<span class="block text-xs text-slate-500 dark:text-slate-400">Chatterbox · natürliche Stimme</span></span>
|
|
<span class="check text-blue-600 dark:text-blue-400 font-bold opacity-0">✓</span>
|
|
</button>
|
|
<button type="button" data-tts="cartesia"
|
|
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
|
|
<span class="text-lg shrink-0">🎙</span>
|
|
<span class="flex-1 min-w-0"><span class="block text-sm font-medium">Cartesia</span>
|
|
<span class="tts-sub block text-xs text-slate-500 dark:text-slate-400">Cartesia Sonic · natürlich & schnell</span></span>
|
|
<span class="check text-blue-600 dark:text-blue-400 font-bold opacity-0">✓</span>
|
|
</button>
|
|
<button type="button" data-tts="openrouter"
|
|
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
|
|
<span class="text-lg shrink-0">☁</span>
|
|
<span class="flex-1 min-w-0"><span class="block text-sm font-medium">Cloud</span>
|
|
<span class="block text-xs text-slate-500 dark:text-slate-400">OpenRouter · benötigt Internet</span></span>
|
|
<span class="check text-blue-600 dark:text-blue-400 font-bold opacity-0">✓</span>
|
|
</button>
|
|
</div>
|
|
<p id="cartesia-lang-hint" class="hidden mt-2 px-1 text-xs text-amber-600 dark:text-amber-400"></p>
|
|
</div>
|
|
|
|
|
|
<!-- Geschlechtspräferenz (wirkt bei Cartesia) -->
|
|
<div id="gender-pref-row">
|
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-1.5 px-1">Stimme</p>
|
|
<div class="flex gap-2">
|
|
<button type="button" id="gender-m"
|
|
class="gender-btn flex-1 rounded-lg border border-slate-200 dark:border-slate-700 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:hover:bg-slate-700/50">
|
|
♂ Männlich
|
|
</button>
|
|
<button type="button" id="gender-f"
|
|
class="gender-btn flex-1 rounded-lg border border-slate-200 dark:border-slate-700 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:hover:bg-slate-700/50">
|
|
♀ Weiblich
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Aktionen -->
|
|
<div class="border-t border-slate-100 dark:border-slate-700 pt-2 space-y-1">
|
|
<button id="new-chat" type="button"
|
|
class="w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 text-left transition-colors">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" class="w-5 h-5 shrink-0"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
|
|
Neues Gespräch
|
|
</button>
|
|
<button id="theme-toggle" type="button"
|
|
class="w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 text-left transition-colors">
|
|
<span id="theme-icon" class="w-5 text-center shrink-0">🌙</span>
|
|
<span>Tag-/Nachtmodus</span>
|
|
</button>
|
|
<button id="admin-toggle" type="button"
|
|
class="hidden w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 text-left transition-colors">
|
|
<span class="w-5 text-center shrink-0">⚙</span>
|
|
<span>Admin-Bereich</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<main id="messages" class="flex-1 overflow-y-auto px-4 py-4 space-y-3 w-full max-w-3xl mx-auto"></main>
|
|
|
|
<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">
|
|
<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>
|
|
<input id="prompt" type="text" autocomplete="off" dir="auto" placeholder="Nachricht …"
|
|
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" title="Senden" aria-label="Senden"
|
|
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>
|
|
<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>
|
|
|
|
<!-- ═══════════════ SOS-Bestätigung (Notruf) ═══════════════ -->
|
|
<div id="sos-confirm" role="dialog" aria-modal="true" aria-labelledby="sos-confirm-title"
|
|
class="hidden fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4">
|
|
<div class="w-full max-w-sm rounded-2xl bg-white dark:bg-slate-800 shadow-xl overflow-hidden">
|
|
<div class="bg-red-600 text-white px-5 py-4 flex items-center gap-3">
|
|
<span class="text-3xl font-extrabold tracking-wide">SOS</span>
|
|
<span id="sos-confirm-title" class="text-lg font-semibold">Hilfe rufen?</span>
|
|
</div>
|
|
<div class="px-5 py-4">
|
|
<p class="text-base text-slate-700 dark:text-slate-200">Möchten Sie wirklich Hilfe rufen?</p>
|
|
<p class="mt-1 text-sm font-semibold text-red-600 dark:text-red-400">Es werden sofort Helfer alarmiert.</p>
|
|
</div>
|
|
<div class="px-5 pb-5 grid grid-cols-1 gap-3">
|
|
<button type="button" id="sos-confirm-yes"
|
|
class="h-14 rounded-xl bg-red-600 hover:bg-red-700 text-white text-xl font-extrabold tracking-wide transition-colors">JA, HILFE RUFEN</button>
|
|
<button type="button" id="sos-confirm-no"
|
|
class="h-12 rounded-xl border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-200 text-lg font-semibold hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════ Notruf bereits ausgelöst (Sperre) ═══════════════ -->
|
|
<div id="sos-cooldown" role="dialog" aria-modal="true" aria-labelledby="sos-cooldown-title"
|
|
class="hidden fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4">
|
|
<div class="w-full max-w-sm rounded-2xl bg-white dark:bg-slate-800 shadow-xl overflow-hidden">
|
|
<div class="bg-green-600 text-white px-5 py-4 flex items-center gap-3">
|
|
<span class="text-3xl font-extrabold">✓</span>
|
|
<span id="sos-cooldown-title" class="text-lg font-semibold">Hilfe ist unterwegs</span>
|
|
</div>
|
|
<div class="px-5 py-4">
|
|
<p id="sos-cooldown-text" class="text-base text-slate-700 dark:text-slate-200"></p>
|
|
</div>
|
|
<div class="px-5 pb-5">
|
|
<button type="button" id="sos-cooldown-ok"
|
|
class="w-full h-14 rounded-xl bg-green-600 hover:bg-green-700 text-white text-xl font-extrabold transition-colors">Verstanden</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/app.js?v=53"></script>
|
|
</body>
|
|
</html>
|