feat: finaliser l’intégration au Référentiel IA
All checks were successful
Deploy Production / deploy (push) Has been skipped
Deploy Recette / deploy (push) Successful in 1m4s

This commit is contained in:
manus-admin
2026-10-06 16:58:11 +02:00
parent fd58573685
commit ba34233189
9 changed files with 218 additions and 183 deletions

View File

@@ -0,0 +1,50 @@
import { Activity, CheckCircle2, CircleAlert, Link2, Loader2, ShieldCheck } from "lucide-react";
import { toast } from "sonner";
import { trpc } from "@/lib/trpc";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
const STATE_LABELS = { available: "Connecté", unconfigured: "Non configuré", authentication_error: "Erreur d’authentification", unreachable: "Indisponible" } as const;
const ORIGIN_LABELS = { proposal: "Proposition du Référentiel", manual: "Choix administrateur", local: "Moteur IA local" } as const;
function displayDate(value?: string) {
if (!value) return "Aucun contrôle";
const date = new Date(value);
return Number.isNaN(date.getTime()) ? "Aucun contrôle" : date.toLocaleString("fr-FR", { dateStyle: "short", timeStyle: "short" });
}
export function ReferentielIaCard() {
const statusQuery = trpc.aiReference.status.useQuery();
const testMutation = trpc.aiReference.test.useMutation({
onSuccess: async result => {
await statusQuery.refetch();
if (result.state === "available") toast.success("Connexion au Référentiel IA vérifiée");
else toast.error("Connexion Référentiel IA non opérationnelle", { description: result.message });
},
onError: error => toast.error("Test du Référentiel IA impossible", { description: error.message }),
});
const status = statusQuery.data;
const connected = status?.state === "available";
return <Card className="border-primary/20 bg-gradient-to-br from-primary/[0.03] to-transparent">
<CardHeader className="pb-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex gap-3">
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary"><Link2 size={19} /></div>
<div><CardTitle className="text-base">Intégration Référentiel IA</CardTitle><CardDescription className="mt-1">Le moteur est décidé et appelé exclusivement par la passerelle Santinova.</CardDescription></div>
</div>
<Button variant="outline" size="sm" className="gap-2 self-start" onClick={() => testMutation.mutate()} disabled={testMutation.isPending}>
{testMutation.isPending ? <Loader2 size={14} className="animate-spin" /> : <Activity size={14} />} Tester la connexion au Référentiel
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{statusQuery.isLoading ? <div className="flex items-center gap-2 rounded-lg bg-muted/50 px-3 py-3 text-sm text-muted-foreground"><Loader2 size={15} className="animate-spin" /> Vérification de la configuration serveur…</div> : statusQuery.error ? <div className="flex items-start gap-2 rounded-lg border border-red-200 bg-red-50 px-3 py-3 text-sm text-red-800"><CircleAlert size={16} className="mt-0.5 shrink-0" /> Impossible de vérifier le Référentiel IA : {statusQuery.error.message}</div> : <>
<div className="flex flex-wrap gap-2"><Badge className={connected ? "bg-emerald-100 text-emerald-800" : "bg-amber-100 text-amber-800"}>{connected ? <CheckCircle2 size={13} className="mr-1" /> : <CircleAlert size={13} className="mr-1" />}{status ? STATE_LABELS[status.state] : "Indisponible"}</Badge><Badge variant="outline">Moteur : {status?.engine?.configured ? "Prêt" : "Non configuré"}</Badge></div>
<div className="grid gap-3 text-sm sm:grid-cols-2"><div className="rounded-lg border bg-background/70 p-3"><p className="text-xs text-muted-foreground">Moteur choisi</p><p className="mt-1 font-medium">{status?.engine?.name ?? "Non disponible"}</p><p className="mt-0.5 text-xs text-muted-foreground">{status?.engine ? `${status.engine.provider} · ${status.engine.model ?? "Modèle non disponible"}` : "Aucune décision disponible"}</p></div><div className="rounded-lg border bg-background/70 p-3"><p className="text-xs text-muted-foreground">Origine de la décision</p><p className="mt-1 font-medium">{status?.engine ? ORIGIN_LABELS[status.engine.origin] : "Non disponible"}</p><p className="mt-0.5 text-xs text-muted-foreground">Dernier contrôle : {displayDate(status?.checkedAt)}</p></div></div>
<div className="flex items-start gap-2 rounded-lg border border-primary/15 bg-primary/5 px-3 py-2.5 text-xs text-muted-foreground"><ShieldCheck size={15} className="mt-0.5 shrink-0 text-primary" /><span>{status?.message ?? "Le statut Référentiel IA sera disponible après vérification."}</span></div>
</>}
</CardContent>
</Card>;
}

View File

@@ -7,6 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { ReferentielIaCard } from "@/components/ReferentielIaCard";
import {
Settings as SettingsIcon,
HardDrive,
@@ -362,11 +363,16 @@ export default function SettingsPage() {
<Tabs defaultValue="source">
<TabsList className="bg-muted/50">
<TabsTrigger value="ai">Référentiel IA</TabsTrigger>
<TabsTrigger value="source">Source des fichiers</TabsTrigger>
<TabsTrigger value="import">Import & Planification</TabsTrigger>
<TabsTrigger value="auth">Authentification</TabsTrigger>
</TabsList>
<TabsContent value="ai" className="space-y-4 mt-4">
<ReferentielIaCard />
</TabsContent>
{/* ─── Source des fichiers ─────────────────────────────────────── */}
<TabsContent value="source" className="space-y-4 mt-4">