ai-detector-backend / script.js
Alstears's picture
Upload 145 files
c5803d8 verified
Raw History Blame Contribute Delete
60.3 kB
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 downloadBtn = document.getElementById("btn-download-feedback");
if (downloadBtn) {
if ((currentUser || "").toLowerCase() === "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-history").style.display = "";
document.getElementById("menu-accuracy").style.display = "";
// Sembunyikan Batch Test untuk user selain Sandikad
if ((currentUser || "").toLowerCase() === "sandikad") {
document.getElementById("menu-batch-test").style.display = "";
const dbBatchBtn = document.getElementById("dashboard-btn-batch-test");
if (dbBatchBtn) dbBatchBtn.style.display = "";
} else {
document.getElementById("menu-batch-test").style.display = "none";
const dbBatchBtn = document.getElementById("dashboard-btn-batch-test");
if (dbBatchBtn) dbBatchBtn.style.display = "none";
}
document.getElementById("auth-page").classList.add("hidden");
document.getElementById("main-app").classList.remove("hidden");
document.getElementById("app-style").href = "style.css?v=1.0.9";
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 downloadBtn = document.getElementById("btn-download-feedback");
if (downloadBtn) {
downloadBtn.style.display = "none";
}
// Reset file uploads and previews
const inputImg = document.getElementById("input-image");
if (inputImg) inputImg.value = "";
const landingInputImg = document.getElementById("landing-input-image");
if (landingInputImg) landingInputImg.value = "";
const placeholder = document.getElementById("upload-placeholder");
if (placeholder) placeholder.style.display = "flex";
const previewContainer = document.getElementById("upload-preview");
if (previewContainer) previewContainer.classList.add("hidden");
const previewImg = document.getElementById("preview-img");
if (previewImg) previewImg.src = "";
const landingPlaceholder = document.getElementById("landing-upload-placeholder");
if (landingPlaceholder) landingPlaceholder.style.display = "flex";
const landingPreviewContainer = document.getElementById("landing-upload-preview");
if (landingPreviewContainer) landingPreviewContainer.classList.add("hidden");
const landingPreviewImg = document.getElementById("landing-preview-img");
if (landingPreviewImg) landingPreviewImg.src = "";
// 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 = "";
document.getElementById("menu-history").style.display = "";
document.getElementById("menu-accuracy").style.display = "";
const dbBatchBtn = document.getElementById("dashboard-btn-batch-test");
if (dbBatchBtn) dbBatchBtn.style.display = "";
document.getElementById("main-app").classList.add("hidden");
document.getElementById("auth-page").classList.remove("hidden");
document.getElementById("app-style").href = "template/style.css?v=1.0.9";
}
// --- 4. NAVIGATION LOGIC ---
function showSection(sectionId) {
if (sectionId === "batch-test" && (currentUser || "").toLowerCase() !== "sandikad") {
alert("Akses Ditolak: Menu Batch Test hanya dapat diakses oleh user 'sandikad'.");
showSection("dashboard");
return;
}
document.querySelectorAll(".section").forEach(s => s.classList.remove("active"));
document.getElementById(`sec-${sectionId}`).classList.add("active");
// Highlight original sidebar/bottom bar item
document.querySelectorAll(".nav-item").forEach(n => n.classList.remove("active"));
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 file = e.target.files[0];
const placeholder = document.getElementById("upload-placeholder");
const previewContainer = document.getElementById("upload-preview");
const previewImg = document.getElementById("preview-img");
if (file) {
const blobUrl = URL.createObjectURL(file);
previewImg.src = blobUrl;
placeholder.style.display = "none";
previewContainer.classList.remove("hidden");
} else {
previewImg.src = "";
placeholder.style.display = "flex";
previewContainer.classList.add("hidden");
}
});
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;
}
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[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 blobUrl = URL.createObjectURL(file);
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();
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)";
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>
`
: ''
}
<div class="result-display-flex" style="display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 20px; align-items: flex-start; justify-content: space-between;">
<div class="details-grid" style="flex: 1; min-width: 250px; margin: 0;">
<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 class="scanned-image-preview" style="width: 100%; max-width: 240px; height: auto; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.03); border: 2px solid ${statusColor}; border-radius: 12px; overflow: hidden; padding: 6px; box-shadow: 0 4px 15px rgba(0,0,0,0.3); align-self: stretch; min-height: 180px;">
<img src="${blobUrl}" style="max-width: 100%; max-height: 220px; object-fit: contain; border-radius: 8px;">
</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");
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();
const normLabel = (labelUpper === "FAKE" || labelUpper === "AI") ? "AI" : (labelUpper === "REAL" ? "REAL" : labelUpper);
batchFiles.push(f);
batchLabels.push(normLabel);
const tr = document.createElement("tr");
const color = normLabel === "REAL" ? "var(--success)" :
(normLabel === "AI" || normLabel === "FAKE") ? "var(--danger)" : "var(--yellow-main)";
tr.innerHTML = `<td>${f.name}</td><td style="color:${color};font-weight:bold">${normLabel || "-"}</td>`;
tbody.appendChild(tr);
}
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!");
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 color = 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>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: ${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)">${data.correct}</div>
</div>
<div class="detail-item">
<div class="detail-label">Salah</div>
<div class="detail-value" style="color:var(--danger)">${data.wrong}</div>
</div>
<div class="detail-item">
<div class="detail-label">Akurasi</div>
<div class="detail-value" style="color:${color}">${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.is_mismatch ? "var(--danger)" : "var(--success)";
const statusText = 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)" : "var(--yellow-main)";
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 || "").toLowerCase() !== "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";
const dbBatchBtn = document.getElementById("dashboard-btn-batch-test");
if (dbBatchBtn) dbBatchBtn.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?v=1.0.9";
// 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();
}
}
// --- QR MODAL LOGIC ---
function openQrModal() {
document.getElementById("qr-modal").classList.remove("hidden");
}
function closeQrModal() {
document.getElementById("qr-modal").classList.add("hidden");
}
function closeQrModalOnOverlay(e) {
if (e.target === document.getElementById("qr-modal")) {
closeQrModal();
}
}
// --- LANDING PAGE FILE SCANNING LOGIC ---
document.getElementById("landing-input-image").addEventListener("change", (e) => {
const file = e.target.files[0];
const placeholder = document.getElementById("landing-upload-placeholder");
const previewContainer = document.getElementById("landing-upload-preview");
const previewImg = document.getElementById("landing-preview-img");
if (file) {
const blobUrl = URL.createObjectURL(file);
previewImg.src = blobUrl;
placeholder.style.display = "none";
previewContainer.classList.remove("hidden");
} else {
previewImg.src = "";
placeholder.style.display = "flex";
previewContainer.classList.add("hidden");
}
});
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 blobUrl = URL.createObjectURL(file);
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();
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)";
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>
`
: ''
}
<div class="result-display-flex" style="display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 20px; align-items: flex-start; justify-content: space-between;">
<div class="details-grid" style="flex: 1; min-width: 250px; margin: 0;">
<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 class="scanned-image-preview" style="width: 100%; max-width: 240px; height: auto; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.03); border: 2px solid ${statusColor}; border-radius: 12px; overflow: hidden; padding: 6px; box-shadow: 0 4px 15px rgba(0,0,0,0.3); align-self: stretch; min-height: 180px;">
<img src="${blobUrl}" style="max-width: 100%; max-height: 220px; object-fit: contain; border-radius: 8px;">
</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>";
}
}