feat(usage): add usage dashboard page with charts and navigation

This commit is contained in:
Gemini Nexus Bot committed 2026-04-22 13:30:58 -03:00
1 parent 1a1c0acd16
commit a225791e71
3 files changed
+270 -3

No files matched your search

+6
View File
@@ -9,6 +9,7 @@ import {
ChevronLeft,
ChevronRight,
RefreshCw,
BarChart3,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
@@ -40,6 +41,11 @@ export const MainLayout: React.FC = () => {
icon: LayoutDashboard,
label: t('nav.accounts'),
},
{
to: '/usage',
icon: BarChart3,
label: t('nav.usage'),
},
{
to: '/proxy',
icon: Network,
+21 -3
View File
@@ -9,10 +9,16 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as UsageRouteImport } from './routes/usage'
import { Route as SettingsRouteImport } from './routes/settings'
import { Route as ProxyRouteImport } from './routes/proxy'
import { Route as IndexRouteImport } from './routes/index'
const UsageRoute = UsageRouteImport.update({
id: '/usage',
path: '/usage',
getParentRoute: () => rootRouteImport,
} as any)
const SettingsRoute = SettingsRouteImport.update({
id: '/settings',
path: '/settings',
@@ -33,34 +39,45 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/proxy': typeof ProxyRoute
'/settings': typeof SettingsRoute
'/usage': typeof UsageRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/proxy': typeof ProxyRoute
'/settings': typeof SettingsRoute
'/usage': typeof UsageRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/proxy': typeof ProxyRoute
'/settings': typeof SettingsRoute
'/usage': typeof UsageRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/proxy' | '/settings'
fullPaths: '/' | '/proxy' | '/settings' | '/usage'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/proxy' | '/settings'
id: '__root__' | '/' | '/proxy' | '/settings'
to: '/' | '/proxy' | '/settings' | '/usage'
id: '__root__' | '/' | '/proxy' | '/settings' | '/usage'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ProxyRoute: typeof ProxyRoute
SettingsRoute: typeof SettingsRoute
UsageRoute: typeof UsageRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/usage': {
id: '/usage'
path: '/usage'
fullPath: '/usage'
preLoaderRoute: typeof UsageRouteImport
parentRoute: typeof rootRouteImport
}
'/settings': {
id: '/settings'
path: '/settings'
@@ -89,6 +106,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ProxyRoute: ProxyRoute,
SettingsRoute: SettingsRoute,
UsageRoute: UsageRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
+243
View File
@@ -0,0 +1,243 @@
import { createFileRoute } from '@tanstack/react-router';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useTranslation } from 'react-i18next';
import { Loader2, TrendingUp, TrendingDown, Activity, Layers } from 'lucide-react';
import { ipc } from '@/ipc/manager';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
Legend,
} from 'recharts';
import { useMemo, useState } from 'react';
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4'];
function UsagePage() {
const { t } = useTranslation();
const [range, setRange] = useState<'24h' | '7d' | '30d'>('7d');
function getTimeRange(r: '24h' | '7d' | '30d') {
const now = Date.now();
if (r === '24h') {
return { start: now - 24 * 60 * 60 * 1000, end: now };
}
if (r === '7d') {
return { start: now - 7 * 24 * 60 * 60 * 1000, end: now };
}
return { start: now - 30 * 24 * 60 * 60 * 1000, end: now };
}
const { data: dailyData, isLoading: dailyLoading } = useQuery({
queryKey: ['usage', 'day', range],
queryFn: () => {
const { start, end } = getTimeRange(range);
return ipc.client.usage.getUsageByDay({ start, end });
},
});
const { data: modelData, isLoading: modelLoading } = useQuery({
queryKey: ['usage', 'model', range],
queryFn: () => {
const { start, end } = getTimeRange(range);
return ipc.client.usage.getUsageByModel({ start, end });
},
});
const { data: hourlyData, isLoading: hourlyLoading } = useQuery({
queryKey: ['usage', 'hour', range],
queryFn: () => {
const { start, end } = getTimeRange(range);
return ipc.client.usage.getUsageByHour({ start, end });
},
});
const isLoading = dailyLoading || modelLoading || hourlyLoading;
const totals = useMemo(() => {
const source = dailyData || [];
return source.reduce(
(acc, row) => ({
promptTokens: acc.promptTokens + row.promptTokens,
completionTokens: acc.completionTokens + row.completionTokens,
totalTokens: acc.totalTokens + row.totalTokens,
requests: acc.requests + row.requests,
}),
{ promptTokens: 0, completionTokens: 0, totalTokens: 0, requests: 0 },
);
}, [dailyData]);
const chartData = useMemo(() => {
return (dailyData || []).map((d) => ({
name: d.bucket,
Prompt: d.promptTokens,
Completion: d.completionTokens,
Total: d.totalTokens,
}));
}, [dailyData]);
const pieData = useMemo(() => {
return (modelData || []).map((d) => ({
name: d.model,
value: d.totalTokens,
}));
}, [modelData]);
const hourlyChartData = useMemo(() => {
return (hourlyData || []).map((d) => ({
name: d.bucket,
Total: d.totalTokens,
}));
}, [hourlyData]);
if (isLoading) {
return (
<div className="flex h-full items-center justify-center">
<Loader2 className="animate-spin" />
</div>
);
}
return (
<div className="container mx-auto max-w-6xl space-y-6 p-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight">{t('usage.title')}</h2>
<p className="text-muted-foreground mt-1">{t('usage.description')}</p>
</div>
<Tabs value={range} onValueChange={(v) => setRange(v as '24h' | '7d' | '30d')}>
<TabsList>
<TabsTrigger value="24h">{t('usage.range24h')}</TabsTrigger>
<TabsTrigger value="7d">{t('usage.range7d')}</TabsTrigger>
<TabsTrigger value="30d">{t('usage.range30d')}</TabsTrigger>
</TabsList>
</Tabs>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{t('usage.totalTokens')}</CardTitle>
<Activity className="text-muted-foreground h-4 w-4" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totals.totalTokens.toLocaleString()}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{t('usage.promptTokens')}</CardTitle>
<TrendingUp className="text-muted-foreground h-4 w-4" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totals.promptTokens.toLocaleString()}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{t('usage.completionTokens')}</CardTitle>
<TrendingDown className="text-muted-foreground h-4 w-4" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totals.completionTokens.toLocaleString()}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{t('usage.totalRequests')}</CardTitle>
<Layers className="text-muted-foreground h-4 w-4" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totals.requests.toLocaleString()}</div>
</CardContent>
</Card>
</div>
{/* Charts */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>{t('usage.dailyUsage')}</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="Prompt" fill="#3b82f6" />
<Bar dataKey="Completion" fill="#10b981" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t('usage.byModel')}</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={pieData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={5}
dataKey="value"
label
>
{pieData.map((_, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{range === '24h' && (
<Card>
<CardHeader>
<CardTitle>{t('usage.hourlyUsage')}</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={hourlyChartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="Total" fill="#8b5cf6" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
)}
</div>
);
}
export const Route = createFileRoute('/usage')({
component: UsagePage,
});