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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@@ -34,14 +34,31 @@ const FIELD_COLORS: Record<string, string> = {
isSubscription: "bg-orange-100 text-orange-800",
};
const DEFAULT_CONFIDENCE_THRESHOLD = 2;
const DEFAULT_THRESHOLD = 2;
export default function LearningSettings() {
const utils = trpc.useUtils();
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 [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({
onSuccess: () => {
@@ -97,6 +114,7 @@ export default function LearningSettings() {
}
setConfidenceThreshold(val);
setEditingThreshold(false);
upsertSettingsMutation.mutate({ learningConfidenceThreshold: val });
toast.success(`Seuil de confiance mis à jour : ${val} application(s)`);
};
@@ -184,10 +202,15 @@ export default function LearningSettings() {
<CardDescription>
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>.
Ce réglage est sauvegardé et partagé entre toutes vos sessions.
</CardDescription>
</CardHeader>
<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">
<Label htmlFor="threshold" className="text-sm whitespace-nowrap">
Nombre minimum d'applications :
@@ -201,7 +224,9 @@ export default function LearningSettings() {
onChange={(e) => setThresholdInput(e.target.value)}
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)); }}>
Annuler
</Button>
@@ -229,8 +254,8 @@ export default function LearningSettings() {
</CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-1">
<p>
Quand vous modifiez manuellement le <strong>type d'achat</strong>, le <strong>service concerné</strong> ou
la <strong>ventilation comptable</strong> d'une facture (dans le détail ou dans la liste BAP), le système mémorise cette
Quand vous modifiez manuellement le <strong>type d'achat</strong>, le <strong>service concerné</strong>,
la <strong>ventilation comptable</strong> ou le champ <strong>Abonnement</strong> d'une facture, le système mémorise cette
correction pour ce fournisseur.
</p>
<p>
@@ -256,7 +281,7 @@ export default function LearningSettings() {
<Brain className="w-12 h-12 text-gray-300 mb-4" />
<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">
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.
</p>
</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,
"tag": "0018_warm_changeling",
"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
// LLM Logs retention
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(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});

View File

@@ -299,11 +299,16 @@ export const appRouter = router({
const learnings = await getLearningsBySupplier(userId, newInvoice.supplierName);
if (learnings.length > 0) {
const learningUpdates: Record<string, string> = {};
const isSubscriptionLearning = learnings.find(l => l.fieldName === 'isSubscription');
for (const learning of learnings) {
if (learning.fieldName === 'typeAchat' || learning.fieldName === 'serviceConcerne' || learning.fieldName === 'ventilationComptable') {
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) {
await updateInvoice(newInvoice.id, learningUpdates as any);
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(),
sftpAutoExport: z.number().optional(),
llmLogsRetentionMonths: z.number().optional(),
learningConfidenceThreshold: z.number().min(1).optional(),
}))
.mutation(async ({ input, ctx }) => {
await upsertUserSettings({