TIF_E41253329/fe-nlp/src/components/landing-page/playground/playground-section.tsx

289 lines
11 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
import { Loader2, Upload, FileDown, Download } from 'lucide-react';
import { sentimentApi } from '@/services/api';
import { readTextsFromFile, downloadResultsAsExcel, downloadTemplateExcel } from '@/utils/excelUtils';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
export function PlaygroundSection() {
const [textInput, setTextInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [results, setResults] = useState<any[]>([]);
// Mapping Sentimen dinamis dari backend
const [sentimentMap, setSentimentMap] = useState<Record<string, string>>({});
const fileInputRef = useRef<HTMLInputElement>(null);
const fetchInfo = async () => {
try {
const info = await sentimentApi.getInfo();
const labelSize = info.data?.label_size || {};
// Ekstrak keys dan konversi ke integer
const keys = Object.keys(labelSize).map(Number).sort((a, b) => a - b);
if (keys.length > 0) {
const minKey = keys[0];
const maxKey = keys[keys.length - 1];
const newMap: Record<string, string> = {};
keys.forEach(k => {
if (k === minKey) newMap[k] = 'Negatif';
else if (k === maxKey) newMap[k] = 'Positif';
else newMap[k] = 'Netral';
});
setSentimentMap(newMap);
return newMap;
}
} catch (error) {
console.error('Gagal mengambil info model:', error);
}
return null;
};
useEffect(() => {
fetchInfo();
}, []);
const getSentimentLabel = (value: number, currentMap: Record<string, string>) => {
if (value === undefined || value === null) return 'Tidak Diketahui';
// Gunakan mapping dari backend jika ada
if (currentMap && currentMap[String(value)]) {
return currentMap[String(value)];
}
// Fallback default jika mapping backend belum/gagal dimuat
if (value === 0) return 'Negatif';
if (value === 1 || value === 2) return 'Positif';
return 'Tidak Diketahui';
};
const handlePredictSingle = async () => {
if (!textInput.trim()) return;
setIsLoading(true);
try {
let activeMap = sentimentMap;
if (Object.keys(activeMap).length === 0) {
const loadedMap = await fetchInfo();
if (loadedMap) activeMap = loadedMap;
}
const response = await sentimentApi.predict([textInput]);
if (response && response.length > 0) {
setResults([
{
Teks: textInput,
Sentimen: getSentimentLabel(response[0].sentiment, activeMap)
}
]);
}
} catch (error: any) {
console.error('Gagal memprediksi:', error);
if (error.response?.status === 500 || error.response?.status === 503) {
alert('Server Machine Learning sedang bermasalah (Error 500/503). Silakan coba beberapa saat lagi.');
} else {
alert('Terjadi kesalahan saat memproses permintaan.');
}
} finally {
setIsLoading(false);
}
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsLoading(true);
setResults([]);
setTextInput('');
try {
const texts = await readTextsFromFile(file);
if (texts.length === 0) {
alert('File kosong atau tidak valid.');
setIsLoading(false);
return;
}
let activeMap = sentimentMap;
if (Object.keys(activeMap).length === 0) {
const loadedMap = await fetchInfo();
if (loadedMap) activeMap = loadedMap;
}
const response = await sentimentApi.predict(texts);
const combinedResults = texts.map((text, idx) => ({
Teks: text,
Sentimen: getSentimentLabel(response[idx]?.sentiment, activeMap)
}));
setResults(combinedResults);
} catch (error: any) {
console.error('Gagal memproses file:', error);
if (error.response?.status === 500 || error.response?.status === 503) {
alert('Server Machine Learning sedang bermasalah (Error 500/503). Silakan coba beberapa saat lagi.');
} else {
alert('Terjadi kesalahan saat memproses file.');
}
} finally {
setIsLoading(false);
// Reset input file
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
const applyRecommendation = (text: string) => {
setTextInput(text);
};
return (
<section className="w-full py-12 md:py-24 flex flex-col items-center">
<div className="container px-4 md:px-6 max-w-4xl space-y-8">
{/* Header Section */}
<div className="text-center space-y-4">
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">Uji Kalimatmu di Sini</h1>
<p className="mx-auto max-w-[700px] text-muted-foreground md:text-lg">
Ketik ulasan, keluhan, atau pujian tentang aplikasi belajar Skolla di dalam kotak ini.
Model Machine Learning (Naive Bayes) yang kami latih akan langsung menebak apakah kalimatmu bersentimen Positif atau Negatif.
</p>
</div>
{/* Input Area */}
<Card>
<CardHeader>
<CardTitle>Coba Prediksi Sentimen</CardTitle>
<CardDescription>
Masukkan teks secara manual atau import dari file Excel/TXT.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-col gap-2">
<Textarea
placeholder="Ketikkan ulasan Anda di sini..."
className="min-h-[120px] resize-y"
value={textInput}
onChange={(e) => setTextInput(e.target.value)}
disabled={isLoading}
/>
</div>
<div className="flex flex-wrap gap-2 pt-2">
<span className="text-sm font-medium text-muted-foreground self-center mr-2">Rekomendasi ulasan:</span>
<Button variant="outline" size="sm" onClick={() => applyRecommendation("Aplikasi skolla sangat membantu belajar UTBK!")} disabled={isLoading}>
Pujian
</Button>
<Button variant="outline" size="sm" onClick={() => applyRecommendation("Sering lag kalau buka video pembelajaran, tolong diperbaiki.")} disabled={isLoading}>
Keluhan
</Button>
</div>
</CardContent>
<CardFooter className="flex flex-col sm:flex-row justify-between gap-4 border-t bg-muted/20 p-6">
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
<input
type="file"
accept=".xlsx, .xls, .txt"
className="hidden"
ref={fileInputRef}
onChange={handleFileUpload}
/>
<Button variant="secondary" onClick={() => fileInputRef.current?.click()} disabled={isLoading} className="w-full sm:w-auto">
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Upload className="mr-2 h-4 w-4" />}
Import Excel / TXT
</Button>
<Button variant="ghost" size="sm" onClick={downloadTemplateExcel} disabled={isLoading}>
<FileDown className="mr-2 h-4 w-4" />
Template
</Button>
</div>
<Button onClick={handlePredictSingle} disabled={isLoading || (!textInput.trim() && results.length === 0)} className="w-full sm:w-auto">
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Memproses Teks...
</>
) : (
'Mulai Analisis'
)}
</Button>
</CardFooter>
</Card>
{/* Result Area */}
{results.length > 0 && !isLoading && (
<Card className="border-primary/20 bg-primary/5">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<div className="space-y-1">
<CardTitle>Hasil Prediksi</CardTitle>
<CardDescription>
Ditemukan {results.length} hasil analisis sentimen.
</CardDescription>
</div>
<Button variant="outline" size="sm" onClick={() => downloadResultsAsExcel(results)}>
<Download className="mr-2 h-4 w-4" />
Unduh Hasil
</Button>
</CardHeader>
<CardContent>
{results.length === 1 ? (
<div className="flex flex-col items-center justify-center p-6 bg-background rounded-lg border shadow-sm">
<span className="text-muted-foreground mb-2">Sentimen:</span>
<span className={`text-3xl font-bold ${
results[0].Sentimen === 'Positif' ? 'text-green-600' :
results[0].Sentimen === 'Negatif' ? 'text-red-600' : 'text-yellow-600'
}`}>
{results[0].Sentimen}
</span>
</div>
) : (
<div className="rounded-md border bg-background max-h-[400px] overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-2/3">Teks Ulasan</TableHead>
<TableHead>Sentimen</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{results.map((row, idx) => (
<TableRow key={idx}>
<TableCell className="font-medium">{row.Teks}</TableCell>
<TableCell>
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${
row.Sentimen === 'Positif' ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' :
row.Sentimen === 'Negatif' ? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400'
}`}>
{row.Sentimen}
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
)}
</div>
</section>
);
}