diff --git a/components/dashboard/BrandFilter.tsx b/components/dashboard/BrandFilter.tsx new file mode 100644 index 0000000..f864fdb --- /dev/null +++ b/components/dashboard/BrandFilter.tsx @@ -0,0 +1,45 @@ +import { cn } from "@/lib/utils"; + +interface Brand { + name: string; + count: number; + logo?: string; +} + +interface BrandFilterProps { + brands: Brand[]; + selectedBrand: string | null; + onSelect: (brand: string | null) => void; +} + +export function BrandFilter({ brands, selectedBrand, onSelect }: BrandFilterProps) { + return ( +
+ + {brands.map((brand) => ( + + ))} +
+ ); +} diff --git a/components/dashboard/Header.tsx b/components/dashboard/Header.tsx new file mode 100644 index 0000000..e9d15ce --- /dev/null +++ b/components/dashboard/Header.tsx @@ -0,0 +1,101 @@ +import { + BarChart3, + Database, + Laptop, + LogOut, + RefreshCw, + User, + UserCircle, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useState } from "react"; +import { cn } from "@/lib/utils"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; + +export function Header() { + const [isRefreshing, setIsRefreshing] = useState(false); + const [open, setOpen] = useState(false); + + const handleRefresh = () => { + setIsRefreshing(true); + setTimeout(() => setIsRefreshing(false), 1500); + }; + + return ( +
+
+
+
+
+ +
+
+

+ SENTILAISES. +

+

+ Analisis Sentimen Ulasan Laptop Tokopedia +

+
+
+ +
+
+
+ + 5 Brand +
+
+ + 12,450 Ulasan +
+
+
setOpen(true)} + onMouseLeave={() => setOpen(false)} + > + + + + + + + + + Menu Profil + + + + + console.log("Logout clicked")} + > + + Logout + + + +
+
+
+
+
+ ); +} diff --git a/components/dashboard/ModelInfo.tsx b/components/dashboard/ModelInfo.tsx new file mode 100644 index 0000000..575126c --- /dev/null +++ b/components/dashboard/ModelInfo.tsx @@ -0,0 +1,97 @@ +import { useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { modelData } from "@/src/app/dashboard/lib/data"; + +export function ModelInfo() { + const [selectedModel, setSelectedModel] = + useState("optimized"); + + const currentModel = modelData[selectedModel]; + + return ( +
+
+ + + + Active + +
+ +

+ {currentModel.description} +

+ +
+ {currentModel.metrics.map((metric) => ( +
+
+ +
+
+

{metric.label}

+

{metric.value}

+
+
+ ))} +
+ +
+
+ Preprocessing + + Case Folding, Stopwords, Stemming + +
+
+ Feature Extraction + TF-IDF Vectorization +
+
+ Training Data + 3.445 ulasan +
+
+
+ ); +} diff --git a/components/dashboard/ReviewTable.tsx b/components/dashboard/ReviewTable.tsx new file mode 100644 index 0000000..6ee19a2 --- /dev/null +++ b/components/dashboard/ReviewTable.tsx @@ -0,0 +1,116 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import { Star } from "lucide-react"; + +interface Review { + id: string; + product: string; + brand: string; + review: string; + rating: number; + sentiment: "positif" | "negatif" | "netral"; + date: string; + confidence: number; +} + +interface ReviewTableProps { + reviews: Review[]; +} + +export function ReviewTable({ reviews }: ReviewTableProps) { + const getSentimentBadge = (sentiment: Review["sentiment"]) => { + const styles = { + positif: "sentiment-positive", + negatif: "sentiment-negative", + netral: "sentiment-neutral", + }; + + const labels = { + positif: "Positif", + negatif: "Negatif", + netral: "Netral", + }; + + return ( + + {labels[sentiment]} + + ); + }; + + const renderStars = (rating: number) => { + return ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ ); + }; + + return ( +
+ + + + Produk + Ulasan + Rating + Sentimen + Confidence + + + + {reviews.map((review, index) => ( + + +
+

{review.brand}

+

+ {review.product} +

+
+
+ +

{review.review}

+

+ {review.date} +

+
+ {renderStars(review.rating)} + {getSentimentBadge(review.sentiment)} + + + {(review.confidence * 100).toFixed(1)}% + + +
+ ))} +
+
+
+ ); +} diff --git a/components/dashboard/SentimentAnalyzer.tsx b/components/dashboard/SentimentAnalyzer.tsx new file mode 100644 index 0000000..e3fd7e6 --- /dev/null +++ b/components/dashboard/SentimentAnalyzer.tsx @@ -0,0 +1,241 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import { + Loader2, + Send, + Sparkles, + ThumbsUp, + ThumbsDown, + Minus, +} from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; + +interface AnalysisResult { + sentiment: "positif" | "negatif" | "netral"; + confidence: number; + keywords: string[]; +} + +export function SentimentAnalyzer() { + const [text, setText] = useState(""); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [result, setResult] = useState(null); + + // Simulated analysis - in real implementation, this would call an XGBoost model API + const analyzeText = async () => { + if (!text.trim()) return; + + setIsAnalyzing(true); + setResult(null); + + await new Promise((resolve) => setTimeout(resolve, 1500)); + + const positiveWords = [ + "bagus", + "cepat", + "aman", + "baik", + "mulus", + "moga", + "awet", + "mantap", + "sangat", + "fungsi", + ]; + + const negativeWords = [ + "lebih", + "jual", + "baru", + "lalu", + "tahun", + "masalah", + "rusak", + "garansi", + "layar", + "kecewa", + ]; + + const lowerText = text.toLowerCase(); + let positiveScore = 0; + let negativeScore = 0; + const foundKeywords: string[] = []; + + positiveWords.forEach((word) => { + if (lowerText.includes(word)) { + positiveScore++; + foundKeywords.push(word); + } + }); + + negativeWords.forEach((word) => { + if (lowerText.includes(word)) { + negativeScore++; + foundKeywords.push(word); + } + }); + + let sentiment: AnalysisResult["sentiment"]; + let confidence: number; + + if (positiveScore > negativeScore) { + sentiment = "positif"; + confidence = 0.75 + Math.random() * 0.2; + } else if (negativeScore > positiveScore) { + sentiment = "negatif"; + confidence = 0.75 + Math.random() * 0.2; + } else { + sentiment = "netral"; + confidence = 0.6 + Math.random() * 0.2; + } + + setResult({ + sentiment, + confidence, + keywords: foundKeywords, + }); + setIsAnalyzing(false); + }; + + const getSentimentDisplay = (sentiment: AnalysisResult["sentiment"]) => { + const config = { + positif: { + icon: ThumbsUp, + label: "Positif", + bgClass: "bg-sentiment-positive-light", + textClass: "text-sentiment-positive", + borderClass: "border-sentiment-positive/30", + }, + negatif: { + icon: ThumbsDown, + label: "Negatif", + bgClass: "bg-sentiment-negative-light", + textClass: "text-sentiment-negative", + borderClass: "border-sentiment-negative/30", + }, + netral: { + icon: Minus, + label: "Netral", + bgClass: "bg-sentiment-neutral-light", + textClass: "text-sentiment-neutral", + borderClass: "border-sentiment-neutral/30", + }, + }; + return config[sentiment]; + }; + + return ( +
+
+ +

Analisis Sentimen Real-time

+
+

+ Masukkan ulasan produk laptop untuk menganalisis sentimennya menggunakan + model XGBoost +

+ +
+