mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-14 18:58:17 +02:00
feat: implement AI Copilot for product analysis, featuring dedicated API, business logic, UI components, and comprehensive documentation.
This commit is contained in:
1 parent
7a6b515db7
commit
1c03403e9a
8 files changed
+229
-11
No files matched your search
@@ -38,9 +38,15 @@ CONTRIBUTION(%) :
|
||||
GARDES-FOUS : ${p.scoring.isTop30Supplier ? "Oui (Top 30% Fournisseur)" : "Non"} | Récent : ${p.scoring.isRecent ? "Oui" : "Non"} | Dernier Prod: ${p.scoring.isLastProduct ? "Oui" : "Non"}
|
||||
` : "";
|
||||
|
||||
const contextRules = p.supplierContext ? `
|
||||
--- RÈGLES MÉTIER SPÉCIFIQUES ---
|
||||
Le manager a défini ces règles absolues pour ce fournisseur (TU DOIS LES RESPECTER EN PRIORITÉ) :
|
||||
"${p.supplierContext}"
|
||||
` : "";
|
||||
|
||||
return `PRODUIT: ${p.libelle1} (${p.codein})
|
||||
PERFORMANCE: Score Global ${p.score.toFixed(1)}/100 | Marge ${p.tauxMarge.toFixed(1)}% | PMV ${pmv.toFixed(2)}€
|
||||
${weights}${scoringInfo}
|
||||
${weights}${scoringInfo}${contextRules}
|
||||
Verdict algorithmique: ${p.scoring?.decision || "Non calculé"}
|
||||
Justifie ce verdict auprès de l'utilisateur.`;
|
||||
}
|
||||
|
||||
@@ -31,16 +31,18 @@ export interface ProductAnalysisInput {
|
||||
codeFournisseur?: string;
|
||||
totalMagasins?: number;
|
||||
isLastProductOfSupplier?: boolean;
|
||||
/** Résultats du ScoringEngine (optionnel pour l'IA) */
|
||||
/** Scoring metadata */
|
||||
scoring?: {
|
||||
compositeScore: number;
|
||||
decision: string;
|
||||
decision: "A" | "Z";
|
||||
labelProfil: string;
|
||||
isTop30Supplier: boolean;
|
||||
isRecent: boolean;
|
||||
isLastProduct: boolean;
|
||||
threshold: number;
|
||||
};
|
||||
/** Optional context rules for the supplier */
|
||||
supplierContext?: string;
|
||||
}
|
||||
|
||||
export interface AnalysisResult {
|
||||
|
||||
@@ -7,8 +7,13 @@ import { ScoringEngine } from "@/features/ai-copilot/business/scoring-engine";
|
||||
import { Sparkles, Loader2, CheckCircle2, XCircle } from "lucide-react";
|
||||
import { ProductRow, GammeCode } from "@/types/grid";
|
||||
import { ProductAnalysisInput } from "@/features/ai-copilot/models/ai-analysis.types";
|
||||
import { SupplierAiContextModal } from "./supplier-ai-context-modal";
|
||||
|
||||
export function BulkAiAnalyzer() {
|
||||
const rows = useGridStore((s) => s.rows);
|
||||
const supplierCode = rows.length > 0 ? rows[0].codeFournisseur : null;
|
||||
const supplierName = rows.length > 0 ? rows[0].nomFournisseur : null;
|
||||
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [progress, setProgress] = useState({ current: 0, total: 0, message: "", errors: 0 });
|
||||
const isCancelledRef = useRef(false);
|
||||
@@ -243,12 +248,15 @@ export function BulkAiAnalyzer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleAnalyze}
|
||||
className="group flex items-center justify-center gap-2 h-10 px-6 rounded-xl text-sm font-black transition-all shadow-md bg-[var(--accent)] text-white hover:brightness-110 active:scale-95 border border-white/10"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Analyse IA
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<SupplierAiContextModal codeFournisseur={supplierCode} nomFournisseur={supplierName} />
|
||||
<button
|
||||
onClick={handleAnalyze}
|
||||
className="group flex items-center justify-center gap-2 h-10 px-6 rounded-xl text-sm font-black transition-all shadow-md bg-[var(--accent)] text-white hover:brightness-110 active:scale-95 border border-white/10"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Analyse IA
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Brain } from "lucide-react";
|
||||
|
||||
interface SupplierAiContextModalProps {
|
||||
codeFournisseur: string | null;
|
||||
nomFournisseur: string | null;
|
||||
}
|
||||
|
||||
export function SupplierAiContextModal({ codeFournisseur, nomFournisseur }: SupplierAiContextModalProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [context, setContext] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && codeFournisseur) {
|
||||
fetchContext();
|
||||
}
|
||||
}, [open, codeFournisseur]);
|
||||
|
||||
const fetchContext = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/ai/context?fournisseur=${codeFournisseur}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setContext(data.context || "");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load AI context", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!codeFournisseur) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const res = await fetch("/api/ai/context", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ codeFournisseur, context }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setOpen(false);
|
||||
} else {
|
||||
console.error("Failed to save AI context");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save AI context", error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!codeFournisseur) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Brain className="w-4 h-4" />
|
||||
Règles IA
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Contexte IA - {nomFournisseur || codeFournisseur}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Définissez ici les règles métier absolues pour ce fournisseur.
|
||||
Mary (l'IA) respectera ces consignes en priorité lors de ses recommandations (ex: "Les calendriers vont en gamme C").
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
<Textarea
|
||||
placeholder="Ex: Les produits dont le nom contient 'Agenda' doivent toujours être classés en [A]..."
|
||||
value={context}
|
||||
onChange={(e) => setContext(e.target.value)}
|
||||
disabled={isLoading}
|
||||
className="min-h-[150px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)} disabled={isSaving}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSaving || isLoading} className="gap-2 bg-gradient-to-r from-violet-600 to-indigo-600 text-white hover:from-violet-700 hover:to-indigo-700">
|
||||
{isSaving ? "Enregistrement..." : "Enregistrer les règles"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user