feat(ai-copilot): add AI analysis engine with prompt generation, recommendation extraction, and insight cleaning capabilities.

This commit is contained in:
Michael committed 2026-02-23 22:52:21 +01:00
1 parent b5b31a35af
commit fbad53df36
2 files changed
+76 -97

No files matched your search

+41 -54
View File
@@ -25,77 +25,64 @@ export default function AiAnalysisDocPage() {
{/* Grid of Rules */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 1. Équité Territoriale */}
{/* 1. Loi de Pareto */}
<DocCard
icon={ShieldCheck}
title="Normalisation Magasins (x2)"
description="Équilibre les chances entre les produits mono-magasin et multi-magasins."
title="Loi de Pareto (Poids %)"
description="Priorité n°1 : Le poids du produit chez le fournisseur."
accent="teal"
>
<p className="text-[13px] leading-relaxed">
Pour éviter qu'un produit présent dans un seul magasin ne soit pénalisé par son volume brut, le système
<strong> double automatiquement</strong> toutes ses statistiques (Ventes, CA, Marge).
Cela simule sa performance s'il était présent sur les deux points de vente de référence.
L'IA identifie les produits <strong>Indiscutables</strong>. Si un produit pèse
<strong> {"> 10%"}</strong> du CA total de son fournisseur, il est jugé stratégique.
Mary refusera de suggérer sa sortie [Z] pour protéger la relation fournisseur et
garantir la stabilité de l'assortiment.
</p>
</DocCard>
{/* 2. Run Rate */}
<DocCard
icon={TrendingUp}
title="Potentiel Annuel (Run Rate)"
description="Évalue les lancements sur leur dynamique réelle, pas sur leur cumul."
accent="indigo"
>
<p className="text-[13px] leading-relaxed">
Si un produit a moins de 12 mois de présence, l'IA calcule un <strong>Run Rate 12 mois</strong>.
<br /><br />
<code className="bg-[var(--bg-muted)] px-2 py-0.5 rounded text-indigo-600 dark:text-indigo-400 font-mono text-[11px]">
Projection = Volume_Réel * (12 / Mois_Présence)
</code>
<br /><br />
Cela permet de confirmer un produit <strong>Permanent [A]</strong> dès ses premiers mois s'il a une forte vélocité.
</p>
</DocCard>
{/* 3. Saisonnalité */}
<DocCard
icon={Calendar}
title="Détection Saisonnalité"
description="Distingue les ruptures temporaires des fins de vie ou saisons."
accent="amber"
>
<p className="text-[13px] leading-relaxed">
L'IA analyse la <strong>récence</strong> des ventes. Si aucune vente n'est constatée depuis plus de
<strong> 2 mois</strong> :
</p>
<ul className="mt-2 space-y-1 text-[13px]">
<li className="flex items-center gap-2">
<Zap className="w-3 h-3 text-emerald-500" />
<span>{"< 2 mois : Lancement probable (actif)"}</span>
</li>
<li className="flex items-center gap-2">
<AlertTriangle className="w-3 h-3 text-amber-500" />
<span>{"> 2 mois : Alerte Saison [C] ou Sortie [Z]"}</span>
</li>
</ul>
</DocCard>
{/* 4. Score Global */}
{/* 2. Équité & Normalisation */}
<DocCard
icon={Scale}
title="Score de Performance"
description="La source de vérité mathématique (0 à 100)."
title="Normalisation & Équité"
description="Égalité de traitement entre mono et multi-magasins."
accent="rose"
>
<p className="text-[13px] leading-relaxed">
Le score affiché dans la grille combine la Quantité, le CA et la Marge.
L'IA utilise ce score comme garde-fou :
Pour une comparaison juste, les statistiques des produits présents sur un seul magasin sont
<strong> doublées (x2)</strong>. L'IA compare ensuite ces données pondérées aux
<strong> Benchmarks du Rayon</strong> (Moyenne du Niveau 2) pour détecter les sur-performances.
</p>
</DocCard>
{/* 3. Cycle de Vie */}
<DocCard
icon={TrendingUp}
title="Potentiel & Cycle de Vie"
description="Détection des lancements et des fins de vie."
accent="indigo"
>
<p className="text-[13px] leading-relaxed">
Mary distingue les produits récents des produits établis :
</p>
<ul className="mt-2 space-y-1 text-[13px]">
<li>• <strong>{"> 70"}</strong> : Candidat naturel au maintien.</li>
<li>• <strong>{"< 30"}</strong> : Signal fort de déréférencement.</li>
<li>• <strong>Run Rate</strong> : Projection sur 12 mois pour les produits de moins d'un an.</li>
<li>• <strong>Inactivité</strong> : Une alerte est levée après <strong>2 mois sans vente</strong>,
orientant le verdict vers le [C] (Saisonnier) ou [Z] (Sortie).</li>
</ul>
</DocCard>
{/* 4. PMV & Trafic */}
<DocCard
icon={Zap}
title="Volumes vs Valeur (PMV)"
description="Identifier les générateurs de trafic."
accent="amber"
>
<p className="text-[13px] leading-relaxed">
Un produit peut être maintenu même avec un faible CA s'il génère du <strong>Trafic</strong> (Volumes {">"} Moyenne Rayon).
À l'inverse, un produit à faible volume peut être un <strong>Contributeur de Marge</strong> précieux s'il possède un Prix Moyen de Vente (PMV) élevé.
</p>
</DocCard>
</div>
{/* Footer / Tip */}
@@ -2,82 +2,74 @@ import { ProductAnalysisInput } from "../models/ai-analysis.types";
export class AnalysisEngine {
static generateSystemPrompt(): string {
return `Tu es Mary, une experte en stratégie retail. Ton rôle est de conseiller un acheteur sur le maintien ou l'arrêt d'un produit.
Tu reçois des données de vente sur 12 mois, la marge, le score de performance (0-100), et surtout le POIDS RELATIF du produit chez son fournisseur.
return `Tu es Mary, une experte Senior en Stratégie Retail et Category Management. Ton rôle est d'arbitrer l'assortiment avec une rigueur de consultant.
Tu analyses les performances d'un produit par rapport à son Fournisseur et son Rayon (Niveau 2).
--- CRITÈRES DE DÉCISION (Priorité Décroissante) ---
1. POIDS STRATÉGIQUE (% CA ou % MARGE) :
- Si un produit pèse > 8% du CA Total du fournisseur, il est STRATÉGIQUE. Ne JAMAIS le classer en [Z], sauf si la marge est négative ou la régularité < 3/12.
- Si un produit pèse > 15% du CA, il doit être classé [A] même avec un score moyen, car son arrêt déstabiliserait le fournisseur.
--- HIÉRARCHIE DE DÉCISION MÉTIER (Priorité Décroissante) ---
2. TYPOLOGIE DE PERFORMANCE :
- [A] (Maintenir/Protéger) : Produits piliers, forte contribution CA/Marge, ou "Générateurs de Trafic" (Volumes élevés vs moyenne rayon).
- [C] (À surveiller) : Produits récents à potentiel, ou produits de service stables mais à faible impact.
- [Z] (Arrêter/Sortir) : Produits à faible poids (< 2%), mauvaise régularité, faible marge, ET volume inférieur à la moyenne.
1. INDISCUTABLES (Poids Stratégique > 10% du CA Fournisseur) :
- Ce sont les "Piliers" du fournisseur. Ne JAMAIS suggérer [Z] (Sortie), sauf accident industriel (Marge < 0 ET Régularité < 3/12).
- Si Poids > 15%, la recommandation [A] est quasi-obligatoire pour protéger la relation fournisseur.
3. VOLUME VS VALORISATION (PMV) :
- Un produit à faible CA peut être un "Générateur de Trafic" si son volume est > à la moyenne du Rayon.
- Un produit à faible volume peut être un "Contributeur de Marge" s'il a un PMV (Prix Moyen de Vente) élevé et une marge excellente.
2. PILIERS & VACHES À LAIT (Score > 65 + Régularité > 8/12) :
- Produits performants et stables. Recommandation [A] (Permanent).
- Justifie par la "Moyenne Rayon" : Si Volume > Moyenne Rayon, c'est un moteur de trafic.
--- TON TON ET STYLE ---
Sois brève (max 2-3 phrases). Justifie toujours par le poids (%) ou le volume comparatif.
Sois directe : "Pèse 12% du CA", "Volume 2x supérieur au rayon", etc.
Commence toujours par ta recommandation entre crochets : [A], [C] ou [Z].
3. DILEMMES & SAISONNIERS (Score moyen OU Régularité faible OU Inactivité > 2) :
- [C] (À surveiller) : Produits récents avec fort "Run Rate" (Projection 12m) ou produits au comportement saisonnier cyclique.
- Un produit récent (Régularité < 6) mais avec une projection 12m élevée mérite d'être protégé [C] pour lui laisser sa chance.
Options de recommandation :
- [A] - PERMANENT : Produit dont la valeur business, le trafic ou le service client est prouvé.
- [C] - SAISONNIER : Pics de ventes concentrés, inactivité hors saison.
Ta réponse doit être courte, directe et sans complaisance.
Format : "[Recommandation] : [Justification factuelle]"`;
4. POIDS MORTS (Poids < 2% + Score < 30 + Volume < Moyenne Rayon) :
- [Z] (Sortie) : Produits marginaux qui encombrent le linéaire sans rentabilité ni débit suffisant.
--- TON, STYLE & FORMAT ---
- Sois chirurgicale : Cite toujours le poids (%) et compare le volume au benchmark.
- Maximum 2-3 phrases. Pas de blabla, juste des faits.
- Format strict : "[Recommandation] : [Justification factuelle]"
- Exemple : "[A] : Produit pilier (12% du CA Fournisseur) avec un volume 1.5x supérieur à la moyenne du rayon."`;
}
static generateUserMessage(p: ProductAnalysisInput): string {
const pmv = p.totalQuantite > 0 ? p.totalCa / p.totalQuantite : 0;
const monthlySummary = Object.entries(p.sales12m || {})
.map(([k, v]) => `${k}: ${Math.round(v)}u`)
.join(", ");
const volumeInfo = `Volumes : ${Math.round(p.totalQuantite)}u sur ${p.storeCount} mag.`;
const volumeInfo = `VOLUMES : ${Math.round(p.totalQuantite)}u sur ${p.storeCount} mag.`;
// Contexte comparatif et contribution
// Contexte comparatif
const avgRef = p.avgQtyRayon || p.avgQtyFournisseur || 0;
const refName = p.avgQtyRayon ? "rayon" : "fournisseur";
const relativeVolume = avgRef > 0 ? ` (Moyenne du ${refName} : ${avgRef.toFixed(1)}u)` : "";
// Poids Stratégique
const weights = p.shareCa !== undefined ? `
CONTRIBUTION (%) :
- Chiffre d'Affaires : ${p.shareCa.toFixed(1)}% du total fournisseur
- Marge Brute : ${p.shareMarge?.toFixed(1)}% du total fournisseur
- Volumes : ${p.shareQty?.toFixed(1)}% du total fournisseur
` : "";
- Volumes : ${p.shareQty?.toFixed(1)}% du total fournisseur` : "";
const totalContext = p.totalFournisseurCa ? `TOTAL FOURNISSEUR : ${p.totalFournisseurCa.toFixed(0)}€ CA` : "";
const totalContext = p.totalFournisseurCa ? `\nVALEUR TOTALE FOURNISSEUR : ${p.totalFournisseurCa.toFixed(0)}€ CA` : "";
const projectionInfo = p.regularityScore > 0 && p.regularityScore < 12 ? `
--- ANALYSE DE POTENTIEL (Produit récent : ${p.regularityScore}/12 mois active) ---
Projection 12 mois : ${Math.round(p.projectedTotalQuantite || 0)}u (${(p.projectedTotalCa || 0).toFixed(2)}€)` : "";
const projectionInfo = (p.regularityScore || 0) > 0 && (p.regularityScore || 0) < 12 ? `
--- ANALYSE DE POTENTIEL (Produit récent : ${p.regularityScore}/12 mois) ---
Projection 12 mois (Run Rate) : ${Math.round(p.projectedTotalQuantite || 0)}u (${(p.projectedTotalCa || 0).toFixed(2)}€)` : "";
const activityAlert = (p.inactivityMonths || 0) > 2
? `\n⚠️ ALERTE : Inactif depuis ${p.inactivityMonths} mois`
? `\n⚠️ ALERTE INACTIVITÉ : ${p.inactivityMonths} mois sans vente.`
: "";
return `Produit : ${p.libelle1} (${p.codein})
PERFORMANCE GLOBALE : ${p.score.toFixed(1)}/100
MARGE : ${p.tauxMarge.toFixed(1)}%
PRIX MOYEN (PMV) : ${pmv.toFixed(2)}€
${volumeInfo}${relativeVolume}
${weights}
${totalContext}
return `PRODUIT : ${p.libelle1} (${p.codein})
PERFORMANCE : Score ${p.score.toFixed(1)}/100 | Marge ${p.tauxMarge.toFixed(1)}% | PMV ${pmv.toFixed(2)}€
${volumeInfo}${relativeVolume}${weights}${totalContext}
Régularité : ${p.regularityScore}/12 mois active
${projectionInfo}${activityAlert}
CYCLE DE VIE : ${p.regularityScore}/12 mois active${projectionInfo}${activityAlert}
--- HISTORIQUE DES VENTES (12 mois) ---
--- HISTORIQUE MENSUEL PONDÉRÉ ---
${monthlySummary}
Recommandation actuelle : ${p.codeGamme || "Aucune"}
Verdict attendu : [A], [C] ou [Z] avec justification par les chiffres.`;
Verdict attendu (A, C ou Z) ?`;
}
static extractRecommendation(content: string): "A" | "C" | "Z" | null {