142 lines
6.2 KiB
TypeScript
142 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from "recharts";
|
|
import { formatTokens, formatUSD } from "@/lib/utils";
|
|
import { useI18n } from "@/lib/i18n";
|
|
|
|
interface TrendPoint { date: string; calls: number; total_tokens: number; prompt_tokens: number; completion_tokens: number; quota?: number; }
|
|
|
|
export function TrendChart({ data, metric = "total_tokens" }: { data: TrendPoint[]; metric?: "total_tokens" | "calls" | "quota" }) {
|
|
const { t, locale } = useI18n();
|
|
|
|
if (!data.length)
|
|
return <div className="flex h-64 items-center justify-center" style={{ color: "var(--text-muted)" }}>{t("common.noData")}</div>;
|
|
|
|
const parseTrendDate = (dateStr: string) => {
|
|
const [datePart, timePart] = dateStr.replace("T", " ").split(" ");
|
|
const [year, month, day] = datePart.split("-").map(Number);
|
|
const hour = timePart ? timePart.slice(0, 5) : "";
|
|
return { year, month, day, hour };
|
|
};
|
|
|
|
const formatDateLabel = (dateStr: string) => {
|
|
const { year, month, day, hour } = parseTrendDate(dateStr);
|
|
const d = new Date(year, month - 1, day);
|
|
if (locale === "zh") {
|
|
return hour ? `${month}/${day} ${hour}` : `${month}/${day}`;
|
|
}
|
|
const dateLabel = d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
return hour ? `${dateLabel}, ${hour}` : dateLabel;
|
|
};
|
|
|
|
const formatTooltipLabel = (label: string) => {
|
|
const { year, month, day, hour } = parseTrendDate(label);
|
|
const d = new Date(year, month - 1, day);
|
|
if (locale === "zh") {
|
|
return hour ? `${year}/${month}/${day} ${hour}` : `${year}/${month}/${day}`;
|
|
}
|
|
const dateLabel = d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
return hour ? `${dateLabel}, ${hour}` : dateLabel;
|
|
};
|
|
|
|
const tooltipStyle = {
|
|
background: "var(--surface)",
|
|
border: "1px solid var(--surface-border)",
|
|
borderRadius: "8px",
|
|
color: "var(--foreground)",
|
|
fontSize: "12px",
|
|
backdropFilter: "blur(16px)",
|
|
};
|
|
|
|
// Token 模式:输入和输出数量级差距大,用双 Y 轴
|
|
if (metric === "total_tokens") {
|
|
return (
|
|
<ResponsiveContainer width="100%" height={320}>
|
|
<LineChart data={data} margin={{ top: 5, right: 60, left: 10, bottom: 5 }}>
|
|
<defs>
|
|
<linearGradient id="gradCyan" x1="0" y1="0" x2="1" y2="0">
|
|
<stop offset="0%" stopColor="var(--accent)" />
|
|
<stop offset="100%" stopColor="var(--accent-purple)" />
|
|
</linearGradient>
|
|
<linearGradient id="gradPurple" x1="0" y1="0" x2="1" y2="0">
|
|
<stop offset="0%" stopColor="var(--accent-purple)" />
|
|
<stop offset="100%" stopColor="var(--accent-pink)" />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--chart-grid)" />
|
|
<XAxis dataKey="date" tickFormatter={formatDateLabel} tick={{ fontSize: 11 }} stroke="var(--chart-grid)" />
|
|
{/* 左 Y 轴:输入 */}
|
|
<YAxis
|
|
yAxisId="left"
|
|
tickFormatter={(v) => formatTokens(v)}
|
|
tick={{ fontSize: 10 }}
|
|
stroke="var(--chart-grid)"
|
|
label={{ value: t("th.input"), angle: -90, position: "insideLeft", style: { fontSize: 10, fill: "var(--text-muted)" } }}
|
|
/>
|
|
{/* 右 Y 轴:输出 */}
|
|
<YAxis
|
|
yAxisId="right"
|
|
orientation="right"
|
|
tickFormatter={(v) => formatTokens(v)}
|
|
tick={{ fontSize: 10 }}
|
|
stroke="var(--chart-grid)"
|
|
label={{ value: t("th.output"), angle: 90, position: "insideRight", style: { fontSize: 10, fill: "var(--text-muted)" } }}
|
|
/>
|
|
<Tooltip
|
|
contentStyle={tooltipStyle}
|
|
labelFormatter={(label) => formatTooltipLabel(String(label))}
|
|
formatter={(value, name) => [
|
|
formatTokens(Number(value)),
|
|
name === t("th.input") ? t("th.input") : t("th.output"),
|
|
]}
|
|
/>
|
|
<Legend />
|
|
<Line yAxisId="left" type="monotone" dataKey="prompt_tokens" name={t("th.input")} stroke="url(#gradCyan)" strokeWidth={2} dot={false} />
|
|
<Line yAxisId="right" type="monotone" dataKey="completion_tokens" name={t("th.output")} stroke="url(#gradPurple)" strokeWidth={2} dot={false} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
);
|
|
}
|
|
|
|
// 金额模式:单 Y 轴
|
|
if (metric === "quota") {
|
|
return (
|
|
<ResponsiveContainer width="100%" height={320}>
|
|
<LineChart data={data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--chart-grid)" />
|
|
<XAxis dataKey="date" tickFormatter={formatDateLabel} tick={{ fontSize: 11 }} stroke="var(--chart-grid)" />
|
|
<YAxis tickFormatter={(v) => formatUSD(v / 500000)} tick={{ fontSize: 11 }} stroke="var(--chart-grid)" />
|
|
<Tooltip
|
|
contentStyle={tooltipStyle}
|
|
labelFormatter={(label) => formatTooltipLabel(String(label))}
|
|
formatter={(value) => [formatUSD(Number(value) / 500000), t("th.cost")]}
|
|
/>
|
|
<Legend />
|
|
<Line type="monotone" dataKey="quota" name={t("th.cost")} stroke="var(--accent)" strokeWidth={2} dot={false} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
);
|
|
}
|
|
|
|
// 调用量模式:单 Y 轴
|
|
return (
|
|
<ResponsiveContainer width="100%" height={320}>
|
|
<LineChart data={data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--chart-grid)" />
|
|
<XAxis dataKey="date" tickFormatter={formatDateLabel} tick={{ fontSize: 11 }} stroke="var(--chart-grid)" />
|
|
<YAxis tickFormatter={(v) => formatTokens(v)} tick={{ fontSize: 11 }} stroke="var(--chart-grid)" />
|
|
<Tooltip
|
|
contentStyle={tooltipStyle}
|
|
labelFormatter={(label) => formatTooltipLabel(String(label))}
|
|
formatter={(value, name) => [
|
|
formatTokens(Number(value)),
|
|
name === t("th.calls") ? t("th.calls") : String(name),
|
|
]}
|
|
/>
|
|
<Legend />
|
|
<Line type="monotone" dataKey="calls" name={t("th.calls")} stroke="var(--accent)" strokeWidth={2} dot={false} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
);
|
|
}
|