From b2922fba99737df53998cc236b94ef69a1ff49fb Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Mon, 23 Feb 2026 15:56:58 +0100 Subject: [PATCH] feat: implement AI explanation modal and insight block components for product analysis. --- .../components/ai-explanation-modal.tsx | 156 ++++++++++++++++++ .../components/ai-insight-block.tsx | 66 ++++++-- task.md | 6 + 3 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 src/features/ai-copilot/components/ai-explanation-modal.tsx diff --git a/src/features/ai-copilot/components/ai-explanation-modal.tsx b/src/features/ai-copilot/components/ai-explanation-modal.tsx new file mode 100644 index 0000000..303659d --- /dev/null +++ b/src/features/ai-copilot/components/ai-explanation-modal.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { X, Bot, Sparkles, TrendingUp, ShieldAlert, History } from "lucide-react"; + +interface AiExplanationModalProps { + isOpen: boolean; + onClose: () => void; + productName: string; + productCode: string; + explanation: string; + recommandation?: "A" | "C" | "Z" | null; +} + +export function AiExplanationModal({ + isOpen, + onClose, + productName, + productCode, + explanation, + recommandation +}: AiExplanationModalProps) { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (isOpen) { + setIsVisible(true); + } else { + setIsVisible(false); + } + }, [isOpen]); + + const handleClose = () => { + setIsVisible(false); + setTimeout(onClose, 250); + }; + + if (!isOpen && !isVisible) return null; + + const getGammeColor = (g?: string | null) => { + switch (g) { + case "A": return "text-emerald-500 border-emerald-500/50 bg-emerald-500/10"; + case "C": return "text-amber-500 border-amber-500/50 bg-amber-500/10"; + case "Z": return "text-rose-500 border-rose-500/50 bg-rose-500/10"; + default: return "text-slate-500 border-slate-500/50 bg-slate-500/10"; + } + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Panel */} +
+ {/* Header with Mary Icon */} +
+
+
+ +
+
+

+ Analyse de Mary đź§™ +

+

+ {productName} ({productCode}) +

+
+
+ +
+ + {/* Content */} +
+
+ {/* Recommandation Badge */} +
+ Verdict : +
+ {recommandation === "A" && } + {recommandation === "C" && } + {recommandation === "Z" && } + {recommandation ? `GAMME ${recommandation}` : "AUCUN"} +
+
+ + {/* Explanation Text */} +
+
+ +
+ "{explanation}" +
+
+
+ +
+
+

+ Analyse générée en temps réel selon les indicateurs CA, Marge et Régularité. +

+
+
+
+ + {/* Footers / Action */} +
+ +
+
+ + +
+ ); +} diff --git a/src/features/ai-copilot/components/ai-insight-block.tsx b/src/features/ai-copilot/components/ai-insight-block.tsx index 08a1f7f..6bb6340 100644 --- a/src/features/ai-copilot/components/ai-insight-block.tsx +++ b/src/features/ai-copilot/components/ai-insight-block.tsx @@ -1,7 +1,10 @@ "use client"; -import { Bot, Loader2, AlertCircle, Sparkles } from "lucide-react"; +import React, { useState } from "react"; + +import { Bot, Loader2, AlertCircle, Sparkles, Maximize2 } from "lucide-react"; import { useAiCopilotStore } from "@/features/ai-copilot/store/use-ai-copilot-store"; +import { AiExplanationModal } from "./ai-explanation-modal"; import type { ProductRow } from "@/types/grid"; import { cn } from "@/lib/utils"; @@ -12,10 +15,11 @@ interface AiInsightBlockProps { } export function AiInsightBlock({ row }: AiInsightBlockProps) { - const insight = useAiCopilotStore((s) => s.insights[row.codein]); - const analyzeProduct = useAiCopilotStore((s) => s.analyzeProduct); - const setLoading = useAiCopilotStore((s) => s.setLoading); - const setDraftGamme = useGridStore((s) => s.setDraftGamme); + const insight = useAiCopilotStore((s: any) => s.insights[row.codein]); + const analyzeProduct = useAiCopilotStore((s: any) => s.analyzeProduct); + const setLoading = useAiCopilotStore((s: any) => s.setLoading); + const setDraftGamme = useGridStore((s: any) => s.setDraftGamme); + const [isModalOpen, setIsModalOpen] = useState(false); const status = insight?.status ?? "idle"; @@ -97,18 +101,46 @@ export function AiInsightBlock({ row }: AiInsightBlockProps) { } return ( -
-
- - - {insight?.insight} - + <> +
+
+ { + e.stopPropagation(); + handleAnalyze(); + }} + /> +
+
{ + e.stopPropagation(); + setIsModalOpen(true); + }} + > + + {insight?.insight} + + +
+
+
+ {insight?.isDuplicate && ( + + ⚠️ Doublon probable + + )}
- {insight?.isDuplicate && ( - - ⚠️ Doublon probable - - )} -
+ + setIsModalOpen(false)} + productName={row.libelle1} + productCode={row.codein} + explanation={insight?.insight || ""} + recommandation={insight?.recommandation} + /> + ); } diff --git a/task.md b/task.md index 1052884..8eefec7 100644 --- a/task.md +++ b/task.md @@ -18,6 +18,8 @@ Activation et initialisation de l'Agent Master BMAD. - [x] Étape 10 : Ajustement du seuil de protection (Score < 20) - [x] Étape 11 : Intégration de l'analyse critique systématique (même score > 50) - [x] Étape 12 : Validation finale et correction TS +- [x] Étape 13 : Implémentation du modal pour les explications IA détaillées +- [/] Étape 14 : Polissage de l'UI (Sally/UX) # Progress Log @@ -38,3 +40,7 @@ Activation et initialisation de l'Agent Master BMAD. - Arbitrage IA maintenu entre 20 et 50 basé sur CA / Marge / Quantité. - Michael exige une analyse critique systématique même pour les scores > 50. - Logique finale implémentée et validée (Score < 20 = Z). +- Demande de Michael : Ajouter une icône + modal pour voir l'explication complète. +- Recherche du composant de la grille en cours. +- Nouveau composant `AiExplanationModal` créé. +- Intégration dans `AiInsightBlock` terminée (icône + déclenchement).