2api
feat: configurable stream stall timeout + per-provider UI
88c4c60
Raw
History Blame Contribute Delete
4.75 kB
"use client";
import { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from "recharts";
import Card from "@/shared/components/Card";
const fmtTokens = (n) => {
if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
if (n >= 1000) return `${(n / 1000).toFixed(1)}K`;
return String(n || 0);
};
const fmtCost = (n) => `$${(n || 0).toFixed(4)}`;
export default function UsageChart({ period = "7d" }) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useState("tokens");
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await fetch(`/api/usage/chart?period=${period}`);
if (res.ok) {
const json = await res.json();
setData(json);
}
} catch (e) {
console.error("Failed to fetch chart data:", e);
} finally {
setLoading(false);
}
}, [period]);
useEffect(() => {
fetchData();
}, [fetchData]);
const hasData = data.some((d) => d.tokens > 0 || d.cost > 0);
return (
<Card className="flex min-w-0 flex-col gap-3 p-3 sm:p-4">
<div className="grid w-full grid-cols-2 items-center gap-1 rounded-lg border border-border bg-bg-subtle p-1 sm:w-auto sm:self-start">
<button
onClick={() => setViewMode("tokens")}
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${viewMode === "tokens" ? "bg-primary text-white shadow-sm" : "text-text-muted hover:text-text hover:bg-bg-hover"}`}
>
Tokens
</button>
<button
onClick={() => setViewMode("cost")}
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${viewMode === "cost" ? "bg-primary text-white shadow-sm" : "text-text-muted hover:text-text hover:bg-bg-hover"}`}
>
Cost
</button>
</div>
{loading ? (
<div className="h-48 flex items-center justify-center text-text-muted text-sm">Loading...</div>
) : !hasData ? (
<div className="h-48 flex items-center justify-center text-text-muted text-sm">No data for this period</div>
) : (
<ResponsiveContainer width="100%" height={220}>
<AreaChart data={data} margin={{ top: 4, right: 8, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="gradTokens" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.25} />
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
</linearGradient>
<linearGradient id="gradCost" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.25} />
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" strokeOpacity={0.1} />
<XAxis
dataKey="label"
tick={{ fontSize: 10, fill: "currentColor", fillOpacity: 0.5 }}
tickLine={false}
axisLine={false}
interval="preserveStartEnd"
/>
<YAxis
tick={{ fontSize: 10, fill: "currentColor", fillOpacity: 0.5 }}
tickLine={false}
axisLine={false}
tickFormatter={viewMode === "tokens" ? fmtTokens : fmtCost}
width={50}
/>
<Tooltip
contentStyle={{
backgroundColor: "var(--color-bg)",
border: "1px solid var(--color-border)",
borderRadius: "8px",
fontSize: "12px",
}}
formatter={(value, name) =>
name === "tokens" ? [fmtTokens(value), "Tokens"] : [fmtCost(value), "Cost"]
}
/>
{viewMode === "tokens" ? (
<Area
type="monotone"
dataKey="tokens"
stroke="#6366f1"
strokeWidth={2}
fill="url(#gradTokens)"
dot={false}
activeDot={{ r: 4 }}
/>
) : (
<Area
type="monotone"
dataKey="cost"
stroke="#f59e0b"
strokeWidth={2}
fill="url(#gradCost)"
dot={false}
activeDot={{ r: 4 }}
/>
)}
</AreaChart>
</ResponsiveContainer>
)}
</Card>
);
}
UsageChart.propTypes = {
period: PropTypes.string,
};