Working version 6.2

This commit is contained in:
2026-04-10 14:44:03 +02:00
parent d526c1ff4a
commit f20d5f09ae
28 changed files with 1231 additions and 1554 deletions

View File

@@ -0,0 +1,185 @@
"use client";
import { useTranslations } from "next-intl";
import { useEffect, useState, useCallback } from "react";
interface DailyUsage {
date: string;
inputTokens: number;
outputTokens: number;
spend: number;
}
interface UsageData {
month: string;
currentPeriod: {
inputTokens: number;
outputTokens: number;
totalSpend: number;
requestCount: number;
};
budget: { maxBudget: number | null; spend: number; remaining: number | null };
rateLimits: { rpm: number | null; tpm: number | null };
dailyUsage: DailyUsage[];
}
function fmt(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}k`;
return n.toString();
}
function usd(n: number): string {
return `$${n.toFixed(4)}`;
}
function getCurrentMonth(): string {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
}
function shiftMonth(month: string, delta: number): string {
const [y, m] = month.split("-").map(Number);
const d = new Date(y, m - 1 + delta, 1);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
}
function formatMonth(month: string, locale: string): string {
const [y, m] = month.split("-").map(Number);
return new Date(y, m - 1).toLocaleDateString(locale, { year: "numeric", month: "long" });
}
function UsageChart({ data }: { data: DailyUsage[] }) {
if (!data.length) return null;
const maxTokens = Math.max(...data.map((d) => d.inputTokens + d.outputTokens), 1);
const barW = Math.max(4, Math.floor(600 / data.length) - 2);
const h = 120;
return (
<div className="overflow-x-auto">
<svg
viewBox={`0 0 ${Math.max(data.length * (barW + 2), 600)} ${h + 24}`}
className="w-full h-36"
preserveAspectRatio="xMinYMid meet"
>
{data.map((d, i) => {
const total = d.inputTokens + d.outputTokens;
const totalH = (total / maxTokens) * h;
const inputH = (d.inputTokens / maxTokens) * h;
const x = i * (barW + 2);
return (
<g key={d.date}>
<title>{d.date}: {fmt(d.inputTokens)} in / {fmt(d.outputTokens)} out {usd(d.spend)}</title>
<rect x={x} y={h - totalH} width={barW} height={totalH - inputH} rx={1} fill="var(--color-accent)" opacity={0.3} />
<rect x={x} y={h - inputH} width={barW} height={inputH} rx={1} fill="var(--color-accent)" opacity={0.7} />
{i % 7 === 0 && (
<text x={x + barW / 2} y={h + 14} textAnchor="middle" fill="var(--color-text-muted)" fontSize="8">{d.date.slice(8)}</text>
)}
</g>
);
})}
</svg>
<div className="flex items-center gap-4 text-xs text-text-muted mt-1">
<span className="flex items-center gap-1">
<span className="inline-block h-2 w-2 rounded-sm bg-accent opacity-70" /> Input
</span>
<span className="flex items-center gap-1">
<span className="inline-block h-2 w-2 rounded-sm bg-accent opacity-30" /> Output
</span>
</div>
</div>
);
}
export function UsageDisplay({ teamId }: { teamId: string | null }) {
const t = useTranslations("usage");
const [month, setMonth] = useState(getCurrentMonth);
const [data, setData] = useState<UsageData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const isCurrentMonth = month === getCurrentMonth();
const fetchUsage = useCallback(() => {
if (!teamId) { setLoading(false); return; }
setLoading(true);
setError(null);
fetch(`/api/usage?teamId=${encodeURIComponent(teamId)}&month=${month}`)
.then((res) => { if (!res.ok) throw new Error(`${res.status}`); return res.json(); })
.then(setData)
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
}, [teamId, month]);
useEffect(() => { fetchUsage(); }, [fetchUsage]);
if (!teamId) return null;
return (
<div className="space-y-4">
{/* Month selector */}
<div className="flex items-center justify-between">
<button
onClick={() => setMonth((m) => shiftMonth(m, -1))}
className="rounded-md px-2 py-1 text-xs text-text-secondary hover:text-text-primary hover:bg-surface-2 transition-colors cursor-pointer"
>
</button>
<span className="font-display text-sm font-medium text-text-primary">
{formatMonth(month, "en")}
</span>
<button
onClick={() => setMonth((m) => shiftMonth(m, 1))}
disabled={isCurrentMonth}
className="rounded-md px-2 py-1 text-xs text-text-secondary hover:text-text-primary hover:bg-surface-2 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
>
</button>
</div>
{loading ? (
<div className="bg-surface-1 border border-border rounded-xl p-6 animate-pulse">
<div className="h-4 w-32 bg-surface-3 rounded mb-4" />
<div className="h-36 bg-surface-2 rounded" />
</div>
) : error || !data ? (
<div className="bg-surface-1 border border-border rounded-xl p-6">
<p className="text-sm text-text-secondary">{error || t("noData")}</p>
</div>
) : (
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<StatCard label={t("inputTokens")} value={fmt(data.currentPeriod.inputTokens)} />
<StatCard label={t("outputTokens")} value={fmt(data.currentPeriod.outputTokens)} />
<StatCard label={t("totalSpend")} value={usd(data.currentPeriod.totalSpend)} accent />
<StatCard
label={t("budget")}
value={data.budget.remaining !== null ? usd(data.budget.remaining) : t("noLimit")}
/>
</div>
<div className="bg-surface-1 border border-border rounded-xl p-5">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold uppercase tracking-wider text-text-muted">
{t("dailyBreakdown")}
</h3>
<span className="text-xs text-text-muted tabular-nums">
{data.currentPeriod.requestCount} {t("requests")}
</span>
</div>
<UsageChart data={data.dailyUsage} />
</div>
</>
)}
</div>
);
}
function StatCard({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
return (
<div className="bg-surface-1 border border-border rounded-xl p-4">
<div className="text-xs text-text-muted mb-1">{label}</div>
<div className={`font-display text-lg font-semibold tabular-nums ${accent ? "text-accent" : "text-text-primary"}`}>{value}</div>
</div>
);
}