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:
@@ -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 été appliqué au moins{" "}
|
Un apprentissage est marqué <strong>Confirmé</strong> quand il a été 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>
|
||||||
|
|||||||
1
drizzle/0019_smooth_lord_hawal.sql
Normal file
1
drizzle/0019_smooth_lord_hawal.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `userSettings` ADD `learningConfidenceThreshold` int DEFAULT 2 NOT NULL;
|
||||||
1702
drizzle/meta/0019_snapshot.json
Normal file
1702
drizzle/meta/0019_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user