146 lines
6.6 KiB
TypeScript
146 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useCallback, startTransition } from "react";
|
|
import { motion } from "motion/react";
|
|
import { Zap, Hash, Users, Cpu, DollarSign, TrendingUp, BarChart3 } from "lucide-react";
|
|
import { StatsCard } from "@/components/StatsCard";
|
|
import { TimeRangeSelector } from "@/components/TimeRangeSelector";
|
|
import { TrendChart } from "@/components/charts/TrendChart";
|
|
import { RankingBar } from "@/components/charts/RankingBar";
|
|
import { buildQuery } from "@/lib/utils";
|
|
import { useTimeRange } from "@/lib/time-range-context";
|
|
import { useI18n } from "@/lib/i18n";
|
|
|
|
interface OverviewData {
|
|
total_calls: number;
|
|
total_tokens: number;
|
|
total_quota: number;
|
|
active_users: number;
|
|
active_models: number;
|
|
}
|
|
|
|
interface TrendPoint {
|
|
date: string;
|
|
calls: number;
|
|
total_tokens: number;
|
|
prompt_tokens: number;
|
|
completion_tokens: number;
|
|
quota: number;
|
|
}
|
|
|
|
interface RankItem {
|
|
name: string;
|
|
total_tokens: number;
|
|
calls: number;
|
|
}
|
|
|
|
export default function DashboardPage() {
|
|
const { t } = useI18n();
|
|
const { getEffectiveRange } = useTimeRange();
|
|
const [granularity, setGranularity] = useState<"hour" | "day" | "week" | "month">("day");
|
|
const [trendMetric, setTrendMetric] = useState<"total_tokens" | "calls" | "quota">("total_tokens");
|
|
const [overview, setOverview] = useState<OverviewData | null>(null);
|
|
const [trends, setTrends] = useState<TrendPoint[]>([]);
|
|
const [userRank, setUserRank] = useState<RankItem[]>([]);
|
|
const [modelRank, setModelRank] = useState<RankItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const fetchData = useCallback(async () => {
|
|
startTransition(() => setLoading(true));
|
|
const { start, end } = getEffectiveRange();
|
|
const tp = { start, end };
|
|
const [ov, tr, ur, mr] = await Promise.all([
|
|
fetch(buildQuery("/api/overview", tp)).then(r => r.json()),
|
|
fetch(buildQuery("/api/trends", { ...tp, granularity })).then(r => r.json()),
|
|
fetch(buildQuery("/api/rankings", { ...tp, type: "user", limit: 10 })).then(r => r.json()),
|
|
fetch(buildQuery("/api/rankings", { ...tp, type: "model", limit: 10 })).then(r => r.json()),
|
|
]);
|
|
startTransition(() => {
|
|
setOverview(ov); setTrends(tr); setUserRank(ur); setModelRank(mr); setLoading(false);
|
|
});
|
|
}, [granularity, getEffectiveRange]);
|
|
|
|
useEffect(() => { fetchData(); }, [fetchData]);
|
|
|
|
const grans = [
|
|
{ key: "hour" as const, label: t("gran.hour") },
|
|
{ key: "day" as const, label: t("gran.day") },
|
|
{ key: "week" as const, label: t("gran.week") },
|
|
{ key: "month" as const, label: t("gran.month") },
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<motion.h1 initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="text-2xl font-bold gradient-text">
|
|
{t("dash.title")}
|
|
</motion.h1>
|
|
<TimeRangeSelector />
|
|
</div>
|
|
|
|
{overview && (
|
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
|
|
<StatsCard title={t("dash.totalCalls")} value={overview.total_calls} icon={Hash} delay={0} />
|
|
<StatsCard title={t("dash.tokenUsage")} value={overview.total_tokens} format="tokens" icon={Zap} delay={0.05} />
|
|
<StatsCard title={t("dash.totalCost")} value={overview.total_quota / 500000} format="usd" icon={DollarSign} delay={0.1} />
|
|
<StatsCard title={t("dash.activeUsers")} value={overview.active_users} icon={Users} delay={0.15} />
|
|
<StatsCard title={t("dash.activeModels")} value={overview.active_models} icon={Cpu} delay={0.2} />
|
|
</div>
|
|
)}
|
|
|
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.15 }} className="glass p-5">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<TrendingUp className="h-4 w-4" style={{ color: "var(--accent)", opacity: 0.6 }} />
|
|
<h2 className="text-sm font-semibold" style={{ color: "var(--text-primary)" }}>{t("dash.trend")}</h2>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<div className="flex gap-1 rounded-md p-0.5" style={{ background: "var(--row-hover)", border: "1px solid var(--surface-border)" }}>
|
|
{grans.map(g => (
|
|
<button key={g.key} onClick={() => setGranularity(g.key)}
|
|
className="px-2.5 py-1 text-xs rounded transition-colors"
|
|
style={{
|
|
background: granularity === g.key ? "var(--btn-active-bg)" : "transparent",
|
|
color: granularity === g.key ? "var(--text-accent)" : "var(--text-muted)",
|
|
border: granularity === g.key ? "1px solid var(--surface-border)" : "1px solid transparent",
|
|
}}
|
|
>{g.label}</button>
|
|
))}
|
|
</div>
|
|
<div className="flex gap-1 rounded-md p-0.5" style={{ background: "var(--row-hover)", border: "1px solid var(--surface-border)" }}>
|
|
{([["total_tokens", t("metric.token")], ["calls", t("metric.calls")], ["quota", t("metric.cost")]] as const).map(([k, l]) => (
|
|
<button key={k} onClick={() => setTrendMetric(k)}
|
|
className="px-2.5 py-1 text-xs rounded transition-colors"
|
|
style={{
|
|
background: trendMetric === k ? "var(--btn-active-bg)" : "transparent",
|
|
color: trendMetric === k ? "var(--text-accent)" : "var(--text-muted)",
|
|
border: trendMetric === k ? "1px solid var(--surface-border)" : "1px solid transparent",
|
|
}}
|
|
>{l}</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{loading ? (
|
|
<div className="flex h-64 items-center justify-center">
|
|
<div className="h-5 w-5 animate-spin rounded-full spinner" />
|
|
</div>
|
|
) : (
|
|
<TrendChart data={trends} metric={trendMetric} />
|
|
)}
|
|
</motion.div>
|
|
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.25 }} className="glass p-5">
|
|
<BarChart3 className="h-4 w-4 mb-1" style={{ color: "var(--accent)", opacity: 0.6 }} />
|
|
<RankingBar data={userRank} title={t("dash.userTop10")} />
|
|
</motion.div>
|
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.3 }} className="glass p-5">
|
|
<BarChart3 className="h-4 w-4 mb-1" style={{ color: "var(--accent-purple)", opacity: 0.6 }} />
|
|
<RankingBar data={modelRank} title={t("dash.modelTop10")} />
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|