Spaces:
Sleeping
Sleeping
anoderb commited on
Commit ·
4411d4b
1
Parent(s): a14ca57
feat: add model evaluation comparison modal and evaluasi button to frontend
Browse files- frontend/app/page.tsx +208 -1
frontend/app/page.tsx
CHANGED
|
@@ -96,6 +96,11 @@ export default function Dashboard() {
|
|
| 96 |
const [forecastNewLabel, setForecastNewLabel] = useState('')
|
| 97 |
const [showForecastSuccessModal, setShowForecastSuccessModal] = useState(false)
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
// --- Settings Portal States ---
|
| 100 |
const [runtimeSettings, setRuntimeSettings] = useState<Record<string, any>>({})
|
| 101 |
const [runtimeSaving, setRuntimeSaving] = useState<string | null>(null)
|
|
@@ -422,6 +427,19 @@ export default function Dashboard() {
|
|
| 422 |
}
|
| 423 |
}
|
| 424 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
// ── SETTINGS PORTAL ACTIONS ────────────────────────────────────────────────
|
| 426 |
const handleSaveSetting = async (key: string) => {
|
| 427 |
setRuntimeSaving(key)
|
|
@@ -705,6 +723,183 @@ export default function Dashboard() {
|
|
| 705 |
</div>
|
| 706 |
)}
|
| 707 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
{/* MAIN CONTAINER CONTENT */}
|
| 709 |
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-8">
|
| 710 |
|
|
@@ -1177,7 +1372,19 @@ export default function Dashboard() {
|
|
| 1177 |
<span className="text-xs font-bold text-slate-900 block">{k.nama}</span>
|
| 1178 |
<span className="text-[10px] text-slate-400 block mt-0.5">Volatilitas: <strong className={k.volatile ? "text-amber-600" : "text-emerald-600"}>{k.volatile ? "Tinggi" : "Rendah"}</strong></span>
|
| 1179 |
</div>
|
| 1180 |
-
<div className="flex gap-1.5">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1181 |
{isRunning ? (
|
| 1182 |
<button
|
| 1183 |
onClick={handleStopForecast}
|
|
|
|
| 96 |
const [forecastNewLabel, setForecastNewLabel] = useState('')
|
| 97 |
const [showForecastSuccessModal, setShowForecastSuccessModal] = useState(false)
|
| 98 |
|
| 99 |
+
// --- Model Detail Modal States ---
|
| 100 |
+
const [showModelDetailModal, setShowModelDetailModal] = useState(false)
|
| 101 |
+
const [selectedCommodityDetail, setSelectedCommodityDetail] = useState<any>(null)
|
| 102 |
+
const [loadingDetailId, setLoadingDetailId] = useState<number | null>(null)
|
| 103 |
+
|
| 104 |
// --- Settings Portal States ---
|
| 105 |
const [runtimeSettings, setRuntimeSettings] = useState<Record<string, any>>({})
|
| 106 |
const [runtimeSaving, setRuntimeSaving] = useState<string | null>(null)
|
|
|
|
| 427 |
}
|
| 428 |
}
|
| 429 |
|
| 430 |
+
const handleOpenDetailModal = async (id: number) => {
|
| 431 |
+
setLoadingDetailId(id)
|
| 432 |
+
try {
|
| 433 |
+
const res = await axios.get(`${API_URL}/forecast/prediksi/${id}`)
|
| 434 |
+
setSelectedCommodityDetail(res.data)
|
| 435 |
+
setShowModelDetailModal(true)
|
| 436 |
+
} catch (err: any) {
|
| 437 |
+
alert(`❌ Gagal mengambil detail model: ${err.response?.data?.detail || err.message}`)
|
| 438 |
+
} finally {
|
| 439 |
+
setLoadingDetailId(null)
|
| 440 |
+
}
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
// ── SETTINGS PORTAL ACTIONS ────────────────────────────────────────────────
|
| 444 |
const handleSaveSetting = async (key: string) => {
|
| 445 |
setRuntimeSaving(key)
|
|
|
|
| 723 |
</div>
|
| 724 |
)}
|
| 725 |
|
| 726 |
+
{/* MODEL EVALUATION COMPARISON MODAL */}
|
| 727 |
+
{showModelDetailModal && selectedCommodityDetail && (
|
| 728 |
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/50 backdrop-blur-sm overflow-y-auto animate-fade-in">
|
| 729 |
+
<div className="bg-white rounded-2xl max-w-3xl w-full p-6 shadow-2xl relative border border-slate-200 my-8 animate-scale-in">
|
| 730 |
+
<button
|
| 731 |
+
onClick={() => setShowModelDetailModal(false)}
|
| 732 |
+
className="absolute top-4 right-4 p-2 text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-all duration-200"
|
| 733 |
+
>
|
| 734 |
+
<X className="w-5 h-5" />
|
| 735 |
+
</button>
|
| 736 |
+
|
| 737 |
+
<div className="flex items-center gap-3 mb-1">
|
| 738 |
+
<div className="w-10 h-10 bg-indigo-50 rounded-xl flex items-center justify-center text-indigo-600 border border-indigo-100">
|
| 739 |
+
<Activity className="w-5 h-5" />
|
| 740 |
+
</div>
|
| 741 |
+
<div>
|
| 742 |
+
<h3 className="text-lg font-bold text-slate-900 leading-snug">
|
| 743 |
+
Evaluasi Model & Proyeksi Harga
|
| 744 |
+
</h3>
|
| 745 |
+
<p className="text-xs text-slate-500">
|
| 746 |
+
{selectedCommodityDetail.komoditas?.nama} — Detail performa model latih terbaru.
|
| 747 |
+
</p>
|
| 748 |
+
</div>
|
| 749 |
+
</div>
|
| 750 |
+
|
| 751 |
+
<hr className="my-5 border-slate-100" />
|
| 752 |
+
|
| 753 |
+
{/* Model Comparison Table */}
|
| 754 |
+
<div className="mb-6">
|
| 755 |
+
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-3">
|
| 756 |
+
Perbandingan Performa Model Latih
|
| 757 |
+
</span>
|
| 758 |
+
|
| 759 |
+
{(!selectedCommodityDetail.all_models || selectedCommodityDetail.all_models.length === 0) ? (
|
| 760 |
+
<div className="bg-slate-50 rounded-xl p-6 text-center text-slate-400 text-xs italic border border-slate-200/50">
|
| 761 |
+
Belum ada model yang dilatih. Silakan jalankan forecast terlebih dahulu.
|
| 762 |
+
</div>
|
| 763 |
+
) : (
|
| 764 |
+
<div className="overflow-x-auto border border-slate-200/80 rounded-xl">
|
| 765 |
+
<table className="w-full text-left border-collapse">
|
| 766 |
+
<thead>
|
| 767 |
+
<tr className="bg-slate-50 border-b border-slate-200 text-[10px] text-slate-500 font-bold uppercase tracking-wider">
|
| 768 |
+
<th className="py-3 px-4">Nama Model</th>
|
| 769 |
+
<th className="py-3 px-3 text-right">MAPE (%)</th>
|
| 770 |
+
<th className="py-3 px-3 text-right">RMSE</th>
|
| 771 |
+
<th className="py-3 px-3 text-right">MAE</th>
|
| 772 |
+
<th className="py-3 px-3 text-right">R² Score</th>
|
| 773 |
+
<th className="py-3 px-4 text-center">Status</th>
|
| 774 |
+
</tr>
|
| 775 |
+
</thead>
|
| 776 |
+
<tbody className="divide-y divide-slate-100 text-xs text-slate-700">
|
| 777 |
+
{selectedCommodityDetail.all_models.map((m: any, i: number) => (
|
| 778 |
+
<tr
|
| 779 |
+
key={i}
|
| 780 |
+
className={`transition ${m.is_active ? 'bg-emerald-50/40 hover:bg-emerald-50/60 font-semibold' : 'hover:bg-slate-50/30'}`}
|
| 781 |
+
>
|
| 782 |
+
<td className="py-3 px-4 font-bold text-slate-900 flex items-center gap-1.5">
|
| 783 |
+
{m.nama_model}
|
| 784 |
+
{m.is_active === 1 && (
|
| 785 |
+
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
| 786 |
+
)}
|
| 787 |
+
</td>
|
| 788 |
+
<td className="py-3 px-3 text-right font-mono font-bold text-indigo-600">
|
| 789 |
+
{m.mape !== null ? `${Number(m.mape).toFixed(2)}%` : '-'}
|
| 790 |
+
</td>
|
| 791 |
+
<td className="py-3 px-3 text-right font-mono text-slate-600">
|
| 792 |
+
{m.rmse !== null ? Number(m.rmse).toLocaleString('id-ID', { maximumFractionDigits: 2 }) : '-'}
|
| 793 |
+
</td>
|
| 794 |
+
<td className="py-3 px-3 text-right font-mono text-slate-600">
|
| 795 |
+
{m.mae !== null ? Number(m.mae).toLocaleString('id-ID', { maximumFractionDigits: 2 }) : '-'}
|
| 796 |
+
</td>
|
| 797 |
+
<td className="py-3 px-3 text-right font-mono text-slate-500">
|
| 798 |
+
{m.r2_score !== null ? Number(m.r2_score).toFixed(4) : '-'}
|
| 799 |
+
</td>
|
| 800 |
+
<td className="py-3 px-4 text-center">
|
| 801 |
+
{m.is_active === 1 ? (
|
| 802 |
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-[9px] font-bold bg-emerald-100 text-emerald-800 border border-emerald-200">
|
| 803 |
+
Terbaik & Terpilih
|
| 804 |
+
</span>
|
| 805 |
+
) : (
|
| 806 |
+
<span className="text-[10px] text-slate-400">Tersimpan</span>
|
| 807 |
+
)}
|
| 808 |
+
</td>
|
| 809 |
+
</tr>
|
| 810 |
+
))}
|
| 811 |
+
</tbody>
|
| 812 |
+
</table>
|
| 813 |
+
</div>
|
| 814 |
+
)}
|
| 815 |
+
</div>
|
| 816 |
+
|
| 817 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 mb-6">
|
| 818 |
+
{/* Proyeksi Harga Card */}
|
| 819 |
+
<div className="bg-slate-50 rounded-2xl p-5 border border-slate-200/60">
|
| 820 |
+
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-3">
|
| 821 |
+
Hasil Proyeksi Harga 7 Hari Ke Depan
|
| 822 |
+
</span>
|
| 823 |
+
|
| 824 |
+
{(!selectedCommodityDetail.prediksi_7hari || selectedCommodityDetail.prediksi_7hari.length === 0) ? (
|
| 825 |
+
<div className="text-slate-400 text-xs italic py-4">Belum ada proyeksi harga.</div>
|
| 826 |
+
) : (
|
| 827 |
+
<div className="space-y-2.5 max-h-[220px] overflow-y-auto pr-1">
|
| 828 |
+
{selectedCommodityDetail.prediksi_7hari.map((p: any, idx: number) => (
|
| 829 |
+
<div key={idx} className="flex justify-between items-center bg-white p-2.5 rounded-xl border border-slate-100 shadow-sm">
|
| 830 |
+
<span className="text-xs font-semibold text-slate-600">
|
| 831 |
+
{new Date(p.tanggal_target).toLocaleDateString('id-ID', { weekday: 'short', day: 'numeric', month: 'short' })}
|
| 832 |
+
</span>
|
| 833 |
+
<span className="text-xs font-bold text-slate-900">
|
| 834 |
+
Rp {Number(p.harga_prediksi).toLocaleString('id-ID', { maximumFractionDigits: 0 })}
|
| 835 |
+
</span>
|
| 836 |
+
</div>
|
| 837 |
+
))}
|
| 838 |
+
</div>
|
| 839 |
+
)}
|
| 840 |
+
</div>
|
| 841 |
+
|
| 842 |
+
{/* Ringkasan Analisis Card */}
|
| 843 |
+
<div className="flex flex-col gap-4">
|
| 844 |
+
<div className="bg-slate-50 rounded-2xl p-5 border border-slate-200/60 flex-1">
|
| 845 |
+
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-2">
|
| 846 |
+
Kesimpulan Analisis
|
| 847 |
+
</span>
|
| 848 |
+
|
| 849 |
+
{selectedCommodityDetail.ringkasan ? (
|
| 850 |
+
<div>
|
| 851 |
+
<div className="flex items-center gap-1.5 mb-2">
|
| 852 |
+
<span className="text-xs font-black text-slate-900">
|
| 853 |
+
{selectedCommodityDetail.ringkasan.status_analisis}
|
| 854 |
+
</span>
|
| 855 |
+
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wide
|
| 856 |
+
${selectedCommodityDetail.ringkasan.tren?.includes('naik') ? 'bg-rose-50 text-rose-700 border border-rose-100' :
|
| 857 |
+
selectedCommodityDetail.ringkasan.tren?.includes('turun') ? 'bg-emerald-50 text-emerald-700 border border-emerald-100' :
|
| 858 |
+
'bg-slate-100 text-slate-600 border border-slate-200'}`}
|
| 859 |
+
>
|
| 860 |
+
{selectedCommodityDetail.ringkasan.tren?.replace('_', ' ')}
|
| 861 |
+
</span>
|
| 862 |
+
</div>
|
| 863 |
+
<p className="text-xs text-slate-500 leading-relaxed">
|
| 864 |
+
{selectedCommodityDetail.ringkasan.deskripsi_status}
|
| 865 |
+
</p>
|
| 866 |
+
</div>
|
| 867 |
+
) : (
|
| 868 |
+
<div className="text-slate-400 text-xs italic py-4">Belum ada data kesimpulan.</div>
|
| 869 |
+
)}
|
| 870 |
+
</div>
|
| 871 |
+
|
| 872 |
+
{/* Insights List */}
|
| 873 |
+
{selectedCommodityDetail.insights && selectedCommodityDetail.insights.length > 0 && (
|
| 874 |
+
<div className="bg-slate-50 rounded-2xl p-5 border border-slate-200/60">
|
| 875 |
+
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-3">
|
| 876 |
+
Temuan & Insight Pasar
|
| 877 |
+
</span>
|
| 878 |
+
<ul className="space-y-2">
|
| 879 |
+
{selectedCommodityDetail.insights.map((ins: any, idx: number) => (
|
| 880 |
+
<li key={idx} className="flex gap-2 text-xs text-slate-600 leading-normal">
|
| 881 |
+
<span className="w-1.5 h-1.5 rounded-full bg-indigo-500 mt-1.5 flex-shrink-0" />
|
| 882 |
+
<span>{ins.konten}</span>
|
| 883 |
+
</li>
|
| 884 |
+
))}
|
| 885 |
+
</ul>
|
| 886 |
+
</div>
|
| 887 |
+
)}
|
| 888 |
+
</div>
|
| 889 |
+
</div>
|
| 890 |
+
|
| 891 |
+
<div className="text-right pt-4 border-t border-slate-100">
|
| 892 |
+
<button
|
| 893 |
+
onClick={() => setShowModelDetailModal(false)}
|
| 894 |
+
className="bg-slate-900 hover:bg-slate-800 text-white font-bold py-2.5 px-6 rounded-xl transition-all duration-200 shadow-md text-xs"
|
| 895 |
+
>
|
| 896 |
+
Tutup Evaluasi
|
| 897 |
+
</button>
|
| 898 |
+
</div>
|
| 899 |
+
</div>
|
| 900 |
+
</div>
|
| 901 |
+
)}
|
| 902 |
+
|
| 903 |
{/* MAIN CONTAINER CONTENT */}
|
| 904 |
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-8">
|
| 905 |
|
|
|
|
| 1372 |
<span className="text-xs font-bold text-slate-900 block">{k.nama}</span>
|
| 1373 |
<span className="text-[10px] text-slate-400 block mt-0.5">Volatilitas: <strong className={k.volatile ? "text-amber-600" : "text-emerald-600"}>{k.volatile ? "Tinggi" : "Rendah"}</strong></span>
|
| 1374 |
</div>
|
| 1375 |
+
<div className="flex gap-1.5 items-center">
|
| 1376 |
+
<button
|
| 1377 |
+
onClick={() => handleOpenDetailModal(k.id)}
|
| 1378 |
+
disabled={loadingDetailId === k.id || isRunning}
|
| 1379 |
+
className="flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-[10px] font-bold bg-slate-100 hover:bg-slate-200 text-slate-700 border border-slate-200 transition-all duration-150 active:scale-[0.98] disabled:opacity-50"
|
| 1380 |
+
>
|
| 1381 |
+
{loadingDetailId === k.id ? (
|
| 1382 |
+
<RefreshCw className="w-3 h-3 animate-spin" />
|
| 1383 |
+
) : (
|
| 1384 |
+
<Info className="w-3 h-3 text-slate-400" />
|
| 1385 |
+
)}
|
| 1386 |
+
Evaluasi
|
| 1387 |
+
</button>
|
| 1388 |
{isRunning ? (
|
| 1389 |
<button
|
| 1390 |
onClick={handleStopForecast}
|