import { Gauge } from "lucide-react" import { useVoiceTrace } from "@/lib/queries" import type { VoiceTurn } from "@/lib/api" // Balken-Stufen in kanonischer Reihenfolge (zeitlich disjunkt). Mem0 ist ein Unter-Detail // INNERHALB von „Hirn" und wird NICHT als eigenes Segment addiert (sonst Doppelzählung). const SEGMENTS = [ { key: "stt_ms", label: "STT", color: "#f59e0b" }, { key: "vision_ms", label: "Sehen", color: "#a78bfa" }, { key: "hirn_ms", label: "Hirn", color: "#2dd4bf" }, { key: "gen_ms", label: "Antwort", color: "#60a5fa" }, ] as const const fmt = (ms: number | null | undefined): string => ms == null ? "–" : `${(ms / 1000).toLocaleString("de-DE", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} s` function ago(ts: number): string { const s = Math.max(0, Date.now() / 1000 - ts) if (s < 60) return `vor ${Math.round(s)} s` if (s < 3600) return `vor ${Math.round(s / 60)} min` return `vor ${Math.round(s / 3600)} h` } /** Summe der sichtbaren Balken-Stufen (STT gehört zur gefühlten Latenz dazu; total_ms des Backends * misst nur den Chat-Request ohne STT). So stimmen Balkenbreite und angezeigte Zahl immer überein. */ function rowTotal(t: VoiceTurn): number { return SEGMENTS.reduce((sum, s) => sum + ((t[s.key] as number | null) ?? 0), 0) } /** Dominante Stufe eines Turns = der „Täter". */ function culprit(t: VoiceTurn): { label: string; color: string; ms: number } | null { let best: { label: string; color: string; ms: number } | null = null for (const s of SEGMENTS) { const ms = t[s.key] as number | null if (ms != null && (best == null || ms > best.ms)) best = { label: s.label, color: s.color, ms } } return best } function TurnRow({ t, scale }: { t: VoiceTurn; scale: number }) { const title = SEGMENTS.map((s) => `${s.label} ${fmt(t[s.key] as number | null)}`).join(" · ") + (t.mem0_ms != null ? ` (davon Gedächtnis ${fmt(t.mem0_ms)})` : "") return (
{ago(t.ts)}
{SEGMENTS.map((s) => { const ms = t[s.key] as number | null if (!ms || ms <= 0) return null return (
) })}
{t.error && (
Fehler
)}
{t.error ? "–" : fmt(rowTotal(t))}
) } export function LatencyCard() { const { data: turns } = useVoiceTrace(12) const list = turns ?? [] const scale = Math.max(1, ...list.map((t) => rowTotal(t))) const latest = list[0] const lead = latest ? culprit(latest) : null return (

Latenz je Turn

live
{latest ? (
{latest.error ? "Fehler" : fmt(rowTotal(latest))} letzter Turn{!latest.error && lead && <> · Täter: {lead.label} {fmt(lead.ms)}}
) : (
Noch kein Voice-Turn aufgezeichnet.
)} {latest && !latest.error && (
{SEGMENTS.filter((s) => (latest[s.key] as number | null) != null).map((s) => `${s.label} ${fmt(latest[s.key] as number)}`).join(" · ")} {latest.mem0_ms != null && · davon Gedächtnis {fmt(latest.mem0_ms)}}
)}
{SEGMENTS.map((s) => (
{s.label}
))}
{list.length > 0 ? (
{list.map((t) => )}
) : (
Sprich einmal mit Lucy — dann erscheint hier der Zeit-Wasserfall pro Turn, damit man den einen Hänger sofort sieht.
)}
„Hirn" = Zeit bis zum ersten Wort (Agent + Mem0-Suche + Modell); Tool-Runden stecken in „Antwort". Reine Telegram-Turns laufen an MC2 vorbei und erscheinen hier nicht.
) }