Checkpoint: Classification IA en deux étapes (filtre pertinence + classification catégorie) intégrée dans le moteur RSS. Inclut : module aiClassifier.ts avec les deux prompts, intégration dans rssEngine.ts avec fallback sur règles, procédures tRPC reclassifyWithAI pour veille et AAP, badge IA violet dans VeilleDashboard, bouton "Classer avec l'IA" (admin), section "Analyse par l'IA" dans la boîte de dialogue détail, 7 nouveaux tests Vitest (20 tests au total). Migration BDD appliquée (4 nouveaux champs IA dans veille_items et aap_items).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "cf512535",
|
||||
"timestamp": 1780256687344
|
||||
"version": "dab72037",
|
||||
"timestamp": 1780479118639
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
BookOpen,
|
||||
Trash2,
|
||||
AlertTriangle,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -61,6 +62,10 @@ interface VeilleItem {
|
||||
lien: string | null;
|
||||
datePublication: Date | null;
|
||||
importedAt: Date;
|
||||
iaRelevant: boolean | null;
|
||||
iaCategorie: string | null;
|
||||
iaClassifiedBy: "ia" | "rules" | null;
|
||||
iaReason: string | null;
|
||||
}
|
||||
|
||||
const TYPE_LABELS: Record<TypeVeille, string> = {
|
||||
@@ -206,6 +211,47 @@ function VeilleDetailDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Analyse IA */}
|
||||
{item.iaClassifiedBy && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles size={14} className="text-violet-500" />
|
||||
<h3 className="text-sm font-semibold text-foreground">Analyse par l'IA</h3>
|
||||
</div>
|
||||
<div className={cn(
|
||||
"p-4 rounded-lg border space-y-2",
|
||||
item.iaRelevant === false
|
||||
? "bg-orange-50 border-orange-200"
|
||||
: item.iaClassifiedBy === "ia"
|
||||
? "bg-violet-50 border-violet-200"
|
||||
: "bg-muted/20 border-border/50"
|
||||
)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn(
|
||||
"inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full",
|
||||
item.iaRelevant === false
|
||||
? "bg-orange-100 text-orange-700 border border-orange-300"
|
||||
: item.iaClassifiedBy === "ia"
|
||||
? "bg-violet-100 text-violet-700 border border-violet-300"
|
||||
: "bg-muted text-muted-foreground border border-border"
|
||||
)}>
|
||||
<Sparkles size={10} />
|
||||
{item.iaClassifiedBy === "ia" ? "Classé par l'IA" : "Classé par règles"}
|
||||
</span>
|
||||
{item.iaRelevant === false && (
|
||||
<span className="text-xs text-orange-700 font-medium">Hors périmètre médico-social</span>
|
||||
)}
|
||||
{item.iaRelevant === true && item.iaCategorie && (
|
||||
<span className="text-xs text-violet-700 font-medium">{item.iaCategorie}</span>
|
||||
)}
|
||||
</div>
|
||||
{item.iaReason && (
|
||||
<p className="text-xs text-foreground/70 leading-relaxed italic">"{item.iaReason}"</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lien externe */}
|
||||
{item.lien && (
|
||||
<div className="pt-1 border-t border-border/50">
|
||||
@@ -232,6 +278,15 @@ export default function VeilleDashboard() {
|
||||
const { user } = useLocalAuth();
|
||||
const isAdmin = user?.role === "admin";
|
||||
const utils = trpc.useUtils();
|
||||
const reclassifyMutation = trpc.veille.reclassifyWithAI.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success(`Classification IA terminée — ${data.processed} article(s) traité(s)${data.errors > 0 ? `, ${data.errors} erreur(s)` : ""}`);
|
||||
utils.veille.list.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(`Erreur lors de la classification IA : ${err.message}`);
|
||||
},
|
||||
});
|
||||
const purgeMutation = trpc.veille.purge.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success(`Purge effectuée — ${data.deleted} entrée(s) supprimée(s)`);
|
||||
@@ -313,6 +368,18 @@ export default function VeilleDashboard() {
|
||||
<LayoutGrid size={15} />Vignettes
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2 border-primary/50 text-primary hover:bg-primary hover:text-primary-foreground"
|
||||
onClick={() => reclassifyMutation.mutate({ limit: 50 })}
|
||||
disabled={reclassifyMutation.isPending}
|
||||
title="Reclassifier les 50 prochains articles non encore traités par l'IA"
|
||||
>
|
||||
{reclassifyMutation.isPending ? <Loader2 size={15} className="animate-spin" /> : <Sparkles size={15} />}
|
||||
{reclassifyMutation.isPending ? "Classification en cours..." : "Classer avec l'IA"}
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2 border-destructive/50 text-destructive hover:bg-destructive hover:text-destructive-foreground ml-2">
|
||||
@@ -345,6 +412,7 @@ export default function VeilleDashboard() {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -442,7 +510,25 @@ function VeilleListView({ items, onDetail }: { items: VeilleItem[]; onDetail: (i
|
||||
{TYPE_LABELS[item.typeVeille as TypeVeille] || item.typeVeille}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground text-xs">{item.categorie || "—"}</td>
|
||||
<td className="px-4 py-3 text-xs">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-muted-foreground">{item.categorie || "—"}</span>
|
||||
{item.iaClassifiedBy === "ia" && (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded-full w-fit",
|
||||
item.iaRelevant === false
|
||||
? "bg-orange-100 text-orange-700 border border-orange-200"
|
||||
: "bg-violet-100 text-violet-700 border border-violet-200"
|
||||
)}
|
||||
title={item.iaReason || "Classé par l'IA"}
|
||||
>
|
||||
<Sparkles size={9} />
|
||||
{item.iaRelevant === false ? "Hors périmètre" : "IA"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground text-xs">{item.niveau || "—"}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground text-xs">{item.territoire || "—"}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground text-xs whitespace-nowrap">{formatDate(item.datePublication) || "—"}</td>
|
||||
|
||||
Reference in New Issue
Block a user