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 (
-
+