From 0496071600c6c6ac6ba36163b824b36318410550 Mon Sep 17 00:00:00 2001 From: Mahen Date: Tue, 26 May 2026 08:44:52 +0700 Subject: [PATCH] feat: add information detail recommendation result. --- src/components/dashboards/AspectScoreInfo.tsx | 554 ++++++++++++++++++ .../dashboards/RadarComparisonChart.tsx | 2 +- src/components/dashboards/ResultDetails.tsx | 64 +- 3 files changed, 609 insertions(+), 11 deletions(-) create mode 100644 src/components/dashboards/AspectScoreInfo.tsx diff --git a/src/components/dashboards/AspectScoreInfo.tsx b/src/components/dashboards/AspectScoreInfo.tsx new file mode 100644 index 0000000..dc891eb --- /dev/null +++ b/src/components/dashboards/AspectScoreInfo.tsx @@ -0,0 +1,554 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { createPortal } from "react-dom"; +import { + X, + Info, + Database, + Brain, + Eye, + AlertCircle, + ChevronDown, +} from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; + +interface AspectScoreInfoProps { + isDark: boolean; + aspectScores: Record; + totalReviews: number; + positiveCount: number; + negativeCount: number; +} + +const ASPECT_KEYWORD_SAMPLES: Record = { + performa: ["cepat", "kencang", "lancar", "lag", "lemot", "gaming", "render"], + layar: [ + "jernih", + "tajam", + "cerah", + "resolusi", + "oled", + "refresh rate", + "dead pixel", + ], + baterai: [ + "awet", + "tahan lama", + "boros", + "cepat habis", + "cas", + "charging", + "mah", + ], + harga: [ + "murah", + "mahal", + "worth it", + "terjangkau", + "promo", + "diskon", + "budget", + ], +}; + +const ASPECT_ICONS: Record = { + performa: "โšก", + layar: "๐Ÿ–ฅ๏ธ", + baterai: "๐Ÿ”‹", + harga: "๐Ÿ’ฐ", +}; + +function ScoreBar({ score, isDark }: { score: number; isDark: boolean }) { + const barColor = + score >= 80 ? "bg-green-500" : score >= 60 ? "bg-yellow-500" : "bg-red-500"; + const textColor = + score >= 80 + ? "text-green-500" + : score >= 60 + ? "text-yellow-500" + : "text-red-500"; + + return ( +
+
+
+
+ + {score.toFixed(0)}% + +
+ ); +} + +function StatusBadge({ score, isDark }: { score: number; isDark: boolean }) { + if (score >= 80) + return ( + + โœ“ Unggul + + ); + if (score >= 60) + return ( + + โš  Perhatian + + ); + return ( + + โœ— Lemah + + ); +} + +function Divider({ isDark }: { isDark: boolean }) { + return ( +
+ ); +} + +function SectionLabel({ + children, + isDark, +}: { + children: React.ReactNode; + isDark: boolean; +}) { + return ( +

+ {children} +

+ ); +} + +export default function AspectScoreInfo({ + isDark, + aspectScores, + totalReviews, + positiveCount, + negativeCount, +}: AspectScoreInfoProps) { + const [isOpen, setIsOpen] = useState(false); + const [activeStep, setActiveStep] = useState(null); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const close = useCallback(() => { + setIsOpen(false); + setActiveStep(null); + }, []); + + useEffect(() => { + if (!isOpen) return; + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") close(); + }; + window.addEventListener("keydown", handleKey); + return () => window.removeEventListener("keydown", handleKey); + }, [isOpen, close]); + + const generalScore = + totalReviews > 0 ? ((positiveCount / totalReviews) * 100).toFixed(1) : "0"; + + const steps = [ + { + title: "Ulasan dipindai kata kunci per aspek", + description: + "Sistem mencari kata-kata tertentu di setiap ulasan, seperti 'awet' untuk baterai atau 'lag' untuk performa. Ulasan yang mengandung kata tersebut akan dianalisis lebih lanjut oleh sistem.", + }, + { + title: "Model XGBoost mengklasifikasikan sentimen", + description: + "Ulasan yang menyebut aspek tersebut kemudian diklasifikasi oleh model prediksi. Apakah bernada positif, negatif, atau netral?", + }, + { + title: "Skor dihitung dari rasio positif", + description: + "Semakin banyak pembeli yang puas terhadap suatu aspek, semakin tinggi skornya. Misalnya jika 8 dari 10 pembeli senang dengan baterai produk ini, maka skor baterainya 80%.", + }, + ]; + + const trustReasons = [ + { + icon: Database, + title: "Berbasis ulasan nyata pembeli", + desc: "Data diambil langsung dari halaman ulasan Tokopedia via scraping, bukan dari spesifikasi produk atau klaim penjual.", + }, + { + icon: Brain, + title: "Model XGBoost Optimized (akurasi 73%)", + desc: "Dilatih dengan pipeline SMOTE + seleksi fitur Chi-Square + Grid Search, divalidasi pada data terpisah.", + }, + { + icon: Eye, + title: "Cara kerja sistem dapat ditelusuri", + desc: "Setiap aspek punya daftar kata kunci tetap yang bisa diaudit. Sistem hanya menghitung proporsi dari apa yang pembeli tulis.", + }, + ]; + + const modalContent = ( + + {isOpen && ( + +