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 = `
`;
document.getElementById("dashboard-learning").innerHTML = `
Data pembelajaran: ${data.learning_data_count} gambar siap training
Detail → `;
} 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 = `
⏳ Melakukan Analisis Forensik Citra Massal (${files.length} Gambar)...
`;
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: ${data.is_ai ? "AI" : "REAL"} (${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 += `
${r.file.name}
Error: ${r.error}
`;
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 += `⚪ Monokrom `;
}
if (data.is_dark) {
badgesHtml += `🌙 Low Light `;
}
if (data.is_outlier) {
badgesHtml += `💡 Outlier `;
}
if (data.is_trap) {
badgesHtml += `🛡️ Trap `;
}
// 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 += `${stepText}
`;
}
}
tbodyHtml += `
${data.filename}
${predText}
${data.accuracy}%
${badgesHtml || "-"}
👁️ Lihat Log
${stepsHtml}
✅ Benar
❌ Salah
`;
});
resultBox.innerHTML = `
📊 Hasil Analisis Citra Massal (${files.length} Gambar)
Gambar
Prediksi
Keakuratan
Info Deteksi
Log Forensik (12 Langkah)
Tindakan / Feedback
${tbodyHtml}
`;
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 = `
🔍 Lihat Log Detil Forensik (12 Langkah)
▼
`;
for (let i = 1; i <= 12; i++) {
const stepText = data.forensic_analysis_logs[`step_${i}`] || `[Step ${i}/12] Analisis selesai.`;
logsHtml += `
${stepText}
`;
}
logsHtml += `
`;
}
resultBox.className = `result-box ${statusClass}`;
resultBox.innerHTML = `
${statusText}
Akurasi: ${data.accuracy}%
🧬 Cosine Similarity: ${(data.similarity * 100).toFixed(1)}%
${data.is_outlier
? `💡 Data Asing (Outlier) `
: `🎯 Klasifikasi Aman `
}
${data.is_trap
? `⚠️ Trap Image Mode `
: ''
}
${data.is_dark
? `🌙 Low Light (Cahaya Rendah) `
: ''
}
${data.is_grayscale
? `⚪ Monokrom (Hitam Putih) `
: ''
}
${(data.is_dark || data.is_grayscale)
? `
⚠️ Rekomendasi Kondisi Deteksi:
${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. ' : ''}
${data.is_grayscale ? '• Gambar monokrom/hitam-putih terdeteksi. Kehilangan informasi kromatik (saluran warna RGB) secara drastis dapat menurunkan performa klasifikasi model AI. ' : ''}
Disarankan untuk melakukan scan ulang menggunakan foto dengan pencahayaan cukup and penuh warna (Full RGB).
`
: ''
}
${logsHtml}
Nama File
${data.filename}
Tipe
${data.type.toUpperCase()}
Ukuran File
${data.file_size}
Sumber Deteksi
${data.source}
Tanggal Scan
${data.date}
Keakuratan
${data.accuracy}%
Apakah hasil ini benar?
✅ Benar
❌ Salah
`;
if (currentUser !== "__guest__") {
updateUserTrustScoreUI(data.trust_score || 50);
}
} else {
resultBox.innerHTML = `Error: ${data.detail} `;
}
} catch (err) {
resultBox.innerHTML = "Gagal terhubung ke server Backend. ";
}
}
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 = "✅ Konfirmasi tersimpan!
";
}
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 = `✅ Koreksi tersimpan! (seharusnya ${correctLabel})
`;
}
// --- 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 = "File ditemukan: ";
const table = document.createElement("table");
table.className = "history-table";
table.innerHTML = `File Folder `;
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 = `${f.name} ${normLabel || "-"} `;
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 += `Total: ${batchFiles.length} gambar
`;
});
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 = `Error: ${data.detail} `;
resultDiv.classList.remove("hidden");
return;
}
displayBatchResults(data, resultDiv, confirmArea);
updateGlobalStats();
} catch (err) {
progress.innerHTML = "Gagal terhubung ke server. ";
}
}
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 = `
${isUnlabeled ? "Hasil Batch Scan" : "Hasil Batch Test"}
Using Threshold: 0.50
Akurasi: ${isUnlabeled ? "-" : data.accuracy + "%"}
Total Gambar
${data.total}
Benar
${isUnlabeled ? "-" : data.correct}
Salah
${isUnlabeled ? "-" : data.wrong}
Akurasi
${isUnlabeled ? "-" : data.accuracy + "%"}
File Folder Prediksi Confidence Status
`;
data.results.forEach((r, i) => {
if (r.error) {
html += `${r.filename} Error: ${r.error} `;
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 += `
${r.filename}
${folderUpper}
${r.prediction}
${r.confidence}%
${statusText}
`;
});
html += `
`;
html += `📊 Lihat Akurasi →
`;
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 = `
${s.precision}%
Precision (Presisi)
${s.recall}%
Recall (Sensitivitas)
${s.f1_score}%
F1-Score (Harmonis)
Batch test: ${report.batch_images} gambar
Scan individu: ${report.scan_count} gambar
Total data pembelajaran: ${report.learning_data_count} gambar (siap training)
`;
// 2. Render Confusion Matrix Values
document.getElementById("cm-tp").innerHTML = `${matrix.tp}TP (True AI) `;
document.getElementById("cm-fp").innerHTML = `${matrix.fp}FP (False AI) `;
document.getElementById("cm-fn").innerHTML = `${matrix.fn}FN (False Real) `;
document.getElementById("cm-tn").innerHTML = `${matrix.tn}TN (True Real) `;
// 3. Render Failure Log Table
const failureBody = document.getElementById("failure-log-body");
failureBody.innerHTML = "";
if (failures.length === 0) {
failureBody.innerHTML = "Tidak ada kesalahan tebak terdeteksi dalam data pengetesan ini! 🎉 ";
} else {
failures.forEach(f => {
const expColor = f.expected === 'AI' ? 'var(--danger)' : 'var(--success)';
const predColor = f.prediction === 'AI' ? 'var(--danger)' : 'var(--success)';
failureBody.innerHTML += `
${f.filename}
${f.expected}
${f.prediction}
${f.confidence}%
${f.date}
`;
});
}
// 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 = "Loading... ";
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 += `
${item.filename}
${item.file_type}
${item.file_size}
${item.source}
${item.accuracy}%
${item.accuracy >= 70 ? '✅ Baik' : item.accuracy >= 40 ? '⚠️ Sedang' : '❌ Buruk'}
${item.scan_date}
`;
} else {
const statusColor = item.is_ai ? "var(--danger)" : "var(--success)";
const statusText = item.is_ai ? "Palsu (AI)" : "Asli (Real)";
tbody.innerHTML += `
${item.filename}
${item.file_type}
${item.file_size}
${item.source}
${item.accuracy}%
${statusText}
${item.scan_date}
`;
}
});
if (data.history.length === 0) tbody.innerHTML = "Belum ada history. Scan gambar atau jalankan batch test. ";
} catch (err) {
tbody.innerHTML = "Gagal memuat data. ";
}
}
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 = `
🔍 Lihat Log Detil Forensik (12 Langkah)
▼
`;
for (let i = 1; i <= 12; i++) {
const stepText = data.forensic_analysis_logs[`step_${i}`] || `[Step ${i}/12] Analisis selesai.`;
logsHtml += `
${stepText}
`;
}
logsHtml += `
`;
}
resultBox.className = `result-box ${statusClass}`;
resultBox.innerHTML = `
${statusText}
Akurasi: ${data.accuracy}%
🧬 Cosine Similarity: ${(data.similarity * 100).toFixed(1)}%
${data.is_outlier
? `💡 Data Asing (Outlier) `
: `🎯 Klasifikasi Aman `
}
${data.is_trap
? `⚠️ Trap Image Mode `
: ''
}
${data.is_dark
? `🌙 Low Light (Cahaya Rendah) `
: ''
}
${data.is_grayscale
? `⚪ Monokrom (Hitam Putih) `
: ''
}
${(data.is_dark || data.is_grayscale)
? `
⚠️ Rekomendasi Kondisi Deteksi:
${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. ' : ''}
${data.is_grayscale ? '• Gambar monokrom/hitam-putih terdeteksi. Kehilangan informasi kromatik (saluran warna RGB) secara drastis dapat menurunkan performa klasifikasi model AI. ' : ''}
Disarankan untuk melakukan scan ulang menggunakan foto dengan pencahayaan cukup dan penuh warna (Full RGB).
`
: ''
}
${logsHtml}
Nama File
${data.filename}
Tipe
${data.type.toUpperCase()}
Ukuran File
${data.file_size}
Sumber Deteksi
${data.source}
Tanggal Scan
${data.date}
Keakuratan
${data.accuracy}%
Apakah hasil ini benar?
✅ Benar
❌ Salah
`;
updateGlobalStats();
} else {
resultBox.innerHTML = `Error: ${data.detail} `;
}
} catch (err) {
resultBox.innerHTML = "Gagal terhubung ke server Backend. ";
}
}