Python 64.6%
TypeScript 33.7%
CSS 0.8%
1import { Link } from 'react-router-dom';2import { useQuery } from '@tanstack/react-query';3import { ArrowLeft } from 'lucide-react';4import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';5import { api } from '@/lib/api';6import { Spinner } from '@/components/ui/spinner';7import { chartPalette } from '@/theme/uqo';89interface Costs { budget: { budget_usd: number; spent_usd: number; ratio: number }; daily: { day: string; model: string; cost_usd: number; tokens_in: number; tokens_out: number; calls: number }[]; by_course: { course: string; cost_usd: number; calls: number }[]; models: Record<string, string> }1011export function AdminCostsPage() {12 const { data } = useQuery({ queryKey: ['admin-costs'], queryFn: () => api<Costs>('/admin/costs?days=30') });13 if (!data) return <div className="flex justify-center py-20"><Spinner /></div>;14 const models = [...new Set(data.daily.map((d) => d.model))];15 const byDay = new Map<string, Record<string, number | string>>();16 for (const d of data.daily) {17 const row = byDay.get(d.day) || { day: d.day };18 row[d.model] = ((row[d.model] as number) || 0) + d.cost_usd;19 byDay.set(d.day, row);20 }21 const rows = [...byDay.values()].sort((a, b) => String(a.day).localeCompare(String(b.day)));22 const daysElapsed = new Date().getDate();23 const projected = daysElapsed ? (data.budget.spent_usd / daysElapsed) * 30 : 0;24 return (25 <div className="min-h-[100dvh] bg-neutral-surface">26 <header className="bg-uqo-gradient text-white"><div className="mx-auto max-w-[1100px] px-4 pt-[calc(12px+var(--safe-top))] pb-3 flex items-center gap-3"><Link to="/" className="h-11 w-11 inline-flex items-center justify-center rounded-xl hover:bg-white/10" aria-label="Retour"><ArrowLeft size={20} /></Link><h1 className="text-lg font-bold">Coûts OpenRouter</h1></div></header>27 <main className="mx-auto max-w-[1100px] px-4 py-5 space-y-4">28 <div className="grid grid-cols-2 md:grid-cols-4 gap-3">29 {[['Dépensé ce mois', `${data.budget.spent_usd.toFixed(2)} $`], ['Budget', `${data.budget.budget_usd} $`], ['Utilisation', `${Math.round(data.budget.ratio * 100)} %`], ['Projection mensuelle', `${projected.toFixed(2)} $`]].map(([l, v]) => (30 <div key={l} className="rounded-2xl bg-white shadow-card p-4"><div className="text-xs text-neutral-muted">{l}</div><div className="text-2xl font-bold text-uqo-blue-dark tabular-nums">{v}</div></div>31 ))}32 </div>33 <section className="rounded-2xl bg-white shadow-card p-4"><h2 className="text-sm font-semibold text-uqo-blue-dark mb-3">Coût par jour et par modèle ($ US)</h2>34 <div className="h-64"><ResponsiveContainer><BarChart data={rows}><CartesianGrid vertical={false} stroke="#EEF2F6" /><XAxis dataKey="day" tickFormatter={(d) => String(d).slice(5)} fontSize={11} /><YAxis fontSize={11} width={40} /><Tooltip formatter={(v: number) => v.toFixed(4)} /><Legend />{models.map((m, i) => <Bar key={m} dataKey={m} stackId="a" fill={chartPalette[i % chartPalette.length]} />)}</BarChart></ResponsiveContainer></div>35 </section>36 <section className="rounded-2xl bg-white shadow-card p-4"><h2 className="text-sm font-semibold text-uqo-blue-dark mb-3">Par cours</h2>37 <table className="text-sm w-full"><tbody>{data.by_course.map((c) => <tr key={c.course} className="border-t border-neutral-line/70"><td className="py-1.5">{c.course}</td><td className="tabular-nums text-right">{c.calls} appels</td><td className="tabular-nums text-right font-medium">{c.cost_usd.toFixed(3)} $</td></tr>)}</tbody></table>38 </section>39 <section className="rounded-2xl bg-white shadow-card p-4"><h2 className="text-sm font-semibold text-uqo-blue-dark mb-3">Modèles configurés</h2>40 <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">{Object.entries(data.models).map(([k, v]) => <><dt key={k} className="text-neutral-muted">{k}</dt><dd key={k + v} className="font-mono text-xs">{v}</dd></>)}</dl>41 </section>42 </main>43 </div>44 );45}46