ai-detector-backend / script.js
Alstears's picture
Upload 135 files
2fe8f88 verified
const API_URL = window.location.origin;
let currentUser = "__guest__";
async function updateGlobalStats() {
try {
const url = `${API_URL}/api/accuracy-report`;
const res = await fetch(url);
const data = await res.json();
const s = data.stats;
const totalFiles = data.scan_count + data.batch_images;
const adAcc = document.getElementById("ad-accuracy-text");
if (adAcc) adAcc.innerText = `Akurasi ${s.accuracy}%`;
const adCount = document.getElementById("ad-detected-count");
if (adCount) adCount.innerText = `${totalFiles} File`;
// Update landing page elements
const landingTotal = document.getElementById("landing-stat-total");
if (landingTotal) landingTotal.innerText = totalFiles;
const landingCorrect = document.getElementById("landing-stat-correct");
if (landingCorrect) landingCorrect.innerText = s.correct;
const landingWrong = document.getElementById("landing-stat-wrong");
if (landingWrong) landingWrong.innerText = s.wrong;
const landingAcc = document.getElementById("landing-stat-accuracy");
if (landingAcc) landingAcc.innerText = `${s.accuracy}%`;
updateGuestScanCountUI();
return { s, data, totalFiles };
} catch (e) {
console.error("Gagal update global stats:", e);
}
}
// --- 1. LOADING SCREEN LOGIC ---
window.onload = () => {
document.getElementById("loading-screen").classList.add("hidden");
document.getElementById("auth-page").classList.remove("hidden");
document.getElementById("auth-page").style.display = "flex";
updateGlobalStats();
checkApiConnection();
// Dynamic connection heartbeat every 10 seconds (Poin 1)
setInterval(checkApiConnection, 10000);
};
async function checkApiConnection() {
const dot = document.getElementById("api-status-dot");
const text = document.getElementById("api-status-text");
const indicator = document.getElementById("api-status-indicator");
if (!dot || !text || !indicator) return;
try {
const res = await fetch(`${API_URL}/api/accuracy-report`);
if (res.ok) {
dot.style.background = "#2ed573";
dot.style.boxShadow = "0 0 10px #2ed573";
text.innerText = "API Online";
text.style.color = "#2ed573";
indicator.style.borderColor = "rgba(46, 213, 115, 0.3)";
} else {
throw new Error();
}
} catch (e) {
dot.style.background = "#ff4757";
dot.style.boxShadow = "0 0 10px #ff4757";
text.innerText = "API Offline";
text.style.color = "#ff4757";
indicator.style.borderColor = "rgba(255, 71, 87, 0.3)";
}
}
// --- 2. ANIMASI PARTIKEL SENTUHAN LAYAR ---
document.addEventListener("click", (e) => {
createParticles(e.clientX, e.clientY);
});
function createParticles(x, y) {
const colors = ['#FFD700', '#0052D4', '#FFFACD', '#4D8BF5'];
for (let i = 0; i < 8; i++) { // Buat 8 partikel per klik
const particle = document.createElement("div");
particle.classList.add("click-particle");
const size = Math.random() * 10 + 5; // Ukuran 5-15px
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
particle.style.left = `${x}px`;
particle.style.top = `${y}px`;
particle.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
// Arah random terbang partikel
const tx = (Math.random() - 0.5) * 150;
const ty = (Math.random() - 0.5) * 150;
particle.style.setProperty('--tx', `${tx}px`);
particle.style.setProperty('--ty', `${ty}px`);
document.body.appendChild(particle);
setTimeout(() => particle.remove(), 800); // Hapus partikel setelah animasi selesai
}
}
// --- 3. AUTH LOGIC ---
function switchTab(tab) {
document.getElementById("form-login").classList.toggle("hidden", tab !== "login");
document.getElementById("form-register").classList.toggle("hidden", tab !== "register");
document.getElementById("tab-login").classList.toggle("active", tab === "login");
document.getElementById("tab-register").classList.toggle("active", tab === "register");
}
function updateUserTrustScoreUI(score) {
const scoreEl = document.getElementById("user-trust-score");
const badgeEl = document.getElementById("user-trust-badge");
if (!scoreEl || !badgeEl) return;
scoreEl.innerText = score;
if (score >= 80) {
badgeEl.innerText = "πŸ›‘οΈ Pakar";
badgeEl.style.background = "rgba(46, 204, 113, 0.15)";
badgeEl.style.color = "var(--success)";
badgeEl.style.borderColor = "rgba(46, 204, 113, 0.25)";
} else if (score < 50) {
badgeEl.innerText = "⚠️ Dicurigai";
badgeEl.style.background = "rgba(255, 71, 87, 0.15)";
badgeEl.style.color = "var(--danger)";
badgeEl.style.borderColor = "rgba(255, 71, 87, 0.25)";
} else {
badgeEl.innerText = "Standar";
badgeEl.style.background = "rgba(255, 215, 0, 0.15)";
badgeEl.style.color = "var(--yellow-main)";
badgeEl.style.borderColor = "rgba(255, 215, 0, 0.25)";
}
}
async function handleLogin(e) {
e.preventDefault();
const u = document.getElementById("login-user").value;
const p = document.getElementById("login-pass").value;
const formData = new URLSearchParams({ username: u, password: p });
try {
const res = await fetch(`${API_URL}/api/login`, { method: "POST", body: formData });
const data = await res.json();
if (res.ok) {
currentUser = data.username;
document.getElementById("user-name").innerText = data.name;
updateUserTrustScoreUI(data.trust_score || 50);
const inputImg = document.getElementById("input-image");
if (inputImg) {
if (currentUser === "Sandikad") {
inputImg.setAttribute("multiple", "multiple");
} else {
inputImg.removeAttribute("multiple");
}
}
const downloadBtn = document.getElementById("btn-download-feedback");
if (downloadBtn) {
if (currentUser === "Sandikad") {
downloadBtn.style.display = "inline-block";
} else {
downloadBtn.style.display = "none";
}
}
// Pulihkan visibilitas elemen UI dari mode tamu
document.getElementById("user-trust-container").style.display = "flex";
document.getElementById("guest-scan-container").style.display = "none";
document.getElementById("menu-batch-test").style.display = "block";
document.getElementById("menu-history").style.display = "block";
document.getElementById("menu-accuracy").style.display = "block";
document.getElementById("auth-page").classList.add("hidden");
document.getElementById("main-app").classList.remove("hidden");
document.getElementById("app-style").href = "style.css";
closeAuthModal();
loadDashboard();
loadHistory();
} else {
document.getElementById("login-error").innerText = data.detail;
}
} catch (err) {
document.getElementById("login-error").innerText = "Gagal menghubungi backend";
}
}
async function handleRegister(e) {
e.preventDefault();
const n = document.getElementById("reg-name").value;
const u = document.getElementById("reg-user").value;
const p = document.getElementById("reg-pass").value;
const formData = new URLSearchParams({ name: n, username: u, password: p });
const res = await fetch(`${API_URL}/api/register`, { method: "POST", body: formData });
const data = await res.json();
if (res.ok) {
alert("Registrasi berhasil! Silakan login.");
switchTab('login');
} else {
document.getElementById("reg-error").innerText = data.detail;
}
}
function handleLogout() {
currentUser = "__guest__";
const inputImg = document.getElementById("input-image");
if (inputImg) {
inputImg.removeAttribute("multiple");
}
const downloadBtn = document.getElementById("btn-download-feedback");
if (downloadBtn) {
downloadBtn.style.display = "none";
}
// Pulihkan kembali elemen UI ke default untuk login berikutnya
document.getElementById("user-trust-container").style.display = "flex";
document.getElementById("guest-scan-container").style.display = "none";
document.getElementById("menu-batch-test").style.display = "block";
document.getElementById("menu-history").style.display = "block";
document.getElementById("menu-accuracy").style.display = "block";
document.getElementById("main-app").classList.add("hidden");
document.getElementById("auth-page").classList.remove("hidden");
document.getElementById("app-style").href = "template/style.css";
}
// --- 4. NAVIGATION LOGIC ---
function showSection(sectionId) {
document.querySelectorAll(".section").forEach(s => s.classList.remove("active"));
document.querySelectorAll(".nav-item").forEach(n => n.classList.remove("active"));
document.getElementById(`sec-${sectionId}`).classList.add("active");
// Safe sidebar nav item highlight resolution (works from sidebar and shortcuts!)
const navItems = document.querySelectorAll(".nav-item");
navItems.forEach(n => {
if (n.getAttribute("onclick") && n.getAttribute("onclick").includes(`'${sectionId}'`)) {
n.classList.add("active");
}
});
if (sectionId === "dashboard") loadDashboard();
if (sectionId === "history") loadHistory();
if (sectionId === "accuracy") loadAccuracyReport();
}
async function loadDashboard() {
if (!currentUser) return;
try {
const statsData = await updateGlobalStats();
if (!statsData) return;
const { s, data } = statsData;
const accColor = s.accuracy >= 70 ? "var(--success)" : s.accuracy >= 40 ? "var(--blue-dark)" : "var(--danger)";
document.getElementById("dashboard-stats").innerHTML = `
<div class="stat-card blue"><h3>${s.total}</h3><p>Total Test</p></div>
<div class="stat-card yellow"><h3>${s.correct}</h3><p>Benar</p></div>
<div class="stat-card blue"><h3>${s.wrong}</h3><p>Salah</p></div>
<div class="stat-card yellow"><h3 style="color:${accColor}">${s.accuracy}%</h3><p>Akurasi</p></div>`;
document.getElementById("dashboard-learning").innerHTML = `
Data pembelajaran: <b style="color:var(--yellow-main)">${data.learning_data_count}</b> gambar siap training
&nbsp; <button class="btn-scan" style="padding:5px 15px;font-size:12px" onclick="showSection('accuracy');event.target.blur()">Detail β†’</button>`;
} catch (e) { }
}
// --- 5. FILE SCANNING LOGIC ---
document.getElementById("input-image").addEventListener("change", (e) => {
const files = e.target.files;
if (files.length > 1) {
document.getElementById("img-name").innerText = `${files.length} file dipilih`;
} else {
document.getElementById("img-name").innerText = files[0]?.name || "Tidak ada file dipilih";
}
});
async function simulateForensicAnalysis(resultBox, fetchPromise) {
const steps = [
"[Step 1/12] Metadata normal.",
"[Step 2/12] Analisis Pixel (Komite Binary). Score: 70%",
"[Step 3/12] Analisis Pola Sensor CFA selesai.",
"[Step 4/12] Pencarian jejak Hex/Binary selesai.",
"[Step 5/12] Pemetaan Noise selesai.",
"[Step 6/12] Analisis Geometri selesai.",
"[Step 7/12] Pencarian Artifact visual selesai.",
"[Step 8/12] Verifikasi Tipe File selesai.",
"[Step 9/12] Analisis Konsistensi Pencahayaan selesai.",
"[Step 10/12] Pemindaian Duplikasi Pixel selesai.",
"[Step 11/12] Analisis Pola Frekuensi GAN selesai.",
"[Step 12/12] Inspeksi Tingkat Error (ELA) selesai."
];
resultBox.innerHTML = "";
resultBox.classList.remove("hidden", "ai", "real");
const title = document.createElement("h3");
title.style.color = "var(--yellow-main)";
title.style.marginBottom = "15px";
title.innerText = "⏳ Melakukan Analisis Forensik Citra...";
resultBox.appendChild(title);
const container = document.createElement("div");
container.style.textAlign = "left";
container.style.fontSize = "13px";
container.style.fontFamily = "monospace";
container.style.color = "#2ed573";
container.style.lineHeight = "1.7";
container.style.padding = "15px";
container.style.background = "rgba(0,0,0,0.4)";
container.style.border = "1px solid rgba(255, 215, 0, 0.2)";
container.style.borderRadius = "12px";
container.style.maxHeight = "220px";
container.style.overflowY = "auto";
container.style.marginBottom = "10px";
resultBox.appendChild(container);
for (let i = 0; i < steps.length; i++) {
const stepDiv = document.createElement("div");
stepDiv.innerText = steps[i];
stepDiv.style.opacity = "0";
stepDiv.style.transform = "translateX(-8px)";
stepDiv.style.transition = "all 0.15s ease-out";
container.appendChild(stepDiv);
stepDiv.offsetHeight;
stepDiv.style.opacity = "1";
stepDiv.style.transform = "translateX(0)";
container.scrollTop = container.scrollHeight;
const delay = i === 1 ? 300 : (80 + Math.random() * 100);
await new Promise(resolve => setTimeout(resolve, delay));
}
const waitDiv = document.createElement("div");
waitDiv.innerText = "πŸ”„ Menyambungkan ke Model Ensemble v4...";
waitDiv.style.color = "var(--yellow-main)";
waitDiv.style.fontWeight = "bold";
waitDiv.style.marginTop = "8px";
container.appendChild(waitDiv);
container.scrollTop = container.scrollHeight;
}
function toggleForensicLogs(btn) {
const content = btn.nextElementSibling;
const arrow = btn.querySelector("span:last-child");
if (content.classList.contains("hidden")) {
content.classList.remove("hidden");
arrow.innerText = "β–²";
} else {
content.classList.add("hidden");
arrow.innerText = "β–Ό";
}
}
function toggleBulkForensicLogs(btn) {
const logsDiv = btn.nextElementSibling;
if (logsDiv.classList.contains("hidden")) {
logsDiv.classList.remove("hidden");
btn.innerText = "βœ–οΈ Tutup Log";
} else {
logsDiv.classList.add("hidden");
btn.innerText = "πŸ‘οΈ Lihat Log";
}
}
async function scanFile() {
const fileInput = document.getElementById("input-image");
const resultBox = document.getElementById("result-image");
if (currentUser === "__guest__") {
let count = parseInt(localStorage.getItem("guest_scan_count") || "0");
if (count >= 5) {
alert("Batas scan gratis Anda (5 kali) sudah habis!\nSilakan Login atau Register terlebih dahulu untuk scan tanpa batas.");
handleLogout();
return;
}
}
if (!fileInput.files || fileInput.files.length === 0) return alert("Pilih file terlebih dahulu!");
// Case 1: Multiple files (Sandikad only)
if (fileInput.files.length > 1) {
if (currentUser !== "Sandikad") {
alert("Akses Ditolak: Hanya pengguna 'Sandikad' yang diizinkan untuk memindai banyak gambar sekaligus.");
return;
}
const files = Array.from(fileInput.files);
// Check file size limits
for (let f of files) {
if (f.size > 10 * 1024 * 1024) {
return alert(`Ukuran file "${f.name}" melebihi batas 10MB!`);
}
}
// Show unified progress log
resultBox.className = "result-box";
resultBox.classList.remove("hidden", "ai", "real");
resultBox.innerHTML = `
<h3 style="color: var(--yellow-main); margin-bottom: 15px;">⏳ Melakukan Analisis Forensik Citra Massal (${files.length} Gambar)...</h3>
<div id="bulk-progress-log" style="text-align: left; font-size: 13px; font-family: monospace; color: #2ed573; line-height: 1.7; padding: 15px; background: rgba(0,0,0,0.4); border: 1px solid rgba(255, 215, 0, 0.2); border-radius: 12px; max-height: 250px; overflow-y: auto;">
</div>
`;
const progressLog = document.getElementById("bulk-progress-log");
const scanResults = [];
let lastTrustScore = 50;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const logItem = document.createElement("div");
logItem.innerText = `πŸ”„ [${i+1}/${files.length}] Memindai ${file.name}...`;
progressLog.appendChild(logItem);
progressLog.scrollTop = progressLog.scrollHeight;
const formData = new FormData();
formData.append("file", file);
formData.append("username", currentUser);
try {
const res = await fetch(`${API_URL}/api/scan-image`, { method: "POST", body: formData });
const data = await res.json();
if (res.ok) {
scanResults.push({ file, success: true, data });
logItem.innerHTML = `βœ… [${i+1}/${files.length}] Selesai: <span style="color: ${data.is_ai ? "var(--danger)" : "var(--success)"}; font-weight: bold;">${data.is_ai ? "AI" : "REAL"}</span> (${data.accuracy}%) - ${file.name}`;
if (data.trust_score !== undefined) {
lastTrustScore = data.trust_score;
}
} else {
scanResults.push({ file, success: false, error: data.detail || "Gagal dipindai" });
logItem.innerHTML = `❌ [${i+1}/${files.length}] Gagal: ${data.detail || "Error"} - ${file.name}`;
logItem.style.color = "var(--danger)";
}
} catch (err) {
scanResults.push({ file, success: false, error: "Gagal menghubungi server" });
logItem.innerHTML = `❌ [${i+1}/${files.length}] Gagal koneksi: ${file.name}`;
logItem.style.color = "var(--danger)";
}
progressLog.scrollTop = progressLog.scrollHeight;
// Small delay to make it feel natural
await new Promise(resolve => setTimeout(resolve, 300));
}
// Render multiple results view
updateUserTrustScoreUI(lastTrustScore);
updateGlobalStats();
let tbodyHtml = "";
scanResults.forEach((r, idx) => {
if (!r.success) {
tbodyHtml += `
<tr>
<td>${r.file.name}</td>
<td colspan="5" style="color: var(--danger)">Error: ${r.error}</td>
</tr>
`;
return;
}
const data = r.data;
const predColor = data.is_ai ? "var(--danger)" : "var(--success)";
const predText = data.is_ai ? "AI" : "REAL";
// Badges
let badgesHtml = "";
if (data.is_grayscale) {
badgesHtml += `<span class="badge" style="background:rgba(149,165,166,0.15);color:#bdc3c7;border:1px solid rgba(149,165,166,0.25);padding:2px 6px;border-radius:4px;font-size:10px;margin-right:4px">βšͺ Monokrom</span>`;
}
if (data.is_dark) {
badgesHtml += `<span class="badge" style="background:rgba(230,126,34,0.1);color:#e67e22;border:1px solid rgba(230,126,34,0.25);padding:2px 6px;border-radius:4px;font-size:10px;margin-right:4px">πŸŒ™ Low Light</span>`;
}
if (data.is_outlier) {
badgesHtml += `<span class="badge" style="background:rgba(255,71,87,0.1);color:var(--danger);border:1px solid rgba(255,71,87,0.25);padding:2px 6px;border-radius:4px;font-size:10px;margin-right:4px">πŸ’‘ Outlier</span>`;
}
if (data.is_trap) {
badgesHtml += `<span class="badge" style="background:rgba(230,126,34,0.15);color:#e67e22;border:1px solid rgba(230,126,34,0.25);padding:2px 6px;border-radius:4px;font-size:10px;margin-right:4px">πŸ›‘οΈ Trap</span>`;
}
// Forensic logs toggle content
let stepsHtml = "";
if (data.forensic_analysis_logs) {
for (let s = 1; s <= 12; s++) {
const stepText = data.forensic_analysis_logs[`step_${s}`] || `[Step ${s}/12] Analisis selesai.`;
stepsHtml += `<div style="margin-bottom: 4px; border-bottom: 1px dashed rgba(255,255,255,0.05); padding-bottom: 2px;">${stepText}</div>`;
}
}
tbodyHtml += `
<tr>
<td style="font-weight: bold;">${data.filename}</td>
<td style="color: ${predColor}; font-weight: bold;">${predText}</td>
<td style="color: ${predColor}">${data.accuracy}%</td>
<td>${badgesHtml || "-"}</td>
<td>
<button class="btn-refresh" style="padding: 4px 10px; font-size: 11px; margin: 0;" onclick="toggleBulkForensicLogs(this)">πŸ‘οΈ Lihat Log</button>
<div class="logs-content hidden" style="margin-top: 8px; background: rgba(0,0,0,0.5); border: 1px dashed rgba(255,215,0,0.3); border-radius: 8px; padding: 10px; font-family: monospace; font-size: 11px; color: #2ed573; line-height: 1.5; text-align: left; max-height: 150px; overflow-y: auto;">
${stepsHtml}
</div>
</td>
<td>
<div style="display: flex; gap: 6px; justify-content: center; align-items: center;">
<button class="btn-scan" style="padding: 4px 10px; font-size: 11px; margin: 0; background: var(--success); border-color: var(--success);" onclick="confirmSingleResult('${data.filename}', '${data.is_ai ? "AI" : "REAL"}', '${data.accuracy}', '${data.feedback_id || ""}', this)">βœ… Benar</button>
<button class="btn-scan" style="padding: 4px 10px; font-size: 11px; margin: 0; background: var(--danger); border-color: var(--danger);" onclick="correctSingleResult('${data.filename}', '${data.is_ai ? "AI" : "REAL"}', '${data.accuracy}', '${data.feedback_id || ""}', this)">❌ Salah</button>
</div>
</td>
</tr>
`;
});
resultBox.innerHTML = `
<h3 style="color: var(--yellow-main); margin-bottom: 15px;">πŸ“Š Hasil Analisis Citra Massal (${files.length} Gambar)</h3>
<div class="table-wrap">
<table class="history-table">
<thead>
<tr>
<th>Gambar</th>
<th>Prediksi</th>
<th>Keakuratan</th>
<th>Info Deteksi</th>
<th>Log Forensik (12 Langkah)</th>
<th>Tindakan / Feedback</th>
</tr>
</thead>
<tbody>
${tbodyHtml}
</tbody>
</table>
</div>
`;
return;
}
// Case 2: Single file
const file = fileInput.files[0];
if (file.size > 10 * 1024 * 1024) return alert("Ukuran file melebihi batas 10MB!");
const formData = new FormData();
formData.append("file", file);
formData.append("username", currentUser);
const fetchPromise = fetch(`${API_URL}/api/scan-image`, { method: "POST", body: formData });
await simulateForensicAnalysis(resultBox, fetchPromise);
try {
const res = await fetchPromise;
const data = await res.json();
// CEK OTOMATIS GAMBAR HITAM PUTIH UNTUK TAMPILKAN BANNER ALERT
const warningBanner = document.getElementById("warning-banner-monokrom");
if (warningBanner) {
if (data.is_monochrome_detected === true) {
warningBanner.style.display = "block";
} else {
warningBanner.style.display = "none";
}
}
if (res.ok) {
if (currentUser === "__guest__") {
let count = parseInt(localStorage.getItem("guest_scan_count") || "0");
count++;
localStorage.setItem("guest_scan_count", count.toString());
updateGuestScanCountUI();
}
const statusClass = data.is_ai ? "ai" : "real";
const statusText = data.is_ai ? "⚠️ GAMBAR PALSU (AI)" : "βœ… GAMBAR ASLI (REAL)";
const statusColor = data.is_ai ? "var(--danger)" : "var(--success)";
let logsHtml = '';
if (data.forensic_analysis_logs && Object.keys(data.forensic_analysis_logs).length > 0) {
logsHtml = `
<div class="forensic-logs-detail" style="margin-top: 15px; margin-bottom: 15px; text-align: left;">
<button class="btn-refresh" style="width: 100%; text-align: left; padding: 10px 15px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,215,0,0.25); border-radius: 8px; color: var(--yellow-main); cursor: pointer; font-size: 13px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 0;" onclick="toggleForensicLogs(this)">
<span>πŸ” Lihat Log Detil Forensik (12 Langkah)</span>
<span style="font-size:10px">β–Ό</span>
</button>
<div class="logs-content hidden" style="background: rgba(0,0,0,0.5); border: 1px solid rgba(255,215,0,0.15); border-top: none; border-radius: 0 0 8px 8px; padding: 15px; font-family: monospace; font-size: 12px; color: #2ed573; line-height: 1.6; max-height: 250px; overflow-y: auto;">
`;
for (let i = 1; i <= 12; i++) {
const stepText = data.forensic_analysis_logs[`step_${i}`] || `[Step ${i}/12] Analisis selesai.`;
logsHtml += `<div style="margin-bottom: 6px; border-bottom: 1px dashed rgba(255,255,255,0.05); padding-bottom: 4px;">${stepText}</div>`;
}
logsHtml += `
</div>
</div>
`;
}
resultBox.className = `result-box ${statusClass}`;
resultBox.innerHTML = `
<div class="result-title" style="color: ${statusColor}">
<span>${statusText}</span>
<span>Akurasi: ${data.accuracy}%</span>
</div>
<div class="badge-bar" style="display:flex;gap:8px;margin-bottom:15px;flex-wrap:wrap">
<span class="badge" style="background:rgba(255,215,0,0.1);color:var(--yellow-main);border:1px solid rgba(255,215,0,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">
🧬 Cosine Similarity: ${(data.similarity * 100).toFixed(1)}%
</span>
${data.is_outlier
? `<span class="badge" style="background:rgba(255,71,87,0.1);color:var(--danger);border:1px solid rgba(255,71,87,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">πŸ’‘ Data Asing (Outlier)</span>`
: `<span class="badge" style="background:rgba(46,204,113,0.1);color:var(--success);border:1px solid rgba(46,204,113,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">🎯 Klasifikasi Aman</span>`
}
${data.is_trap
? `<span class="badge" style="background:rgba(230,126,34,0.15);color:#e67e22;border:1px solid rgba(230,126,34,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">⚠️ Trap Image Mode</span>`
: ''
}
${data.is_dark
? `<span class="badge" style="background:rgba(230,126,34,0.1);color:#e67e22;border:1px solid rgba(230,126,34,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">πŸŒ™ Low Light (Cahaya Rendah)</span>`
: ''
}
${data.is_grayscale
? `<span class="badge" style="background:rgba(149,165,166,0.15);color:#bdc3c7;border:1px solid rgba(149,165,166,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">βšͺ Monokrom (Hitam Putih)</span>`
: ''
}
</div>
${(data.is_dark || data.is_grayscale)
? `
<div style="background: rgba(230,126,34,0.1); border: 1px dashed rgba(230,126,34,0.3); border-radius: 8px; padding: 12px; margin-bottom: 15px; font-size: 12px; color: #f39c12; line-height: 1.5; text-align: left;">
<b>⚠️ Rekomendasi Kondisi Deteksi:</b><br/>
${data.is_dark ? 'β€’ Cahaya terdeteksi rendah (kecerahan rata-rata: ' + data.avg_brightness + '/255). Hal ini memicu noise sensor kamera yang dapat mengganggu keakuratan forensik AI.<br/>' : ''}
${data.is_grayscale ? 'β€’ Gambar monokrom/hitam-putih terdeteksi. Kehilangan informasi kromatik (saluran warna RGB) secara drastis dapat menurunkan performa klasifikasi model AI.<br/>' : ''}
<i style="display:block;margin-top:6px;color:rgba(255,255,255,0.7)">Disarankan untuk melakukan scan ulang menggunakan foto dengan pencahayaan cukup and penuh warna (Full RGB).</i>
</div>
`
: ''
}
${logsHtml}
<div class="details-grid">
<div class="detail-item">
<div class="detail-label">Nama File</div>
<div class="detail-value">${data.filename}</div>
</div>
<div class="detail-item">
<div class="detail-label">Tipe</div>
<div class="detail-value">${data.type.toUpperCase()}</div>
</div>
<div class="detail-item">
<div class="detail-label">Ukuran File</div>
<div class="detail-value">${data.file_size}</div>
</div>
<div class="detail-item">
<div class="detail-label">Sumber Deteksi</div>
<div class="detail-value" style="font-size:12px">${data.source}</div>
</div>
<div class="detail-item">
<div class="detail-label">Tanggal Scan</div>
<div class="detail-value">${data.date}</div>
</div>
<div class="detail-item">
<div class="detail-label">Keakuratan</div>
<div class="detail-value" style="color: ${statusColor}">${data.accuracy}%</div>
</div>
</div>
<div style="margin-top:15px;padding-top:15px;border-top:1px solid rgba(255,255,255,0.2)">
<p style="margin-bottom:10px;color:var(--yellow-main)">Apakah hasil ini benar?</p>
<button class="btn-scan" style="padding:8px 25px;font-size:14px;margin-right:10px" onclick="confirmSingleResult('${data.filename}', '${data.is_ai ? "AI" : "REAL"}', '${data.accuracy}', '${data.feedback_id || ""}', this)">βœ… Benar</button>
<button class="btn-scan" style="padding:8px 25px;font-size:14px" onclick="correctSingleResult('${data.filename}', '${data.is_ai ? "AI" : "REAL"}', '${data.accuracy}', '${data.feedback_id || ""}', this)">❌ Salah</button>
<div id="single-correction-area" style="margin-top:10px"></div>
</div>
`;
if (currentUser !== "__guest__") {
updateUserTrustScoreUI(data.trust_score || 50);
}
} else {
resultBox.innerHTML = `<h3 style="color: var(--danger)">Error: ${data.detail}</h3>`;
}
} catch (err) {
resultBox.innerHTML = "<h3 style='color: var(--danger)'>Gagal terhubung ke server Backend.</h3>";
}
}
function confirmSingleResult(filename, prediction, accuracy, feedbackId, btn) {
fetch(`${API_URL}/api/correction-single`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: currentUser,
filename: filename,
original_prediction: prediction,
correct_label: prediction,
confidence: parseFloat(accuracy),
feedback_id: feedbackId
})
})
.then(res => res.json())
.then(data => {
updateGlobalStats();
if (data.is_trap) {
if (currentUser !== "__guest__") {
updateUserTrustScoreUI(data.new_trust);
const scoreDiff = data.trust_change > 0 ? `+${data.trust_change}` : `${data.trust_change}`;
const statusIcon = data.trap_correct ? "πŸŽ‰ BENAR!" : "❌ SALAH!";
alert(`πŸ›‘οΈ TRAP IMAGE DETECTED!\nFeedback Anda ${statusIcon}\nSkor Kredibilitas Anda: ${scoreDiff} (Sekarang: ${data.new_trust}/100)`);
} else {
alert(`πŸ›‘οΈ TRAP IMAGE DETECTED! (Mode Tamu)`);
}
}
})
.catch(() => { });
btn.closest("div").innerHTML = "<p style='color:var(--success)'>βœ… Konfirmasi tersimpan!</p>";
}
function correctSingleResult(filename, prediction, accuracy, feedbackId, btn) {
const correctLabel = prediction === "AI" ? "REAL" : "AI";
fetch(`${API_URL}/api/correction-single`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: currentUser,
filename: filename,
original_prediction: prediction,
correct_label: correctLabel,
confidence: parseFloat(accuracy),
feedback_id: feedbackId
})
})
.then(res => res.json())
.then(data => {
updateGlobalStats();
if (data.is_trap) {
if (currentUser !== "__guest__") {
updateUserTrustScoreUI(data.new_trust);
const scoreDiff = data.trust_change > 0 ? `+${data.trust_change}` : `${data.trust_change}`;
const statusIcon = data.trap_correct ? "πŸŽ‰ BENAR!" : "❌ SALAH!";
alert(`πŸ›‘οΈ TRAP IMAGE DETECTED!\nFeedback Anda ${statusIcon}\nSkor Kredibilitas Anda: ${scoreDiff} (Sekarang: ${data.new_trust}/100)`);
} else {
alert(`πŸ›‘οΈ TRAP IMAGE DETECTED! (Mode Tamu)`);
}
}
})
.catch(() => { });
btn.closest("div").innerHTML = `<p style='color:var(--success)'>βœ… Koreksi tersimpan! (seharusnya ${correctLabel})</p>`;
}
// --- 7. BATCH TEST LOGIC ---
let batchFiles = [];
let batchLabels = [];
document.getElementById("input-batch").addEventListener("change", (e) => {
const files = e.target.files;
if (!files.length) return;
const folderName = files[0].webkitRelativePath.split('/')[0];
document.getElementById("batch-folder-name").innerText = folderName;
batchFiles = [];
batchLabels = [];
const listDiv = document.getElementById("batch-file-list");
listDiv.innerHTML = "<h4 style='margin-bottom:10px'>File ditemukan:</h4>";
const table = document.createElement("table");
table.className = "history-table";
table.innerHTML = `<thead><tr><th>File</th><th>Folder</th></tr></thead><tbody></tbody>`;
const tbody = table.querySelector("tbody");
let hasValidSubfolders = false;
for (let f of files) {
const parts = f.webkitRelativePath.split('/');
const label = parts.length > 1 ? parts[parts.length - 2] : "";
if (!f.name.match(/\.(png|jpg|jpeg|webp)$/i)) continue;
// Normalize folder label for display consistency (Poin 2)
const labelUpper = label.toUpperCase();
let normLabel = "";
if (labelUpper === "FAKE" || labelUpper === "AI") {
normLabel = "AI";
hasValidSubfolders = true;
} else if (labelUpper === "REAL") {
normLabel = "REAL";
hasValidSubfolders = true;
}
batchFiles.push(f);
batchLabels.push(normLabel);
const tr = document.createElement("tr");
const color = normLabel === "REAL" ? "var(--success)" :
(normLabel === "AI") ? "var(--danger)" : "rgba(255,255,255,0.4)";
tr.innerHTML = `<td>${f.name}</td><td style="color:${color};font-weight:bold">${normLabel || "-"}</td>`;
tbody.appendChild(tr);
}
if (currentUser !== "Sandikad" && !hasValidSubfolders) {
alert("Akses Ditolak: Hanya pengguna 'Sandikad' yang diizinkan melakukan Batch Scan tanpa struktur subfolder (real/fake).\nSilakan unggah folder yang berisi subfolder 'real' dan 'fake/ai'.");
batchFiles = [];
batchLabels = [];
document.getElementById("batch-folder-name").innerText = "Belum ada folder dipilih";
listDiv.innerHTML = "";
return;
}
listDiv.appendChild(table);
listDiv.innerHTML += `<p style="margin-top:10px;color:var(--yellow-main)">Total: <b>${batchFiles.length}</b> gambar</p>`;
});
async function startBatchScan() {
if (!batchFiles.length) return alert("Pilih folder terlebih dahulu!");
if (!currentUser) return alert("Login dulu!");
let hasValidSubfolders = batchLabels.some(l => l === "REAL" || l === "AI");
if (currentUser !== "Sandikad" && !hasValidSubfolders) {
alert("Akses Ditolak: Hanya pengguna 'Sandikad' yang diizinkan melakukan Batch Scan tanpa struktur subfolder (real/fake).");
return;
}
const progress = document.getElementById("batch-progress");
const resultDiv = document.getElementById("batch-result");
const confirmArea = document.getElementById("batch-confirm-area");
progress.classList.remove("hidden");
resultDiv.classList.add("hidden");
if (confirmArea) confirmArea.classList.add("hidden");
progress.innerHTML = "⏳ Mengirim file ke server...";
const formData = new FormData();
for (let f of batchFiles) {
formData.append("files", f);
}
formData.append("username", currentUser);
// Convert to robust filename -> label map to prevent any index alignment shifts
const labelMap = {};
for (let i = 0; i < batchFiles.length; i++) {
labelMap[batchFiles[i].name] = batchLabels[i];
}
formData.append("labels", JSON.stringify(labelMap));
try {
const res = await fetch(`${API_URL}/api/batch-scan`, { method: "POST", body: formData });
const data = await res.json();
progress.classList.add("hidden");
if (!res.ok) {
resultDiv.innerHTML = `<h3 style="color:var(--danger)">Error: ${data.detail}</h3>`;
resultDiv.classList.remove("hidden");
return;
}
displayBatchResults(data, resultDiv, confirmArea);
updateGlobalStats();
} catch (err) {
progress.innerHTML = "<h3 style='color:var(--danger)'>Gagal terhubung ke server.</h3>";
}
}
function displayBatchResults(data, resultDiv, confirmArea) {
const isUnlabeled = data.accuracy === -1;
const color = isUnlabeled ? "var(--yellow-main)" : (data.accuracy >= 70 ? "var(--success)" : data.accuracy >= 40 ? "var(--blue-dark)" : "var(--danger)");
let html = `
<div class="result-box" style="border-color:${color}">
<div class="result-title" style="color:${color}; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
<div>
<span>${isUnlabeled ? "Hasil Batch Scan" : "Hasil Batch Test"}</span>
<span style="font-size: 12px; background: rgba(255, 215, 0, 0.15); color: var(--yellow-main); padding: 4px 8px; border-radius: 6px; margin-left: 10px; font-weight: normal; border: 1px solid rgba(255, 215, 0, 0.3)">Using Threshold: 0.50</span>
</div>
<span>Akurasi: ${isUnlabeled ? "-" : data.accuracy + "%"}</span>
</div>
<div class="details-grid">
<div class="detail-item">
<div class="detail-label">Total Gambar</div>
<div class="detail-value">${data.total}</div>
</div>
<div class="detail-item">
<div class="detail-label">Benar</div>
<div class="detail-value" style="color:var(--success)">${isUnlabeled ? "-" : data.correct}</div>
</div>
<div class="detail-item">
<div class="detail-label">Salah</div>
<div class="detail-value" style="color:var(--danger)">${isUnlabeled ? "-" : data.wrong}</div>
</div>
<div class="detail-item">
<div class="detail-label">Akurasi</div>
<div class="detail-value" style="color:${color}">${isUnlabeled ? "-" : data.accuracy + "%"}</div>
</div>
</div>
</div>
<div class="table-wrap" style="margin-top:15px">
<table class="history-table">
<thead><tr><th>File</th><th>Folder</th><th>Prediksi</th><th>Confidence</th><th>Status</th></tr></thead>
<tbody>`;
data.results.forEach((r, i) => {
if (r.error) {
html += `<tr><td>${r.filename}</td><td colspan="4" style="color:var(--danger)">Error: ${r.error}</td></tr>`;
return;
}
const statusColor = r.folder_label ? (r.is_mismatch ? "var(--danger)" : "var(--success)") : "rgba(255, 255, 255, 0.4)";
const statusText = r.folder_label ? (r.is_mismatch ? "❌ SALAH" : "βœ… BENAR") : "-";
// Capitalize folder label consistently (Poin 2)
const folderUpper = r.folder_label ? r.folder_label.toUpperCase() : "-";
const folderColor = folderUpper === "REAL" ? "var(--success)" :
(folderUpper === "AI" || folderUpper === "FAKE") ? "var(--danger)" : "rgba(255, 255, 255, 0.4)";
const predColor = r.prediction === "REAL" ? "var(--success)" : "var(--danger)";
// Highlight incorrect rows (Poin 3)
const rowBg = r.is_mismatch ? "background: rgba(255, 71, 87, 0.08);" : "";
html += `<tr style="${rowBg}">
<td>${r.filename}</td>
<td style="color:${folderColor};font-weight:bold">${folderUpper}</td>
<td style="color:${predColor};font-weight:bold">${r.prediction}</td>
<td style="color:var(--yellow-main)">${r.confidence}%</td>
<td style="color:${statusColor};font-weight:bold">${statusText}</td>
</tr>`;
});
html += `</tbody></table></div>`;
html += `<div style="margin-top:15px;text-align:center"><button class="btn-scan" onclick="showSection('accuracy');event.target.closest('#batch-result .btn-scan').remove()" style="padding:10px 30px;font-size:14px">πŸ“Š Lihat Akurasi β†’</button></div>`;
resultDiv.innerHTML = html;
resultDiv.classList.remove("hidden");
}
// --- 7. ACCURACY REPORT ---
async function loadAccuracyReport() {
if (!currentUser) return;
const summaryDiv = document.getElementById("accuracy-summary");
const timeFilter = document.getElementById("accuracy-time-filter")?.value || "all";
try {
const res = await fetch(`${API_URL}/api/accuracy-report?filter=${timeFilter}`);
const report = await res.json();
const s = report.stats;
const matrix = report.confusion_matrix;
const dist = report.confidence_distribution;
const failures = report.failures;
const textColor = s.accuracy >= 70 ? "var(--success)" : s.accuracy >= 40 ? "var(--yellow-main)" : "var(--danger)";
// 1. Render Summary stats & Advanced metrics
summaryDiv.innerHTML = `
<!-- Primary Stats -->
<div class="stats-grid">
<div class="stat-card blue"><h3>${s.total}</h3><p>Total Test</p></div>
<div class="stat-card yellow"><h3>${s.correct}</h3><p>Benar</p></div>
<div class="stat-card blue"><h3>${s.wrong}</h3><p>Salah</p></div>
<div class="stat-card yellow"><h3 style="color:${textColor}">${s.accuracy}%</h3><p>Akurasi</p></div>
</div>
<!-- Advanced ML Metrics -->
<div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); margin-top: 15px;">
<div class="stat-card" style="background: #002244 !important; border: 1px solid rgba(255, 215, 0, 0.35) !important; border-left: 4px solid var(--yellow-main) !important; padding: 15px; border-radius: 12px; text-align: center; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.25) !important;">
<h3 style="font-size: 24px; color: #FFD700 !important; font-weight: 800; margin: 0; opacity: 1 !important;">${s.precision}%</h3>
<p style="font-size: 11px; margin: 6px 0 0 0; color: #ffffff !important; opacity: 1 !important; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px;">Precision (Presisi)</p>
</div>
<div class="stat-card" style="background: #002244 !important; border: 1px solid rgba(255, 215, 0, 0.35) !important; border-left: 4px solid var(--yellow-main) !important; padding: 15px; border-radius: 12px; text-align: center; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.25) !important;">
<h3 style="font-size: 24px; color: #FFD700 !important; font-weight: 800; margin: 0; opacity: 1 !important;">${s.recall}%</h3>
<p style="font-size: 11px; margin: 6px 0 0 0; color: #ffffff !important; opacity: 1 !important; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px;">Recall (Sensitivitas)</p>
</div>
<div class="stat-card" style="background: #002244 !important; border: 1px solid rgba(255, 215, 0, 0.35) !important; border-left: 4px solid var(--yellow-main) !important; padding: 15px; border-radius: 12px; text-align: center; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.25) !important;">
<h3 style="font-size: 24px; color: #FFD700 !important; font-weight: 800; margin: 0; opacity: 1 !important;">${s.f1_score}%</h3>
<p style="font-size: 11px; margin: 6px 0 0 0; color: #ffffff !important; opacity: 1 !important; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px;">F1-Score (Harmonis)</p>
</div>
</div>
<!-- Info Box -->
<div class="info-box" style="margin-top: 15px;">
<p>Batch test: <b style="color:var(--yellow-main)">${report.batch_images}</b> gambar</p>
<p>Scan individu: <b style="color:var(--yellow-main)">${report.scan_count}</b> gambar</p>
<p>Total data pembelajaran: <b style="color:var(--yellow-main)">${report.learning_data_count}</b> gambar (siap training)</p>
</div>
`;
// 2. Render Confusion Matrix Values
document.getElementById("cm-tp").innerHTML = `${matrix.tp}<br><span style="font-size: 9px; opacity: 0.8; font-weight: normal; margin-top: 3px;">TP (True AI)</span>`;
document.getElementById("cm-fp").innerHTML = `${matrix.fp}<br><span style="font-size: 9px; opacity: 0.8; font-weight: normal; margin-top: 3px;">FP (False AI)</span>`;
document.getElementById("cm-fn").innerHTML = `${matrix.fn}<br><span style="font-size: 9px; opacity: 0.8; font-weight: normal; margin-top: 3px;">FN (False Real)</span>`;
document.getElementById("cm-tn").innerHTML = `${matrix.tn}<br><span style="font-size: 9px; opacity: 0.8; font-weight: normal; margin-top: 3px;">TN (True Real)</span>`;
// 3. Render Failure Log Table
const failureBody = document.getElementById("failure-log-body");
failureBody.innerHTML = "";
if (failures.length === 0) {
failureBody.innerHTML = "<tr><td colspan='5' style='text-align:center;padding:20px;color:rgba(255,255,255,0.3)'>Tidak ada kesalahan tebak terdeteksi dalam data pengetesan ini! πŸŽ‰</td></tr>";
} else {
failures.forEach(f => {
const expColor = f.expected === 'AI' ? 'var(--danger)' : 'var(--success)';
const predColor = f.prediction === 'AI' ? 'var(--danger)' : 'var(--success)';
failureBody.innerHTML += `
<tr>
<td>${f.filename}</td>
<td><span style="color: ${expColor}; font-weight:bold">${f.expected}</span></td>
<td><span style="color: ${predColor}">${f.prediction}</span></td>
<td style="color: var(--yellow-main); font-weight:bold">${f.confidence}%</td>
<td style="font-size: 11px; opacity: 0.7;">${f.date}</td>
</tr>
`;
});
}
// 4. Render Charts
drawDonutChart(s.correct, s.wrong);
drawBarChart(report.batches);
drawConfidenceChart(dist);
} catch (err) {
console.error("Gagal memuat accuracy report:", err);
}
}
let chartDonut = null, chartBar = null, chartConfidence = null;
function drawDonutChart(correct, wrong) {
const ctx = document.getElementById("chart-donut").getContext("2d");
if (chartDonut) chartDonut.destroy();
if (correct + wrong === 0) return;
chartDonut = new Chart(ctx, {
type: "doughnut",
data: {
labels: ["Benar", "Salah"],
datasets: [{
data: [correct, wrong],
backgroundColor: ["#2ed573", "#ff4757"],
borderWidth: 0
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: {
title: { display: true, text: "Perbandingan Benar vs Salah", color: "#FFD700" },
legend: { labels: { color: "#fff" } }
}
}
});
}
function drawBarChart(batches) {
const ctx = document.getElementById("chart-bar").getContext("2d");
if (chartBar) chartBar.destroy();
if (!batches.length) return;
chartBar = new Chart(ctx, {
type: "bar",
data: {
labels: batches.slice(0, 10).map(b => "#" + b.id), // limit to latest 10 batches
datasets: [
{ label: "Benar", data: batches.slice(0, 10).map(b => b.correct_count), backgroundColor: "#2ed573" },
{ label: "Salah", data: batches.slice(0, 10).map(b => b.wrong_count), backgroundColor: "#ff4757" }
]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { ticks: { color: "#fff" }, stacked: true },
y: { ticks: { color: "#fff" }, stacked: true }
},
plugins: {
title: { display: true, text: "Akurasi per Batch Test (10 Terakhir)", color: "#FFD700" },
legend: { labels: { color: "#fff" } }
}
}
});
}
function drawConfidenceChart(dist) {
const ctx = document.getElementById("chart-confidence").getContext("2d");
if (chartConfidence) chartConfidence.destroy();
chartConfidence = new Chart(ctx, {
type: "line",
data: {
labels: dist.buckets,
datasets: [
{
label: "REAL Predictions",
data: dist.real,
borderColor: "#2ed573",
backgroundColor: "rgba(46, 213, 115, 0.1)",
fill: true,
tension: 0.4
},
{
label: "AI Predictions",
data: dist.ai,
borderColor: "#ff4757",
backgroundColor: "rgba(255, 71, 87, 0.1)",
fill: true,
tension: 0.4
}
]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { ticks: { color: "#fff" }, grid: { color: "rgba(255,255,255,0.05)" } },
y: { ticks: { color: "#fff" }, grid: { color: "rgba(255,255,255,0.05)" }, beginAtZero: true }
},
plugins: {
title: { display: true, text: "Sebaran Skor Keyakinan (Confidence)", color: "#FFD700" },
legend: { labels: { color: "#fff" } }
}
}
});
}
async function confirmClearHistory() {
if (!confirm("⚠️ PERINGATAN: Apakah Anda yakin ingin menghapus seluruh riwayat scan dan merestart semua statistik pengujian kembali ke angka nol? Tindakan ini tidak dapat dibatalkan!")) {
return;
}
try {
const res = await fetch(`${API_URL}/api/clear-history`);
const data = await res.json();
if (res.ok) {
alert("Statistik berhasil direset ke nol!");
loadAccuracyReport();
loadHistory();
updateGlobalStats();
}
} catch (e) {
alert("Gagal mereset statistik.");
}
}
function downloadFeedback() {
if (currentUser !== "Sandikad") {
alert("Hanya user Sandikad yang dapat mendownload feedback.");
return;
}
window.open(`${API_URL}/api/download-feedback?username=${currentUser}`, "_blank");
}
async function loadHistory() {
if (!currentUser) return;
const tbody = document.getElementById("history-body");
const count = document.getElementById("history-count");
tbody.innerHTML = "<tr><td colspan='7' style='text-align:center;padding:25px;color:rgba(255,255,255,0.3)'>Loading...</td></tr>";
try {
const res = await fetch(`${API_URL}/api/history/${currentUser}`);
const data = await res.json();
tbody.innerHTML = "";
count.innerText = `${data.history.length} item`;
data.history.forEach(item => {
if (item._type === "batch") {
const batchColor = item.accuracy >= 70 ? "var(--success)" : item.accuracy >= 40 ? "var(--yellow-main)" : "var(--danger)";
tbody.innerHTML += `
<tr style="background:rgba(255,215,0,0.05)">
<td style="color:var(--yellow-main)">${item.filename}</td>
<td>${item.file_type}</td>
<td>${item.file_size}</td>
<td style="font-size:12px">${item.source}</td>
<td style="color: ${batchColor}; font-weight:bold">${item.accuracy}%</td>
<td style="color: ${batchColor}; font-weight:bold">${item.accuracy >= 70 ? 'βœ… Baik' : item.accuracy >= 40 ? '⚠️ Sedang' : '❌ Buruk'}</td>
<td>${item.scan_date}</td>
</tr>`;
} else {
const statusColor = item.is_ai ? "var(--danger)" : "var(--success)";
const statusText = item.is_ai ? "Palsu (AI)" : "Asli (Real)";
tbody.innerHTML += `
<tr>
<td>${item.filename}</td>
<td>${item.file_type}</td>
<td>${item.file_size}</td>
<td style="font-size:12px">${item.source}</td>
<td style="color: var(--yellow-main); font-weight:bold">${item.accuracy}%</td>
<td style="color: ${statusColor}; font-weight:bold">${statusText}</td>
<td>${item.scan_date}</td>
</tr>
`;
}
});
if (data.history.length === 0) tbody.innerHTML = "<tr><td colspan='7' style='text-align:center;padding:30px;color:rgba(255,255,255,0.3)'>Belum ada history. Scan gambar atau jalankan batch test.</td></tr>";
} catch (err) {
tbody.innerHTML = "<tr><td colspan='7' style='text-align:center;padding:30px;color:var(--danger)'>Gagal memuat data.</td></tr>";
}
}
function startGuestMode(e) {
if (e) e.preventDefault();
currentUser = "__guest__";
document.getElementById("user-name").innerText = "Guest";
// Sembunyikan skor kredibilitas & tampilkan sisa scan gratis
document.getElementById("user-trust-container").style.display = "none";
const limitContainer = document.getElementById("guest-scan-container");
if (limitContainer) {
limitContainer.style.display = "flex";
updateGuestScanCountUI();
}
// Batasi akses sidebar hanya ke Dashboard dan Scan Gambar
document.getElementById("menu-batch-test").style.display = "none";
document.getElementById("menu-history").style.display = "none";
document.getElementById("menu-accuracy").style.display = "none";
// Transisi ke aplikasi utama
document.getElementById("auth-page").classList.add("hidden");
document.getElementById("main-app").classList.remove("hidden");
document.getElementById("app-style").href = "style.css";
// Tampilkan bagian dashboard secara default
showSection("dashboard");
loadDashboard();
}
function updateGuestScanCountUI() {
let count = parseInt(localStorage.getItem("guest_scan_count") || "0");
const remaining = Math.max(0, 5 - count);
const remEl = document.getElementById("guest-scan-remaining");
if (remEl) {
remEl.innerText = remaining;
}
const landingQuotaEl = document.getElementById("landing-quota-left");
if (landingQuotaEl) {
landingQuotaEl.innerText = remaining;
}
}
// --- AUTH MODAL LOGIC ---
function openAuthModal() {
document.getElementById("auth-modal").classList.remove("hidden");
}
function closeAuthModal() {
document.getElementById("auth-modal").classList.add("hidden");
}
function closeAuthModalOnOverlay(e) {
if (e.target === document.getElementById("auth-modal")) {
closeAuthModal();
}
}
// --- LANDING PAGE FILE SCANNING LOGIC ---
document.getElementById("landing-input-image").addEventListener("change", (e) => {
document.getElementById("landing-img-name").innerText = e.target.files[0]?.name || "Tidak ada file dipilih";
});
async function scanLandingFile() {
const fileInput = document.getElementById("landing-input-image");
const resultBox = document.getElementById("landing-result-image");
let count = parseInt(localStorage.getItem("guest_scan_count") || "0");
if (count >= 5) {
alert("Batas scan gratis Anda (5 kali) sudah habis!\nSilakan Login atau Register terlebih dahulu untuk scan tanpa batas.");
openAuthModal();
return;
}
if (!fileInput.files[0]) return alert("Pilih file terlebih dahulu!");
const file = fileInput.files[0];
if (file.size > 10 * 1024 * 1024) return alert("Ukuran file melebihi batas 10MB!");
const formData = new FormData();
formData.append("file", file);
formData.append("username", "__guest__");
const fetchPromise = fetch(`${API_URL}/api/scan-image`, { method: "POST", body: formData });
await simulateForensicAnalysis(resultBox, fetchPromise);
try {
const res = await fetchPromise;
const data = await res.json();
// CEK OTOMATIS GAMBAR HITAM PUTIH UNTUK TAMPILKAN BANNER ALERT
const warningBanner = document.getElementById("landing-warning-banner-monokrom");
if (warningBanner) {
if (data.is_monochrome_detected === true) {
warningBanner.style.display = "block";
} else {
warningBanner.style.display = "none";
}
}
if (res.ok) {
let count = parseInt(localStorage.getItem("guest_scan_count") || "0");
count++;
localStorage.setItem("guest_scan_count", count.toString());
updateGuestScanCountUI();
const statusClass = data.is_ai ? "ai" : "real";
const statusText = data.is_ai ? "⚠️ GAMBAR PALSU (AI)" : "βœ… GAMBAR ASLI (REAL)";
const statusColor = data.is_ai ? "var(--danger)" : "var(--success)";
let logsHtml = '';
if (data.forensic_analysis_logs && Object.keys(data.forensic_analysis_logs).length > 0) {
logsHtml = `
<div class="forensic-logs-detail" style="margin-top: 15px; margin-bottom: 15px; text-align: left;">
<button class="btn-refresh" style="width: 100%; text-align: left; padding: 10px 15px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,215,0,0.25); border-radius: 8px; color: var(--yellow-main); cursor: pointer; font-size: 13px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 0;" onclick="toggleForensicLogs(this)">
<span>πŸ” Lihat Log Detil Forensik (12 Langkah)</span>
<span style="font-size:10px">β–Ό</span>
</button>
<div class="logs-content hidden" style="background: rgba(0,0,0,0.5); border: 1px solid rgba(255,215,0,0.15); border-top: none; border-radius: 0 0 8px 8px; padding: 15px; font-family: monospace; font-size: 12px; color: #2ed573; line-height: 1.6; max-height: 250px; overflow-y: auto;">
`;
for (let i = 1; i <= 12; i++) {
const stepText = data.forensic_analysis_logs[`step_${i}`] || `[Step ${i}/12] Analisis selesai.`;
logsHtml += `<div style="margin-bottom: 6px; border-bottom: 1px dashed rgba(255,255,255,0.05); padding-bottom: 4px;">${stepText}</div>`;
}
logsHtml += `
</div>
</div>
`;
}
resultBox.className = `result-box ${statusClass}`;
resultBox.innerHTML = `
<div class="result-title" style="color: ${statusColor}">
<span>${statusText}</span>
<span>Akurasi: ${data.accuracy}%</span>
</div>
<div class="badge-bar" style="display:flex;gap:8px;margin-bottom:15px;flex-wrap:wrap">
<span class="badge" style="background:rgba(255,215,0,0.1);color:var(--yellow-main);border:1px solid rgba(255,215,0,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">
🧬 Cosine Similarity: ${(data.similarity * 100).toFixed(1)}%
</span>
${data.is_outlier
? `<span class="badge" style="background:rgba(255,71,87,0.1);color:var(--danger);border:1px solid rgba(255,71,87,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">πŸ’‘ Data Asing (Outlier)</span>`
: `<span class="badge" style="background:rgba(46,204,113,0.1);color:var(--success);border:1px solid rgba(46,204,113,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">🎯 Klasifikasi Aman</span>`
}
${data.is_trap
? `<span class="badge" style="background:rgba(230,126,34,0.15);color:#e67e22;border:1px solid rgba(230,126,34,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">⚠️ Trap Image Mode</span>`
: ''
}
${data.is_dark
? `<span class="badge" style="background:rgba(230,126,34,0.1);color:#e67e22;border:1px solid rgba(230,126,34,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">πŸŒ™ Low Light (Cahaya Rendah)</span>`
: ''
}
${data.is_grayscale
? `<span class="badge" style="background:rgba(149,165,166,0.15);color:#bdc3c7;border:1px solid rgba(149,165,166,0.25);padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700;display:inline-flex;align-items:center;gap:4px">βšͺ Monokrom (Hitam Putih)</span>`
: ''
}
</div>
${(data.is_dark || data.is_grayscale)
? `
<div style="background: rgba(230,126,34,0.1); border: 1px dashed rgba(230,126,34,0.3); border-radius: 8px; padding: 12px; margin-bottom: 15px; font-size: 12px; color: #f39c12; line-height: 1.5; text-align: left;">
<b>⚠️ Rekomendasi Kondisi Deteksi:</b><br/>
${data.is_dark ? 'β€’ Cahaya terdeteksi rendah (kecerahan rata-rata: ' + data.avg_brightness + '/255). Hal ini memicu noise sensor kamera yang dapat mengganggu keakuratan forensik AI.<br/>' : ''}
${data.is_grayscale ? 'β€’ Gambar monokrom/hitam-putih terdeteksi. Kehilangan informasi kromatik (saluran warna RGB) secara drastis dapat menurunkan performa klasifikasi model AI.<br/>' : ''}
<i style="display:block;margin-top:6px;color:rgba(255,255,255,0.7)">Disarankan untuk melakukan scan ulang menggunakan foto dengan pencahayaan cukup dan penuh warna (Full RGB).</i>
</div>
`
: ''
}
${logsHtml}
<div class="details-grid">
<div class="detail-item">
<div class="detail-label">Nama File</div>
<div class="detail-value">${data.filename}</div>
</div>
<div class="detail-item">
<div class="detail-label">Tipe</div>
<div class="detail-value">${data.type.toUpperCase()}</div>
</div>
<div class="detail-item">
<div class="detail-label">Ukuran File</div>
<div class="detail-value">${data.file_size}</div>
</div>
<div class="detail-item">
<div class="detail-label">Sumber Deteksi</div>
<div class="detail-value" style="font-size:12px">${data.source}</div>
</div>
<div class="detail-item">
<div class="detail-label">Tanggal Scan</div>
<div class="detail-value">${data.date}</div>
</div>
<div class="detail-item">
<div class="detail-label">Keakuratan</div>
<div class="detail-value" style="color: ${statusColor}">${data.accuracy}%</div>
</div>
</div>
<div style="margin-top:15px;padding-top:15px;border-top:1px solid rgba(255,255,255,0.2)">
<p style="margin-bottom:10px;color:var(--yellow-main)">Apakah hasil ini benar?</p>
<button class="btn-scan" style="padding:8px 25px;font-size:14px;margin-right:10px" onclick="confirmSingleResult('${data.filename}', '${data.is_ai ? "AI" : "REAL"}', '${data.accuracy}', '${data.feedback_id || ""}', this)">βœ… Benar</button>
<button class="btn-scan" style="padding:8px 25px;font-size:14px" onclick="correctSingleResult('${data.filename}', '${data.is_ai ? "AI" : "REAL"}', '${data.accuracy}', '${data.feedback_id || ""}', this)">❌ Salah</button>
<div id="single-correction-area" style="margin-top:10px"></div>
</div>
`;
updateGlobalStats();
} else {
resultBox.innerHTML = `<h3 style="color: var(--danger)">Error: ${data.detail}</h3>`;
}
} catch (err) {
resultBox.innerHTML = "<h3 style='color: var(--danger)'>Gagal terhubung ke server Backend.</h3>";
}
}