diff --git a/src/app/(dashboard)/docs/ai-analysis/page.tsx b/src/app/(dashboard)/docs/ai-analysis/page.tsx index 95c6845..b9bef47 100644 --- a/src/app/(dashboard)/docs/ai-analysis/page.tsx +++ b/src/app/(dashboard)/docs/ai-analysis/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { Bot, TrendingUp, Calendar, Zap, AlertTriangle, ShieldCheck, Scale, Info } from "lucide-react"; +import { Bot, TrendingUp, Calendar, Zap, AlertTriangle, ShieldCheck, Scale, Info, Brain } from "lucide-react"; import { cn } from "@/lib/utils"; export default function AiAnalysisDocPage() { @@ -83,6 +83,20 @@ export default function AiAnalysisDocPage() { À l'inverse, un produit à faible volume peut être un Contributeur de Marge précieux s'il possède un Prix Moyen de Vente (PMV) élevé.

+ + {/* 5. Règles IA Fournisseur */} + +

+ Vous pouvez définir des Règles IA spécifiques pour chaque fournisseur (via le bouton avec le cerveau 🧠). + Ces directives sont mémorisées d'une session à l'autre et l'IA a pour consigne stricte de les respecter en priorité absolue + sur les calculs algorithmiques standards. +

+
{/* Footer / Tip */} diff --git a/src/app/api/ai/analyze/route.ts b/src/app/api/ai/analyze/route.ts index c97ad9d..d2f1507 100644 --- a/src/app/api/ai/analyze/route.ts +++ b/src/app/api/ai/analyze/route.ts @@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from "next/server"; import { getSavedDatabaseConfig } from "@/features/settings/actions"; import { OpenRouterClient } from "@/features/ai-copilot/data/open-router-client"; import { ProductAnalysisInput } from "@/features/ai-copilot/models/ai-analysis.types"; +import { db } from "@/db"; +import { aiSupplierContext } from "@/db/schema"; +import { eq } from "drizzle-orm"; export async function POST(req: NextRequest) { const config = await getSavedDatabaseConfig(); @@ -16,6 +19,21 @@ export async function POST(req: NextRequest) { try { const body: ProductAnalysisInput = await req.json(); + // Inject supplier context if available + if (body.codeFournisseur) { + try { + const contextRecord = await db.select() + .from(aiSupplierContext) + .where(eq(aiSupplierContext.codeFournisseur, body.codeFournisseur)) + .limit(1); + if (contextRecord.length > 0 && contextRecord[0].context) { + body.supplierContext = contextRecord[0].context; + } + } catch (err) { + console.error("[AI] Failed to fetch supplier context:", err); + } + } + const client = new OpenRouterClient({ apiKey, model }); const result = await client.analyzeProduct(body); diff --git a/src/app/api/ai/context/route.ts b/src/app/api/ai/context/route.ts new file mode 100644 index 0000000..f637122 --- /dev/null +++ b/src/app/api/ai/context/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from "next/server"; +import { db } from "@/db"; +import { aiSupplierContext } from "@/db/schema"; +import { eq } from "drizzle-orm"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const codeFournisseur = searchParams.get("fournisseur"); + + if (!codeFournisseur) { + return NextResponse.json({ error: "Fournisseur manquant" }, { status: 400 }); + } + + try { + const result = await db.select() + .from(aiSupplierContext) + .where(eq(aiSupplierContext.codeFournisseur, codeFournisseur)) + .limit(1); + + return NextResponse.json({ context: result[0]?.context || "" }); + } catch (error) { + console.error("Error fetching AI context:", error); + // Fallback en cas d'erreur de BDD ou si la table n'est pas encore créée + return NextResponse.json({ context: "" }); + } +} + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { codeFournisseur, context } = body; + + if (!codeFournisseur) { + return NextResponse.json({ error: "Fournisseur manquant" }, { status: 400 }); + } + + await db.insert(aiSupplierContext) + .values({ + codeFournisseur, + context: context || "", + updatedAt: new Date() + }) + .onConflictDoUpdate({ + target: aiSupplierContext.codeFournisseur, + set: { + context: context || "", + updatedAt: new Date() + } + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error saving AI context:", error); + return NextResponse.json({ error: "Erreur lors de la sauvegarde" }, { status: 500 }); + } +} diff --git a/src/db/schema.ts b/src/db/schema.ts index 35ccf2e..3a98813 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -76,3 +76,13 @@ export const sessionSnapshots = pgTable("session_snapshots", { type: varchar("type", { length: 20 }).default("snapshot"), createdAt: timestamp("created_at").defaultNow(), }); + +/** AI Context rules per supplier (Epic: AI Context) */ +export const aiSupplierContext = pgTable("ai_supplier_context", { + /** Supplier code serving as the primary key */ + codeFournisseur: varchar("code_fournisseur", { length: 20 }).primaryKey(), + /** The business rules text provided by the user */ + context: text("context").notNull(), + /** When it was last updated */ + updatedAt: timestamp("updated_at").defaultNow(), +}); diff --git a/src/features/ai-copilot/business/analysis-engine.ts b/src/features/ai-copilot/business/analysis-engine.ts index 7544992..154edc3 100644 --- a/src/features/ai-copilot/business/analysis-engine.ts +++ b/src/features/ai-copilot/business/analysis-engine.ts @@ -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.`; } diff --git a/src/features/ai-copilot/models/ai-analysis.types.ts b/src/features/ai-copilot/models/ai-analysis.types.ts index 9e87ee3..12e1e21 100644 --- a/src/features/ai-copilot/models/ai-analysis.types.ts +++ b/src/features/ai-copilot/models/ai-analysis.types.ts @@ -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 { diff --git a/src/features/grid/components/bulk-ai-analyzer.tsx b/src/features/grid/components/bulk-ai-analyzer.tsx index 6a28ec2..1cbea61 100644 --- a/src/features/grid/components/bulk-ai-analyzer.tsx +++ b/src/features/grid/components/bulk-ai-analyzer.tsx @@ -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 ( - +
+ + +
); } diff --git a/src/features/grid/components/supplier-ai-context-modal.tsx b/src/features/grid/components/supplier-ai-context-modal.tsx new file mode 100644 index 0000000..4f1ab10 --- /dev/null +++ b/src/features/grid/components/supplier-ai-context-modal.tsx @@ -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 ( + + + + + + + Contexte IA - {nomFournisseur || codeFournisseur} + + 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"). + + + +
+