Checkpoint: isSubscription appliqué lors des imports. Seuil de confiance persisté en base (champ learningConfidenceThreshold dans userSettings). LearningSettings.tsx charge et sauvegarde le seuil via settings.get/upsert.

This commit is contained in:
Manus
2026-04-12 15:27:19 -04:00
parent 95aee693ea
commit 132f8ccec2
6 changed files with 1752 additions and 9 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import DashboardLayout from "@/components/DashboardLayout"; import DashboardLayout from "@/components/DashboardLayout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -34,14 +34,31 @@ const FIELD_COLORS: Record<string, string> = {
isSubscription: "bg-orange-100 text-orange-800", isSubscription: "bg-orange-100 text-orange-800",
}; };
const DEFAULT_CONFIDENCE_THRESHOLD = 2; const DEFAULT_THRESHOLD = 2;
export default function LearningSettings() { export default function LearningSettings() {
const utils = trpc.useUtils(); const utils = trpc.useUtils();
const { data: learnings, isLoading } = trpc.learnings.list.useQuery(); const { data: learnings, isLoading } = trpc.learnings.list.useQuery();
const [confidenceThreshold, setConfidenceThreshold] = useState(DEFAULT_CONFIDENCE_THRESHOLD); const { data: settings, isLoading: settingsLoading } = trpc.settings.get.useQuery();
const [confidenceThreshold, setConfidenceThreshold] = useState(DEFAULT_THRESHOLD);
const [editingThreshold, setEditingThreshold] = useState(false); const [editingThreshold, setEditingThreshold] = useState(false);
const [thresholdInput, setThresholdInput] = useState(String(DEFAULT_CONFIDENCE_THRESHOLD)); const [thresholdInput, setThresholdInput] = useState(String(DEFAULT_THRESHOLD));
// Sync threshold from DB when settings load
useEffect(() => {
if (settings?.learningConfidenceThreshold != null) {
setConfidenceThreshold(settings.learningConfidenceThreshold);
setThresholdInput(String(settings.learningConfidenceThreshold));
}
}, [settings?.learningConfidenceThreshold]);
const upsertSettingsMutation = trpc.settings.upsert.useMutation({
onSuccess: () => {
utils.settings.get.invalidate();
},
onError: () => toast.error("Erreur lors de la sauvegarde du seuil"),
});
const deleteMutation = trpc.learnings.delete.useMutation({ const deleteMutation = trpc.learnings.delete.useMutation({
onSuccess: () => { onSuccess: () => {
@@ -97,6 +114,7 @@ export default function LearningSettings() {
} }
setConfidenceThreshold(val); setConfidenceThreshold(val);
setEditingThreshold(false); setEditingThreshold(false);
upsertSettingsMutation.mutate({ learningConfidenceThreshold: val });
toast.success(`Seuil de confiance mis à jour : ${val} application(s)`); toast.success(`Seuil de confiance mis à jour : ${val} application(s)`);
}; };
@@ -184,10 +202,15 @@ export default function LearningSettings() {
<CardDescription> <CardDescription>
Un apprentissage est marqué <strong>Confirmé</strong> quand il a é appliqué au moins{" "} Un apprentissage est marqué <strong>Confirmé</strong> quand il a é appliqué au moins{" "}
<strong>{confidenceThreshold} fois</strong>. En dessous, il est <strong>En observation</strong>. <strong>{confidenceThreshold} fois</strong>. En dessous, il est <strong>En observation</strong>.
Ce réglage est sauvegardé et partagé entre toutes vos sessions.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{editingThreshold ? ( {settingsLoading ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<RefreshCw className="w-4 h-4 animate-spin" /> Chargement...
</div>
) : editingThreshold ? (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Label htmlFor="threshold" className="text-sm whitespace-nowrap"> <Label htmlFor="threshold" className="text-sm whitespace-nowrap">
Nombre minimum d'applications : Nombre minimum d'applications :
@@ -201,7 +224,9 @@ export default function LearningSettings() {
onChange={(e) => setThresholdInput(e.target.value)} onChange={(e) => setThresholdInput(e.target.value)}
className="w-24" className="w-24"
/> />
<Button size="sm" onClick={handleSaveThreshold}>Appliquer</Button> <Button size="sm" onClick={handleSaveThreshold} disabled={upsertSettingsMutation.isPending}>
{upsertSettingsMutation.isPending ? <RefreshCw className="w-4 h-4 animate-spin" /> : "Appliquer"}
</Button>
<Button size="sm" variant="ghost" onClick={() => { setEditingThreshold(false); setThresholdInput(String(confidenceThreshold)); }}> <Button size="sm" variant="ghost" onClick={() => { setEditingThreshold(false); setThresholdInput(String(confidenceThreshold)); }}>
Annuler Annuler
</Button> </Button>
@@ -229,8 +254,8 @@ export default function LearningSettings() {
</CardHeader> </CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-1"> <CardContent className="text-sm text-muted-foreground space-y-1">
<p> <p>
Quand vous modifiez manuellement le <strong>type d'achat</strong>, le <strong>service concerné</strong> ou Quand vous modifiez manuellement le <strong>type d'achat</strong>, le <strong>service concerné</strong>,
la <strong>ventilation comptable</strong> d'une facture (dans le détail ou dans la liste BAP), le système mémorise cette la <strong>ventilation comptable</strong> ou le champ <strong>Abonnement</strong> d'une facture, le système mémorise cette
correction pour ce fournisseur. correction pour ce fournisseur.
</p> </p>
<p> <p>
@@ -256,7 +281,7 @@ export default function LearningSettings() {
<Brain className="w-12 h-12 text-gray-300 mb-4" /> <Brain className="w-12 h-12 text-gray-300 mb-4" />
<h3 className="text-lg font-semibold text-gray-500">Aucun apprentissage enregistré</h3> <h3 className="text-lg font-semibold text-gray-500">Aucun apprentissage enregistré</h3>
<p className="text-sm text-muted-foreground mt-2 max-w-sm"> <p className="text-sm text-muted-foreground mt-2 max-w-sm">
Modifiez manuellement les champs d'une facture (type d'achat, service, ventilation) pour que le système Modifiez manuellement les champs d'une facture (type d'achat, service, ventilation, abonnement) pour que le système
commence à apprendre. commence à apprendre.
</p> </p>
</CardContent> </CardContent>

View File

@@ -0,0 +1 @@
ALTER TABLE `userSettings` ADD `learningConfidenceThreshold` int DEFAULT 2 NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@@ -134,6 +134,13 @@
"when": 1776019747088, "when": 1776019747088,
"tag": "0018_warm_changeling", "tag": "0018_warm_changeling",
"breakpoints": true "breakpoints": true
},
{
"idx": 19,
"version": "5",
"when": 1776021930426,
"tag": "0019_smooth_lord_hawal",
"breakpoints": true
} }
] ]
} }

View File

@@ -146,6 +146,8 @@ export const userSettings = mysqlTable("userSettings", {
sftpAutoExport: int("sftpAutoExport").default(0).notNull(), // 0 = manual, 1 = automatic sftpAutoExport: int("sftpAutoExport").default(0).notNull(), // 0 = manual, 1 = automatic
// LLM Logs retention // LLM Logs retention
llmLogsRetentionMonths: int("llmLogsRetentionMonths").default(3).notNull(), // Durée de conservation des logs LLM en mois (défaut: 3 mois) llmLogsRetentionMonths: int("llmLogsRetentionMonths").default(3).notNull(), // Durée de conservation des logs LLM en mois (défaut: 3 mois)
// Seuil de confiance pour les apprentissages IA
learningConfidenceThreshold: int("learningConfidenceThreshold").default(2).notNull(), // Nombre minimum d'applications pour marquer un apprentissage comme Confirmé
createdAt: timestamp("createdAt").defaultNow().notNull(), createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
}); });

View File

@@ -299,11 +299,16 @@ export const appRouter = router({
const learnings = await getLearningsBySupplier(userId, newInvoice.supplierName); const learnings = await getLearningsBySupplier(userId, newInvoice.supplierName);
if (learnings.length > 0) { if (learnings.length > 0) {
const learningUpdates: Record<string, string> = {}; const learningUpdates: Record<string, string> = {};
const isSubscriptionLearning = learnings.find(l => l.fieldName === 'isSubscription');
for (const learning of learnings) { for (const learning of learnings) {
if (learning.fieldName === 'typeAchat' || learning.fieldName === 'serviceConcerne' || learning.fieldName === 'ventilationComptable') { if (learning.fieldName === 'typeAchat' || learning.fieldName === 'serviceConcerne' || learning.fieldName === 'ventilationComptable') {
learningUpdates[learning.fieldName] = learning.correctedValue; learningUpdates[learning.fieldName] = learning.correctedValue;
} }
} }
// Appliquer l'apprentissage isSubscription
if (isSubscriptionLearning) {
(learningUpdates as any)['isSubscription'] = isSubscriptionLearning.correctedValue === 'OUI' ? 1 : 0;
}
if (Object.keys(learningUpdates).length > 0) { if (Object.keys(learningUpdates).length > 0) {
await updateInvoice(newInvoice.id, learningUpdates as any); await updateInvoice(newInvoice.id, learningUpdates as any);
console.log(`[Learning] Applied ${Object.keys(learningUpdates).length} learning(s) to invoice ${newInvoice.id} (${newInvoice.supplierName})`); console.log(`[Learning] Applied ${Object.keys(learningUpdates).length} learning(s) to invoice ${newInvoice.id} (${newInvoice.supplierName})`);
@@ -990,6 +995,7 @@ export const appRouter = router({
sftpRemotePath: z.string().optional(), sftpRemotePath: z.string().optional(),
sftpAutoExport: z.number().optional(), sftpAutoExport: z.number().optional(),
llmLogsRetentionMonths: z.number().optional(), llmLogsRetentionMonths: z.number().optional(),
learningConfidenceThreshold: z.number().min(1).optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await upsertUserSettings({ await upsertUserSettings({