feat: add BulkAiAnalyzer component for AI-driven product scoring and gamme suggestions.

This commit is contained in:
Michael committed 2026-02-27 10:49:58 +01:00
1 parent b0bb8cc09a
commit a053556a64
1 file changed
+50 -32
@@ -131,47 +131,65 @@ export function BulkAiAnalyzer() {
}); });
// 2. Calculer le scoring algorithmique pour chaque produit // 2. Calculer scoring + context profiling en micro-batches asynchrones
const scoringResults = new Map( // pour ne pas bloquer le thread UI (Chrome "Page ne répondant pas").
initialPayloads.map(p => [p.codein, ScoringEngine.analyzeRayon(p, initialPayloads)]) setIsAnalyzing(true);
); setProgress({ current: 0, total: initialPayloads.length, message: "Calcul du scoring...", errors: 0 });
// 3. Générer la fiche de contexte enrichie (ContextProfiler v3) const SCORING_BATCH_SIZE = 25;
// On transmet TOUS les produits du lot pour que les percentiles et poids const scoringResults = new Map<string, ReturnType<typeof ScoringEngine.analyzeRayon>>();
// soient calculés sur la distribution réelle (fournisseur complet). const productPayloads: (ProductAnalysisInput & { scoring: any })[] = [];
const productPayloads = initialPayloads.map(p => {
const scoringRes = scoringResults.get(p.codein)!;
// Fiche contextuelle (poids CA/QTÉ fournisseur + rayon, percentiles, signaux) for (let i = 0; i < initialPayloads.length; i += SCORING_BATCH_SIZE) {
let contextProfile: ProductAnalysisInput["contextProfile"]; if (isCancelledRef.current) break;
try {
contextProfile = ContextProfiler.buildProfile(p, initialPayloads, scoringRes); const batch = initialPayloads.slice(i, i + SCORING_BATCH_SIZE);
} catch (err) {
console.warn(`[BulkAnalyzer] ContextProfiler failed for ${p.codein}:`, err); for (const p of batch) {
contextProfile = undefined; const scoringRes = ScoringEngine.analyzeRayon(p, initialPayloads);
scoringResults.set(p.codein, scoringRes);
let contextProfile: ProductAnalysisInput["contextProfile"];
try {
contextProfile = ContextProfiler.buildProfile(p, initialPayloads, scoringRes);
} catch (err) {
console.warn(`[BulkAnalyzer] ContextProfiler failed for ${p.codein}:`, err);
contextProfile = undefined;
}
productPayloads.push({
...p,
contextProfile,
scoring: {
compositeScore: scoringRes.compositeScore,
decision: scoringRes.decision.recommendation,
labelProfil: scoringRes.decision.labelProfil,
isTop30Supplier: scoringRes.decision.isTop30Supplier,
isRecent: scoringRes.decision.isRecent,
isLastProduct: scoringRes.decision.isLastProduct,
threshold: scoringRes.decision.threshold,
},
});
} }
return { // Yield au navigateur entre chaque batch pour éviter le freeze
...p, setProgress(prev => ({
contextProfile, ...prev,
scoring: { current: Math.min(i + SCORING_BATCH_SIZE, initialPayloads.length),
compositeScore: scoringRes.compositeScore, message: `Scoring : ${Math.min(i + SCORING_BATCH_SIZE, initialPayloads.length)}/${initialPayloads.length}`,
decision: scoringRes.decision.recommendation, }));
labelProfil: scoringRes.decision.labelProfil, await new Promise(r => setTimeout(r, 0));
isTop30Supplier: scoringRes.decision.isTop30Supplier, }
isRecent: scoringRes.decision.isRecent,
isLastProduct: scoringRes.decision.isLastProduct,
threshold: scoringRes.decision.threshold,
},
};
});
setIsAnalyzing(true); if (isCancelledRef.current) {
setIsAnalyzing(false);
return;
}
let completed = 0; let completed = 0;
let errorsCount = 0; let errorsCount = 0;
const totalItems = productPayloads.length; const totalItems = productPayloads.length;
setProgress({ current: 0, total: totalItems, message: "Initialisation...", errors: 0 }); setProgress({ current: 0, total: totalItems, message: "Envoi aux modèles IA...", errors: 0 });
// Reset global ATOMIQUE et IMMÉDIAT // Reset global ATOMIQUE et IMMÉDIAT
const codeins = productPayloads.map(p => p.codein); const codeins = productPayloads.map(p => p.codein);