Misty-Rhetorik-Coach/templates/dashboard.html

470 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Misty Rhetorik-Coach</title>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #f0f4ff;
--white: #ffffff;
--blue: #1a56db;
--blue-light: #dbeafe;
--blue-mid: #93c5fd;
--dark: #1e2d5a;
--muted: #6b7db3;
--green: #059669;
--green-light: #d1fae5;
--amber: #d97706;
--amber-light: #fef3c7;
--red: #dc2626;
--red-light: #fee2e2;
--border: #e2e8f0;
}
body {
background: var(--bg);
color: var(--dark);
font-family: 'Nunito', sans-serif;
min-height: 100vh;
padding: 28px;
}
header {
background: var(--white);
border-radius: 20px;
padding: 20px 28px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(37,99,235,0.08);
border: 1px solid var(--border);
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-avatar {
width: 48px; height: 48px;
border-radius: 14px;
background: linear-gradient(135deg, var(--blue), #3b82f6);
display: grid; place-items: center;
font-size: 24px;
box-shadow: 0 4px 12px rgba(37,99,235,0.3);
}
.brand-name { font-weight: 900; font-size: 20px; letter-spacing: -0.5px; }
.brand-sub { font-size: 12px; color: var(--muted); margin-top: 2px; font-weight: 500; }
.status-pill {
display: flex; align-items: center; gap: 8px;
padding: 10px 20px; border-radius: 100px;
font-size: 13px; font-weight: 700;
}
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
animation: blink 1.5s ease-in-out infinite;
}
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
.grid-top {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px; margin-bottom: 20px;
}
.grid-bottom {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px; margin-bottom: 20px;
}
.card {
background: var(--white);
border-radius: 20px; padding: 24px;
border: 1px solid var(--border);
box-shadow: 0 2px 12px rgba(37,99,235,0.06);
}
.card-blue {
background: linear-gradient(135deg, var(--blue) 0%, #3b82f6 100%);
color: white; border: none;
box-shadow: 0 8px 24px rgba(37,99,235,0.3);
position: relative; overflow: hidden;
}
.dots {
position: absolute; top: 16px; right: 16px;
opacity: 0.15; font-size: 20px;
letter-spacing: 3px; color: white;
}
.card-label {
font-size: 11px; font-weight: 700;
letter-spacing: 1.5px; text-transform: uppercase;
color: var(--muted); margin-bottom: 14px;
}
.card-blue .card-label { color: rgba(255,255,255,0.7); }
.metric-num {
font-size: 56px; font-weight: 900;
line-height: 1; letter-spacing: -2px;
}
.metric-unit { font-size: 13px; color: var(--muted); margin-top: 4px; font-weight: 600; }
.card-blue .metric-unit { color: rgba(255,255,255,0.7); }
.badge {
display: inline-flex; align-items: center; gap: 5px;
padding: 5px 12px; border-radius: 100px;
font-size: 12px; font-weight: 700; margin-top: 12px;
}
.badge-green { background: var(--green-light); color: var(--green); }
.badge-amber { background: var(--amber-light); color: var(--amber); }
.badge-red { background: var(--red-light); color: var(--red); }
.badge-blue { background: rgba(255,255,255,0.2); color: white; }
.gauge-wrap { margin-top: 16px; }
.gauge-track {
height: 8px; border-radius: 4px;
background: var(--blue-light);
position: relative; overflow: visible;
}
.gz { position: absolute; height: 100%; border-radius: 4px; }
.gauge-knob {
position: absolute; top: 50%;
transform: translate(-50%, -50%);
width: 18px; height: 18px;
background: white; border-radius: 50%;
border: 3px solid var(--blue);
box-shadow: 0 2px 8px rgba(37,99,235,0.4);
}
.gauge-labels {
display: flex; justify-content: space-between;
margin-top: 8px; font-size: 10px;
color: var(--muted); font-weight: 600;
}
.face-card {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
min-height: 140px; text-align: center;
}
.face-circle {
width: 120px; height: 80px; border-radius: 12px;
background: var(--blue-light);
display: grid; place-items: center;
margin-bottom: 10px;
border: 3px solid var(--blue-mid);
overflow: hidden;
animation: float 3s ease-in-out infinite;
}
@keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-5px)} }
.face-name {
font-size: 11px; color: var(--muted); font-weight: 600;
background: var(--blue-light); padding: 4px 10px; border-radius: 6px;
}
.transcript-box {
background: var(--bg); border-radius: 12px;
padding: 16px 18px; font-size: 14px;
line-height: 1.8; color: #475569;
border: 1px solid var(--border); font-weight: 500;
}
.tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.tag {
display: inline-flex; align-items: center; gap: 6px;
background: var(--amber-light); border: 1px solid #fde68a;
color: var(--amber); padding: 6px 12px;
border-radius: 8px; font-size: 12px; font-weight: 700;
}
.tag-n {
background: #fcd34d; color: #92400e;
border-radius: 4px; padding: 1px 6px; font-size: 11px;
}
.feedback-body {
font-size: 14px; line-height: 1.7;
color: #475569; font-weight: 500;
}
.feedback-icon { font-size: 32px; margin-bottom: 10px; }
.verlauf-row {
display: flex; align-items: center; gap: 14px;
padding: 12px 0; border-bottom: 1px solid var(--border);
}
.verlauf-row:last-child { border-bottom: none; padding-bottom: 0; }
.v-num { font-size: 12px; font-weight: 800; color: var(--muted); width: 30px; flex-shrink: 0; }
.v-bar { flex: 1; height: 8px; background: var(--blue-light); border-radius: 4px; overflow: hidden; }
.v-fill { height: 100%; border-radius: 4px; }
.v-meta {
display: flex; align-items: center; gap: 10px;
font-size: 12px; font-weight: 600; color: var(--muted);
flex-shrink: 0; min-width: 190px; justify-content: flex-end;
}
.v-face { width: 40px; height: 28px; border-radius: 6px; object-fit: cover; }
.c-green { color: var(--green); }
.c-amber { color: var(--amber); }
.c-red { color: var(--red); }
.c-blue { color: var(--blue); }
.empty-state {
text-align: center; padding: 40px;
color: var(--muted); font-size: 15px; font-weight: 600;
line-height: 1.8;
}
.fuellwort-card {
background: var(--white);
border-radius: 20px; padding: 24px;
border: 1px solid var(--border);
box-shadow: 0 2px 12px rgba(37,99,235,0.06);
margin-bottom: 20px;
}
.fuellwort-liste {
display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0;
}
.fuellwort-tag {
display: inline-flex; align-items: center; gap: 8px;
background: var(--blue-light); border: 1px solid var(--blue-mid);
color: var(--dark); padding: 6px 12px;
border-radius: 8px; font-size: 13px; font-weight: 700;
}
.fuellwort-tag a {
color: var(--red); text-decoration: none;
font-size: 16px; font-weight: 900; line-height: 1;
}
.fuellwort-tag a:hover { color: #991b1b; }
.fuellwort-form {
display: flex; gap: 10px; margin-top: 14px;
}
.fuellwort-input {
flex: 1; padding: 10px 16px;
border: 2px solid var(--border); border-radius: 10px;
font-family: 'Nunito', sans-serif; font-size: 14px;
font-weight: 600; color: var(--dark);
outline: none;
}
.fuellwort-input:focus { border-color: var(--blue); }
.fuellwort-btn {
background: var(--blue); color: white;
border: none; border-radius: 10px;
padding: 10px 20px; font-family: 'Nunito', sans-serif;
font-size: 14px; font-weight: 700; cursor: pointer;
}
.fuellwort-btn:hover { background: #1e40af; }
.fehler-box {
background: var(--red-light);
border: 2px solid var(--red);
border-radius: 20px; padding: 24px;
margin-bottom: 20px;
display: flex; align-items: center; gap: 16px;
}
.fehler-icon { font-size: 32px; flex-shrink: 0; }
.fehler-titel { font-size: 16px; font-weight: 800; color: var(--red); margin-bottom: 4px; }
.fehler-text { font-size: 14px; color: #7f1d1d; font-weight: 500; }
</style>
</head>
<body>
<header>
<div class="brand">
<div class="brand-avatar">🤖</div>
<div>
<div class="brand-name">Misty Rhetorik-Coach</div>
<div class="brand-sub">PH Weingarten · Modul M2 · WS 2025/26</div>
</div>
</div>
<div class="status-pill" id="status-pill"
style="background:{% if daten.status == 'fehler' %}var(--red-light);color:var(--red){% else %}var(--green-light);color:var(--green){% endif %};">
<div class="status-dot" id="status-dot"
style="background:{% if daten.status == 'fehler' %}var(--red){% else %}var(--green){% endif %};"></div>
<span id="status-text">
{% if daten.status == "wartend" %}Wartet auf Fußdruck
{% elif daten.status == "aufnehmend" %}Aufnahme läuft...
{% elif daten.status == "analysierend" %}Analyse läuft...
{% elif daten.status == "fehler" %}Fehler aufgetreten
{% endif %}
</span>
</div>
</header>
<!-- Fehlermeldung -->
{% if daten.status == "fehler" and daten.fehler %}
<div class="fehler-box">
<div class="fehler-icon">⚠️</div>
<div>
<div class="fehler-titel">Verbindungsfehler</div>
<div class="fehler-text">{{ daten.fehler }}</div>
</div>
</div>
{% endif %}
<!-- Füllwörter-Verwaltung -->
<div class="fuellwort-card">
<div class="card-label">Füllwörter verwalten</div>
<div class="fuellwort-liste">
{% for wort in fuellwoerter %}
<span class="fuellwort-tag">
{{ wort }}
<a href="/fuellwort/entfernen/{{ wort }}" title="Entfernen">×</a>
</span>
{% endfor %}
</div>
<form class="fuellwort-form" method="POST" action="/fuellwort/hinzufuegen">
<input class="fuellwort-input" type="text" name="wort" placeholder="Neues Füllwort eingeben..." autocomplete="off">
<button class="fuellwort-btn" type="submit">+ Hinzufügen</button>
</form>
</div>
<!-- Hauptinhalt -->
<div id="hauptinhalt">
{% if daten.sessions %}
{% set letzte = daten.sessions[-1] %}
<div class="grid-top">
<div class="card card-blue">
<span class="dots">· · · · ·<br>· · · · ·<br>· · · · ·</span>
<div class="card-label">Füllwörter</div>
<div class="metric-num" style="color:white;">{{ letzte.fuellwoerter_anzahl }}</div>
<div class="metric-unit">in dieser Session</div>
{% if daten.sessions|length > 1 %}
{% set vorher = daten.sessions[-2] %}
{% if letzte.fuellwoerter_anzahl < vorher.fuellwoerter_anzahl %}
<div class="badge badge-blue">↓ {{ vorher.fuellwoerter_anzahl - letzte.fuellwoerter_anzahl }} weniger als zuvor</div>
{% elif letzte.fuellwoerter_anzahl > vorher.fuellwoerter_anzahl %}
<div class="badge badge-blue">↑ {{ letzte.fuellwoerter_anzahl - vorher.fuellwoerter_anzahl }} mehr als zuvor</div>
{% else %}
<div class="badge badge-blue">= gleich wie zuvor</div>
{% endif %}
{% endif %}
</div>
<div class="card">
<div class="card-label">Sprechtempo</div>
<div class="metric-num c-blue">{{ letzte.tempo }}</div>
<div class="metric-unit">Wörter pro Minute</div>
<div class="gauge-wrap">
<div class="gauge-track">
<div class="gz" style="left:0;width:30%;background:#fde68a;border-radius:4px 0 0 4px;"></div>
<div class="gz" style="left:30%;width:40%;background:#bbf7d0;"></div>
<div class="gz" style="left:70%;width:30%;background:#fecaca;border-radius:0 4px 4px 0;"></div>
{% if letzte.tempo < 90 %}
<div class="gauge-knob" style="left:15%;"></div>
{% elif letzte.tempo <= 150 %}
<div class="gauge-knob" style="left:{{ 30 + ((letzte.tempo - 90) / 60 * 40)|int }}%;"></div>
{% else %}
<div class="gauge-knob" style="left:85%;"></div>
{% endif %}
</div>
<div class="gauge-labels">
<span>zu langsam</span>
<span>optimal</span>
<span>zu schnell</span>
</div>
</div>
{% if letzte.tempo < 90 %}
<div class="badge badge-amber">zu langsam</div>
{% elif letzte.tempo <= 150 %}
<div class="badge badge-green">✓ Sehr angenehm</div>
{% else %}
<div class="badge badge-red">zu schnell</div>
{% endif %}
</div>
<div class="card">
<div class="card-label">Mistys Reaktion</div>
<div class="face-card">
<div class="face-circle">
<img src="/static/{{ letzte.gesicht }}" style="width:100%; height:100%; object-fit:cover;">
</div>
<div class="face-name">{{ letzte.gesicht }}</div>
</div>
</div>
</div>
<div class="grid-bottom">
<div class="card">
<div class="card-label">Erkannter Text</div>
<div class="transcript-box">„{{ letzte.text }}"</div>
{% if letzte.gefundene_woerter %}
<div class="tags">
{% for wort, anzahl in letzte.gefundene_woerter.items() %}
<span class="tag">{{ wort }} <span class="tag-n">{{ anzahl }}×</span></span>
{% endfor %}
</div>
{% endif %}
</div>
<div class="card">
<div class="card-label">Feedback von Misty</div>
<div class="feedback-icon">💬</div>
<div class="feedback-body">{{ letzte.feedback }}</div>
</div>
</div>
{% if daten.sessions|length > 1 %}
<div class="card">
<div class="card-label">Sessionverlauf</div>
{% for session in daten.sessions|reverse %}
{% if not loop.first %}
<div class="verlauf-row">
<div class="v-num">#{{ session.nummer }}</div>
<div class="v-bar">
<div class="v-fill" style="width:{{ [session.fuellwoerter_anzahl * 10, 100]|min }}%;
background:{% if session.fuellwoerter_anzahl == 0 %}#6ee7b7
{% elif session.fuellwoerter_anzahl <= 2 %}#fcd34d
{% else %}#fca5a5{% endif %};"></div>
</div>
<div class="v-meta">
<b class="{% if session.fuellwoerter_anzahl == 0 %}c-green{% elif session.fuellwoerter_anzahl <= 2 %}c-amber{% else %}c-red{% endif %}">
{{ session.fuellwoerter_anzahl }}
</b> Füllwörter ·
<b>{{ session.tempo }}</b> W/min ·
<img src="/static/{{ session.gesicht }}" class="v-face">
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% else %}
<div class="card">
<div class="empty-state">
🤖 Willkommen beim Misty Rhetorik-Coach!<br><br>
Hier werden die Ergebnisse jeder Präsentation Füllwörter, Sprechtempo und Mistys Feedback gezeigt.<br><br>
Sobald der erste Durchgang abgeschlossen ist, erscheinen die Ergebnisse automatisch.<br><br>
Im Sessionverlauf ist zu sehen, wie sich die Leistung über mehrere Durchgänge hinweg verbessert.<br><br>
<strong>👣 Drücke Mistys Fuß um die Aufnahme zu starten!</strong>
</div>
</div>
{% endif %}
</div>
<script>
const statusPill = document.getElementById('status-pill');
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
function aktualisiereStatus(status) {
if (status === 'wartend') {
statusPill.style.background = 'var(--green-light)';
statusPill.style.color = 'var(--green)';
statusDot.style.background = 'var(--green)';
statusText.textContent = 'Wartet auf Fußdruck';
} else if (status === 'aufnehmend') {
statusPill.style.background = 'var(--red-light)';
statusPill.style.color = 'var(--red)';
statusDot.style.background = 'var(--red)';
statusText.textContent = 'Aufnahme läuft...';
} else if (status === 'analysierend') {
statusPill.style.background = 'var(--amber-light)';
statusPill.style.color = 'var(--amber)';
statusDot.style.background = 'var(--amber)';
statusText.textContent = 'Analyse läuft...';
} else if (status === 'fehler') {
statusPill.style.background = 'var(--red-light)';
statusPill.style.color = 'var(--red)';
statusDot.style.background = 'var(--red)';
statusText.textContent = 'Fehler aufgetreten';
}
}
let letzteSessionAnzahl = {{ daten.sessions|length }};
let letzterFehler = {{ (daten.fehler if daten.fehler is defined else none)|tojson }};
const evtSource = new EventSource('/stream');
evtSource.onmessage = function(e) {
const daten = JSON.parse(e.data);
aktualisiereStatus(daten.status);
const neueAnzahl = daten.sessions ? daten.sessions.length : 0;
const neuerFehler = daten.fehler || null;
if (neueAnzahl !== letzteSessionAnzahl || neuerFehler !== letzterFehler) {
letzteSessionAnzahl = neueAnzahl;
letzterFehler = neuerFehler;
window.location.reload();
}
};
evtSource.onerror = function() {
console.log('SSE Verbindung unterbrochen, versuche neu...');
};
</script>
</body>
</html>