SPB Git forge

spb/immbot-ai

Public
1commits 1branches 0releases
1.5 MBsize
maindefault branch
20 days agolast push
TypeScript 98.3% CSS 0.9% Shell 0.7%
7.9 KB · 180 lines tsx
Raw Blame History
1"use client";2// Vue générale : indicateurs clés, coût par jour (14 jours), modèles les plus3// utilisés (30 jours), état de la dernière ingestion.4import { Activity } from "lucide-react";5import { PageHeader } from "@/components/app-shell";6import { Badge, Card, EmptyState, ProgressBar, Skeleton } from "@/components/ui";7import {8  BarChart, ErrorBanner, SectionTitle, StatTile,9  fmtDate, fmtInt, fmtUSD, useFetchJson, type BarDatum,10} from "./shared";1112type Overview = {13  users: number;14  students: number;15  activeWeek: number;16  conversations: number;17  messages: number;18  documents: number;19  chunks: number;20  flagsOpen: number;21  apiErrors7d: number;22  costMonth: number;23  costToday: number;24  budgets: { dailyPerUserUSD: number; monthlyPerUserUSD: number; monthlyGlobalUSD: number; dailyRequestsPerUser: number };25  lastIngestion: { id: number; started_at: string; finished_at: string | null; status: string; chunks_created: number; files_ingested: number } | null;26  costByDay: { day: string; cost: number; calls: number }[];27  topModels: { model: string; calls: number; cost: number; tin: number | null; tout: number | null }[];28  invalidCitations7d: number;29};3031function ingestionTone(status: string): "green" | "red" | "amber" {32  const s = status.toLowerCase();33  if (["ok", "success", "succès", "completed", "done"].some((k) => s.includes(k))) return "green";34  if (["error", "fail", "échec", "erreur"].some((k) => s.includes(k))) return "red";35  return "amber";36}3738export function AdminOverview() {39  const { data, error, loading, reload } = useFetchJson<Overview>("/api/admin/overview");4041  if (loading) {42    return (43      <div className="animate-fade-up">44        <PageHeader title="Vue générale" subtitle="État de la plateforme en un coup d'œil" />45        <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">46          {Array.from({ length: 10 }).map((_, i) => (47            <Skeleton key={i} className="h-24" />48          ))}49        </div>50        <div className="mt-4 grid gap-4 lg:grid-cols-2">51          <Skeleton className="h-64" />52          <Skeleton className="h-64" />53        </div>54      </div>55    );56  }5758  if (error || !data) {59    return (60      <div>61        <PageHeader title="Vue générale" />62        <ErrorBanner message={error ?? "Données indisponibles."} onRetry={reload} />63      </div>64    );65  }6667  // Série des 14 derniers jours, jours sans usage inclus (valeur 0).68  const days: BarDatum[] = [];69  for (let i = 13; i >= 0; i--) {70    const d = new Date();71    d.setDate(d.getDate() - i);72    const key = d.toISOString().slice(0, 10);73    const row = data.costByDay.find((r) => r.day === key);74    days.push({ label: key.slice(5), value: row?.cost ?? 0, hint: `${fmtInt(row?.calls ?? 0)} appels` });75  }7677  const budgetRatio = data.budgets.monthlyGlobalUSD > 0 ? data.costMonth / data.budgets.monthlyGlobalUSD : 0;78  const ingest = data.lastIngestion;7980  return (81    <div className="animate-fade-up">82      <PageHeader title="Vue générale" subtitle="État de la plateforme en un coup d'œil" />8384      <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">85        <StatTile label="Utilisateurs" value={fmtInt(data.users)} sub={`${fmtInt(data.students)} étudiant·e·s`} />86        <StatTile label="Actifs (7 jours)" value={fmtInt(data.activeWeek)} />87        <StatTile label="Conversations" value={fmtInt(data.conversations)} />88        <StatTile label="Messages" value={fmtInt(data.messages)} />89        <StatTile label="Documents" value={fmtInt(data.documents)} sub={`${fmtInt(data.chunks)} fragments`} />90        <StatTile label="Coût du jour" value={fmtUSD(data.costToday)} />91        <StatTile92          label="Signalements ouverts"93          value={fmtInt(data.flagsOpen)}94          tone={data.flagsOpen > 0 ? "amber" : "default"}95        />96        <StatTile97          label="Erreurs API (7 j)"98          value={fmtInt(data.apiErrors7d)}99          tone={data.apiErrors7d > 0 ? "red" : "default"}100        />101        <StatTile102          label="Citations invalides (7 j)"103          value={fmtInt(data.invalidCitations7d)}104          tone={data.invalidCitations7d > 0 ? "amber" : "default"}105        />106        <Card className="p-4">107          <p className="text-[12px] font-medium text-muted">Coût du mois</p>108          <p className="mt-1 text-xl font-bold tabular-nums leading-tight text-fg">{fmtUSD(data.costMonth)}</p>109          <ProgressBar value={budgetRatio} tone={budgetRatio >= 0.9 ? "gold" : "brand"} className="mt-2" />110          <p className="mt-1 text-[12px] tabular-nums text-muted">111            {Math.round(budgetRatio * 100)} % du budget global ({fmtUSD(data.budgets.monthlyGlobalUSD, 0)}/mois)112          </p>113        </Card>114      </div>115116      <div className="mt-5 grid gap-4 lg:grid-cols-2">117        <Card className="p-5">118          <SectionTitle sub="Dépense quotidienne, tous modèles confondus">Coût par jour — 14 derniers jours</SectionTitle>119          <BarChart data={days} formatValue={(v) => fmtUSD(v, 4)} emptyLabel="Aucune dépense sur les 14 derniers jours." />120        </Card>121122        <Card className="p-5">123          <SectionTitle sub="Sur les 30 derniers jours">Modèles les plus utilisés</SectionTitle>124          {data.topModels.length === 0 ? (125            <EmptyState icon={<Activity />} title="Aucun appel de modèle" description="Les statistiques d'usage apparaîtront après les premières conversations." />126          ) : (127            <div className="overflow-x-auto">128              <table className="w-full text-[13px]">129                <thead>130                  <tr className="border-b border-app text-left text-[12px] text-muted">131                    <th className="py-2 pr-3 font-medium">Modèle</th>132                    <th className="py-2 pr-3 text-right font-medium">Appels</th>133                    <th className="py-2 pr-3 text-right font-medium">Coût</th>134                    <th className="py-2 text-right font-medium">Jetons (entrée / sortie)</th>135                  </tr>136                </thead>137                <tbody>138                  {data.topModels.map((m) => (139                    <tr key={m.model} className="border-b border-app last:border-0">140                      <td className="max-w-[220px] truncate py-2 pr-3 font-mono text-[12px] text-fg" title={m.model}>{m.model}</td>141                      <td className="py-2 pr-3 text-right tabular-nums text-fg">{fmtInt(m.calls)}</td>142                      <td className="py-2 pr-3 text-right tabular-nums text-fg">{fmtUSD(m.cost, 4)}</td>143                      <td className="py-2 text-right tabular-nums text-muted">144                        {fmtInt(m.tin ?? 0)} / {fmtInt(m.tout ?? 0)}145                      </td>146                    </tr>147                  ))}148                </tbody>149              </table>150            </div>151          )}152        </Card>153      </div>154155      <Card className="mt-4 p-5">156        <SectionTitle>Dernière ingestion du contenu</SectionTitle>157        {!ingest ? (158          <p className="text-sm text-muted">Aucune ingestion n'a encore été exécutée. Lancez-la depuis l'onglet « Cours &amp; contenu ».</p>159        ) : (160          <div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-[13px]">161            <Badge tone={ingestionTone(ingest.status)}>{ingest.status}</Badge>162            <span className="text-muted">163              Démarrée&nbsp;: <span className="tabular-nums text-fg">{fmtDate(ingest.started_at)}</span>164            </span>165            <span className="text-muted">166              Terminée&nbsp;: <span className="tabular-nums text-fg">{ingest.finished_at ? fmtDate(ingest.finished_at) : "en cours…"}</span>167            </span>168            <span className="text-muted">169              Fichiers ingérés&nbsp;: <span className="tabular-nums text-fg">{fmtInt(ingest.files_ingested)}</span>170            </span>171            <span className="text-muted">172              Fragments créés&nbsp;: <span className="tabular-nums text-fg">{fmtInt(ingest.chunks_created)}</span>173            </span>174          </div>175        )}176      </Card>177    </div>178  );179}180