Misty-Sprach-Coach/templates/dashboard.html
2026-05-06 12:46:05 +00:00

348 lines
12 KiB
HTML
Raw 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">
<meta http-equiv="refresh" content="3">
<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-wartend { background: var(--green-light); color: var(--green); }
.status-aufnehmend { background: var(--red-light); color: var(--red); }
.status-analysierend { background: var(--amber-light); color: var(--amber); }
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
animation: blink 1.5s ease-in-out infinite;
}
.status-wartend .status-dot { background: var(--green); }
.status-aufnehmend .status-dot { background: var(--red); }
.status-analysierend .status-dot { background: var(--amber); }
@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: 28px; height: 28px; border-radius: 50%; 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;
}
</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 Entwicklung Interaktiver Medien</div>
</div>
</div>
<div class="status-pill status-{{ daten.status }}">
<div class="status-dot"></div>
{% if daten.status == "wartend" %}Wartet auf Fußdruck
{% elif daten.status == "aufnehmend" %}Aufnahme läuft...
{% elif daten.status == "analysierend" %}Analyse läuft...
{% endif %}
</div>
</header>
{% 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">
<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 %}
</body>
</html>