SPB Git forge

spb/uqo-chat

Public
14commits 1branches 0releases
1.4 MBsize
maindefault branch
17 days agolast push
Python 64.6% TypeScript 33.7% CSS 0.8%
4.0 KB · 46 lines tsx
Raw Blame History
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