import React, { useMemo, useState } from "react"; import { Card } from "@/components/ui/card"; import type { DailyUsage } from "@/lib/api"; interface TokenUsageTrendProps { days: DailyUsage[]; } // Simple number formatters const fmtTokens = (n: number) => { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return `${n}`; }; const fmtUSD = (n: number) => new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 4 }).format(n); /** * A lightweight multi-series line/area chart implemented with SVG and basic UI primitives. * - Left axis: Tokens (input/output/cache write/cache read) * - Right axis: Cost (USD) and Requests count (normalized to its own max) * - Tooltip closely matches the screenshot content */ export const TokenUsageTrend: React.FC = ({ days }) => { const [hoverIndex, setHoverIndex] = useState(null); const { labels, series, maxTokens, maxCost, maxReq } = useMemo(() => { const sorted = days.slice().reverse(); // chronological left->right const labels = sorted.map((d) => new Date(d.date.replace(/-/g, "/")).toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit" }) ); const series = { input: sorted.map((d) => d.input_tokens || 0), output: sorted.map((d) => d.output_tokens || 0), cacheW: sorted.map((d) => d.cache_creation_tokens || 0), cacheR: sorted.map((d) => d.cache_read_tokens || 0), cost: sorted.map((d) => d.total_cost || 0), reqs: sorted.map((d) => d.request_count || 0), sumTokens: sorted.map( (d) => (d.input_tokens || 0) + (d.output_tokens || 0) + (d.cache_creation_tokens || 0) + (d.cache_read_tokens || 0) ), } as const; const maxTokens = Math.max(1, ...series.sumTokens, ...series.input, ...series.output, ...series.cacheW, ...series.cacheR); const maxCost = Math.max(1, ...series.cost); const maxReq = Math.max(1, ...series.reqs); return { labels, series, maxTokens, maxCost, maxReq }; }, [days]); const width = 900; const height = 260; const padL = 56; // room for left ticks const padR = 56; // room for right ticks const padT = 16; const padB = 36; const plotW = width - padL - padR; const plotH = height - padT - padB; const n = labels.length; const x = (i: number) => padL + (plotW * i) / Math.max(1, n - 1); const yToken = (v: number) => padT + plotH * (1 - v / maxTokens); const yCost = (v: number) => padT + plotH * (1 - v / maxCost); const yReq = (v: number) => padT + plotH * (1 - v / maxReq); const pathFrom = (vals: number[], y: (v: number) => number) => vals.map((v, i) => `${i === 0 ? "M" : "L"} ${x(i)} ${y(v)}`).join(" "); const colors = { input: "#3b82f6", // blue-500 output: "#ec4899", // pink-500 cacheW: "#60a5fa", // blue-400 cacheR: "#a78bfa", // violet-400 cost: "#22c55e", // green-500 req: "#16a34a", // green-600 grid: "var(--border)", text: "var(--muted-foreground)", } as const; const hovered = hoverIndex != null ? hoverIndex : null; const renderTooltip = () => { if (hovered == null) return null; const dateText = new Date(days.slice().reverse()[hovered].date.replace(/-/g, "/")).toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit", }); const d = days.slice().reverse()[hovered]; return (
{dateText}
费用(USD):{fmtUSD(d.total_cost)}
缓存读取Token: {fmtTokens(d.cache_read_tokens || 0)} tokens
缓存创建Token: {fmtTokens(d.cache_creation_tokens || 0)} tokens
输出Token: {fmtTokens(d.output_tokens || 0)} tokens
输入Token: {fmtTokens(d.input_tokens || 0)} tokens
请求数:{d.request_count || 0} 次
); }; return (

Token使用趋势

{/* axes */} {/* left ticks (tokens) 0, 25%, 50%, 75%, 100% */} {[0, 0.25, 0.5, 0.75, 1].map((t) => ( {fmtTokens(Math.round(maxTokens * t))} ))} {/* right ticks (cost/requests) */} {[0, 0.5, 1].map((t) => ( {t === 1 ? fmtUSD(maxCost) : t === 0.5 ? fmtUSD(maxCost / 2) : "$0"} ))} {/* token lines */} {/* cost line (right axis) */} {/* requests as small circles on right scale */} {series.reqs.map((v, i) => ( ))} {/* x labels and hover hit-areas */} {labels.map((lab, i) => ( setHoverIndex(i)} onMouseLeave={() => setHoverIndex(null)}> {lab} {/* vertical hover guide */} {hoverIndex === i && ( )} {/* invisible hit area */} ))} {/* Tooltip container */} {hoverIndex != null && (
{renderTooltip()}
)}
{/* legend */}
输入Token
输出Token
缓存创建Token
缓存读取Token
费用(USD)
请求数
); };