feat(cc-050): AI Costs & Usage page

This commit is contained in:
admin-valentin 2026-08-01 09:57:56 +00:00
parent 58b8a5f9ba
commit bafc493961

View file

@ -0,0 +1,149 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '../../../../lib/api';
import { useSession } from '../../../../components/session-provider';
interface AiCosts {
totals: { totalCostUsd: string | null; totalRequests: number };
byModel: { model: string; requests: number; costUsd: string | null }[];
byPurpose: { purpose: string; requests: number; costUsd: string | null }[];
byStatus: { resultStatus: string; requests: number }[];
}
export default function AiCostsPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data, isLoading } = useQuery({
queryKey: ['ai-costs', tenantId],
queryFn: () => apiFetch<AiCosts>('/v1/ai-requests/costs', { tenantId }),
enabled: Boolean(tenantId),
staleTime: 60_000,
});
const totalCost = Number(data?.totals?.totalCostUsd ?? 0);
const totalReqs = data?.totals?.totalRequests ?? 0;
return (
<div className="max-w-5xl space-y-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Costuri & Utilizare AI</h1>
<p className="mt-1 text-sm text-ink-faint">
Statistici agregate pentru toate cererile AI din workspace.
</p>
</div>
{isLoading ? (
<p className="text-sm text-ink-faint">Se încarcă</p>
) : (
<>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div className="card p-5">
<p className="text-xs font-medium uppercase tracking-wider text-ink-faint">Total cereri</p>
<p className="mt-1 font-display text-3xl font-semibold text-ink">{totalReqs}</p>
</div>
<div className="card p-5">
<p className="text-xs font-medium uppercase tracking-wider text-ink-faint">Cost total</p>
<p className="mt-1 font-mono text-2xl font-semibold text-ink">${totalCost.toFixed(4)}</p>
</div>
<div className="card p-5">
<p className="text-xs font-medium uppercase tracking-wider text-ink-faint">Cost mediu/cerere</p>
<p className="mt-1 font-mono text-2xl font-semibold text-ink">
{totalReqs > 0 ? `$${(totalCost / totalReqs).toFixed(4)}` : '—'}
</p>
</div>
<div className="card p-5">
<p className="text-xs font-medium uppercase tracking-wider text-ink-faint">Modele utilizate</p>
<p className="mt-1 font-display text-3xl font-semibold text-ink">
{data?.byModel?.length ?? 0}
</p>
</div>
</div>
{totalReqs === 0 ? (
<div className="card p-10 text-center">
<p className="text-sm font-medium text-ink">Nicio cerere AI înregistrată</p>
<p className="mt-1 text-xs text-ink-faint">
Costurile sunt înregistrate automat la utilizarea funcționalităților AI.
</p>
</div>
) : (
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<section className="card p-5">
<h2 className="mb-4 text-xs font-semibold uppercase tracking-wider text-ink-faint">
Cost per model
</h2>
<ul className="space-y-3">
{(data?.byModel ?? [])
.sort((a, b) => Number(b.costUsd ?? 0) - Number(a.costUsd ?? 0))
.map((m) => {
const pct = totalCost > 0 ? (Number(m.costUsd ?? 0) / totalCost) * 100 : 0;
return (
<li key={m.model}>
<div className="mb-1 flex items-center justify-between gap-2">
<span className="min-w-0 truncate font-mono text-xs text-ink">{m.model}</span>
<span className="shrink-0 font-mono text-xs text-ink-faint">
${Number(m.costUsd ?? 0).toFixed(4)} · {m.requests}x
</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-paper-sunken">
<div className="h-full rounded-full bg-bronze-deep" style={{ width: `${pct}%` }} />
</div>
</li>
);
})}
</ul>
</section>
<section className="card p-5">
<h2 className="mb-4 text-xs font-semibold uppercase tracking-wider text-ink-faint">
Cost per scop
</h2>
<ul className="space-y-3">
{(data?.byPurpose ?? [])
.sort((a, b) => Number(b.costUsd ?? 0) - Number(a.costUsd ?? 0))
.slice(0, 8)
.map((p) => {
const pct = totalCost > 0 ? (Number(p.costUsd ?? 0) / totalCost) * 100 : 0;
return (
<li key={p.purpose}>
<div className="mb-1 flex items-center justify-between gap-2">
<span className="min-w-0 flex-1 truncate text-xs text-ink capitalize">
{p.purpose.replace(/_/g, ' ')}
</span>
<span className="shrink-0 font-mono text-xs text-ink-faint">
${Number(p.costUsd ?? 0).toFixed(4)}
</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-paper-sunken">
<div className="h-full rounded-full bg-ink-soft" style={{ width: `${pct}%` }} />
</div>
</li>
);
})}
</ul>
</section>
<section className="card p-5 lg:col-span-2">
<h2 className="mb-4 text-xs font-semibold uppercase tracking-wider text-ink-faint">
Distribuție status
</h2>
<ul className="flex flex-wrap gap-4">
{(data?.byStatus ?? []).map((s) => (
<li key={s.resultStatus} className="flex items-center gap-2">
<span className="rounded-full bg-paper-sunken px-3 py-1 text-xs font-medium capitalize text-ink">
{s.resultStatus.replace(/_/g, ' ')}
</span>
<span className="font-mono text-sm font-semibold text-ink">{s.requests}</span>
</li>
))}
</ul>
</section>
</div>
)}
</>
)}
</div>
);
}