This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import { Activity } from "lucide-react"
|
||||
import { useTokenStats, useMetricHistory } from "@/lib/queries"
|
||||
import { useTokHistory } from "@/lib/metricsStore"
|
||||
import { LiveAreaChart, type ChartSeries } from "./LiveAreaChart"
|
||||
import { RangeSwitch, RANGE_MINUTES, type ChartRange } from "./RangeSwitch"
|
||||
|
||||
const SERIES: ChartSeries[] = [
|
||||
{ key: "prompt", label: "Prompt", color: "#f59e0b" },
|
||||
{ key: "completion", label: "Antwort", color: "#2dd4bf" },
|
||||
]
|
||||
|
||||
export function TokenPerformanceCard() {
|
||||
const { data: ts } = useTokenStats()
|
||||
const hist = useTokHistory() // Live-Durchsatz aus dem modul-globalen Store (~2 min)
|
||||
const [range, setRange] = useState<ChartRange>("live")
|
||||
const histQ = useMetricHistory(range === "live" ? 60 : RANGE_MINUTES[range], range !== "live")
|
||||
|
||||
// 1h/24h: Raten aus den Deltas der Gesamtzähler (tok/s zwischen zwei Sample-Punkten).
|
||||
const chartData = useMemo(() => {
|
||||
if (range === "live") return hist
|
||||
const pts = histQ.data?.points ?? []
|
||||
const out: { t: number; prompt: number; completion: number }[] = []
|
||||
for (let i = 1; i < pts.length; i++) {
|
||||
const a = pts[i - 1], b = pts[i]
|
||||
if (a.tp == null || b.tp == null || a.tc == null || b.tc == null) continue
|
||||
const dt = Math.max(b.t - a.t, 1)
|
||||
out.push({
|
||||
t: b.t * 1000,
|
||||
prompt: Math.max(0, (b.tp - a.tp) / dt),
|
||||
completion: Math.max(0, (b.tc - a.tc) / dt),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}, [range, hist, histQ.data])
|
||||
|
||||
const last = hist[hist.length - 1]
|
||||
const curRate = last ? Math.round(last.prompt + last.completion) : 0
|
||||
|
||||
return (
|
||||
<div className="mc-card p-5">
|
||||
<div className="mb-3 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4.5 w-4.5 text-primary" />
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Token-Durchsatz</h2>
|
||||
{range === "live" && (
|
||||
<span className="ml-1 inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground/70">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> live
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{ts && (
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="font-space text-3xl font-bold tracking-tight text-foreground tabular-nums">
|
||||
{curRate.toLocaleString("de-DE")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">tok/s aktuell</span>
|
||||
</div>
|
||||
)}
|
||||
{ts && (
|
||||
<div className="mt-0.5 space-y-0.5 font-mono text-[11px] text-muted-foreground/70">
|
||||
<div>
|
||||
{ts.total_tokens.toLocaleString("de-DE")} Tokens gesamt · <span className="text-emerald-400/90">{ts.saved_eur.toLocaleString("de-DE", { minimumFractionDigits: 2 })} € gespart</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground/55">
|
||||
Input {ts.prompt_tokens.toLocaleString("de-DE")} · Output {ts.completion_tokens.toLocaleString("de-DE")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col items-end gap-2 pt-1">
|
||||
<RangeSwitch value={range} onChange={setRange} />
|
||||
<div className="flex flex-col items-end gap-1.5">
|
||||
{SERIES.map((s) => (
|
||||
<div key={s.key} className="flex items-center gap-1.5">
|
||||
<span className="h-2 w-2 rounded-full" style={{ background: s.color }} />
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">{s.label}</span>
|
||||
<span className="font-mono text-xs font-bold tabular-nums text-foreground">
|
||||
{Math.round((last?.[s.key as "prompt" | "completion"]) ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{range !== "live" && chartData.length === 0 ? (
|
||||
<div className="flex h-[150px] items-center justify-center text-xs text-muted-foreground">
|
||||
{histQ.isLoading ? "Verlauf wird geladen …" : "Noch kein Verlauf — der Sammler baut die Historie gerade erst auf."}
|
||||
</div>
|
||||
) : ts ? (
|
||||
<LiveAreaChart data={chartData} series={SERIES} unit=" tok/s" yMode="auto" height={150} showTime />
|
||||
) : (
|
||||
<div className="flex h-[150px] items-center justify-center text-xs text-muted-foreground">Lade Durchsatz…</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 border-t border-border/30 pt-2 text-[10px] text-muted-foreground/70">
|
||||
Durchsatz aus dem Gateway abgeleitet (Prefill vs. Generierung). Flach bei Leerlauf.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { cn } from "@/lib/utils"
|
||||
import { CockpitTile } from "./CockpitTile"
|
||||
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
|
||||
import { LatencyCard } from "@/components/dashboard/LatencyCard"
|
||||
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
||||
|
||||
// Öffnet die bestehende System-Schublade (Pflege/Logs) via globalem Event.
|
||||
const openDrawer = (tab: "maintenance" | "logs") =>
|
||||
@@ -118,6 +119,9 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
<div className="mt-4">
|
||||
<LatencyCard />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TokenPerformanceCard />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Bereichs-Kacheln (6 statt 7 - Logs ist in die Werkzeug-Liste gewandert) */}
|
||||
|
||||
Reference in New Issue
Block a user