🎛️ Admin: Pill-Buttons + Schnellfilter | 💬 Chat: TTS-Speaker-Button (Web Speech API)

This commit is contained in:
Hermes Agent
2026-06-17 13:55:28 +02:00
parent 924a759637
commit 289d60b32e
+57 -4
View File
@@ -881,7 +881,13 @@ def admin_dashboard():
body.light .admin-header {{ background:rgba(242,242,247,0.72); }} body.light .admin-header {{ background:rgba(242,242,247,0.72); }}
.admin-header-inner {{ max-width:1200px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:0.5rem; }} .admin-header-inner {{ max-width:1200px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:0.5rem; }}
.admin-header h1 {{ font-size:1.25rem; font-weight:700; letter-spacing:-0.02em; color:var(--text); }} .admin-header h1 {{ font-size:1.25rem; font-weight:700; letter-spacing:-0.02em; color:var(--text); }}
.admin-header .meta {{ font-size:0.75rem; color:var(--text2); display:flex; gap:0.5rem; flex-wrap:wrap; align-items:center; }} .admin-header .meta {{ font-size:0.75rem; color:var(--text2); display:flex; gap:0.35rem; flex-wrap:wrap; align-items:center; }}
.count-badge {{ background:var(--surface2); color:var(--text2); padding:0.3rem 0.7rem; border-radius:999px; font-size:0.7rem; font-weight:600; white-space:nowrap; }}
.admin-btn {{ display:inline-block; padding:0.35rem 0.85rem; border-radius:999px; font-size:0.72rem; font-weight:600; text-decoration:none; transition:all 0.15s; white-space:nowrap; background:var(--surface2); color:var(--text2); border:1.5px solid transparent; }}
.admin-btn:hover {{ background:var(--surface); color:var(--text); border-color:var(--border); }}
.admin-btn.active {{ background:var(--accent); color:#fff; border-color:var(--accent); }}
.admin-btn.secondary {{ background:transparent; color:var(--text3); }}
.admin-btn.secondary:hover {{ color:var(--text); background:var(--surface2); }}
.admin-header a {{ color:var(--accent); text-decoration:none; font-weight:500; }} .admin-header a {{ color:var(--accent); text-decoration:none; font-weight:500; }}
.admin-header a:hover {{ opacity:0.8; }} .admin-header a:hover {{ opacity:0.8; }}
@@ -946,7 +952,15 @@ function toggleTheme(){{document.body.classList.toggle('light');localStorage.set
<header class="admin-header"> <header class="admin-header">
<div class="admin-header-inner"> <div class="admin-header-inner">
<h1>📨 BSN Intake</h1> <h1>📨 BSN Intake</h1>
<div class="meta">{len(rows)} Submissions | <a href="/test">🧪 Test</a> | <a href="/">🌐 Frontend</a> | <a href="/health">⚙️ Health</a></div> <div class="meta">
<span class="count-badge">{len(rows)} Submissions</span>
<a href="/admin" class="admin-btn{' active' if not status_filter else ''}">📋 Alle</a>
<a href="/admin?status=new" class="admin-btn{' active' if status_filter=='new' else ''}">🆕 Neu</a>
<a href="/admin?status=published" class="admin-btn{' active' if status_filter=='published' else ''}">✅ Publiziert</a>
<a href="/admin?status=flagged" class="admin-btn{' active' if status_filter=='flagged' else ''}">🚩 Markiert</a>
<a href="/test" class="admin-btn secondary">🧪 Test</a>
<a href="/" class="admin-btn secondary">🌐 Frontend</a>
</div>
</div> </div>
</header> </header>
<main class="admin-main"> <main class="admin-main">
@@ -1826,7 +1840,10 @@ def public_frontend():
<button id="chatClose" aria-label="Schließen">✕</button> <button id="chatClose" aria-label="Schließen">✕</button>
</div> </div>
<div class="chat-body" id="chatBody"> <div class="chat-body" id="chatBody">
<div class="chat-msg assistant">Hey! 👋 Ich bin der BSN Community Assistant. Frag mich nach Spielen, Ständen oder was auf der SPIEL Essen los ist!</div> <div style="display:flex;align-items:flex-end;gap:0.3rem;align-self:flex-start;">
<div class="chat-msg assistant">Hey! 👋 Ich bin der BSN Community Assistant. Frag mich nach Spielen, Ständen oder was auf der SPIEL Essen los ist!</div>
<button onclick="(function(t){var u=new SpeechSynthesisUtterance(t);u.lang='de-DE';u.rate=0.95;var v=speechSynthesis.getVoices();var d=v.find(function(x){return x.lang.startsWith('de')});if(d)u.voice=d;speechSynthesis.cancel();speechSynthesis.speak(u)})('Hey! Ich bin der BSN Community Assistant. Frag mich nach Spielen, Ständen oder was auf der SPIEL Essen los ist!')" title="Vorlesen" style="background:none;border:none;cursor:pointer;font-size:0.85rem;opacity:0.5;padding:0 0 0.15rem 0;transition:opacity 0.15s;flex-shrink:0;" onmouseenter="this.style.opacity='1'" onmouseleave="this.style.opacity='0.5'">🔊</button>
</div>
</div> </div>
<div class="chat-footer"> <div class="chat-footer">
<input type="text" id="chatInput" placeholder="Frag was..." aria-label="Chat-Nachricht"> <input type="text" id="chatInput" placeholder="Frag was..." aria-label="Chat-Nachricht">
@@ -1855,14 +1872,50 @@ def public_frontend():
closeBtn.addEventListener('click', () => { open = false; overlay.classList.remove('open'); }); closeBtn.addEventListener('click', () => { open = false; overlay.classList.remove('open'); });
function addMsg(role, text) { function addMsg(role, text) {
const wrapper = document.createElement('div');
wrapper.style.display = 'flex';
wrapper.style.alignItems = 'flex-end';
wrapper.style.gap = '0.3rem';
if (role === 'assistant') wrapper.style.alignSelf = 'flex-start';
else wrapper.style.alignSelf = 'flex-end';
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'chat-msg ' + role; div.className = 'chat-msg ' + role;
div.textContent = text; div.textContent = text;
body.appendChild(div); wrapper.appendChild(div);
if (role === 'assistant') {
const btn = document.createElement('button');
btn.innerHTML = '🔊';
btn.title = 'Vorlesen';
btn.style.cssText = 'background:none;border:none;cursor:pointer;font-size:0.85rem;opacity:0.5;padding:0 0 0.15rem 0;transition:opacity 0.15s;flex-shrink:0;';
btn.onmouseenter = () => btn.style.opacity = '1';
btn.onmouseleave = () => btn.style.opacity = '0.5';
btn.onclick = () => speak(text);
wrapper.appendChild(btn);
}
body.appendChild(wrapper);
body.scrollTop = body.scrollHeight; body.scrollTop = body.scrollHeight;
return div; return div;
} }
function speak(text) {
if (!window.speechSynthesis) return;
window.speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text);
u.lang = 'de-DE';
u.rate = 0.95;
// Prefer German voice
const voices = speechSynthesis.getVoices();
const de = voices.find(v => v.lang.startsWith('de'));
if (de) u.voice = de;
speechSynthesis.speak(u);
}
// Preload voices
if (window.speechSynthesis) speechSynthesis.getVoices();
function addTyping() { function addTyping() {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'chat-msg assistant typing'; div.className = 'chat-msg assistant typing';