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 = `

${s.total}

Total Test

${s.correct}

Benar

${s.wrong}

Salah

${s.accuracy}%

Akurasi

`; document.getElementById("dashboard-learning").innerHTML = ` Data pembelajaran: ${data.learning_data_count} gambar siap training   `; } 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 || "-"}
`; }); resultBox.innerHTML = `

📊 Hasil Analisis Citra Massal (${files.length} Gambar)

${tbodyHtml}
Gambar Prediksi Keakuratan Info Deteksi Log Forensik (12 Langkah) Tindakan / Feedback
`; 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 = `
`; } 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?

`; 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 = `FileFolder`; 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 + "%"}
`; data.results.forEach((r, i) => { if (r.error) { html += ``; 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 += ``; }); html += `
FileFolderPrediksiConfidenceStatus
${r.filename}Error: ${r.error}
${r.filename} ${folderUpper} ${r.prediction} ${r.confidence}% ${statusText}
`; html += `
`; 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.total}

Total Test

${s.correct}

Benar

${s.wrong}

Salah

${s.accuracy}%

Akurasi

${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 = `
`; } 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?

`; updateGlobalStats(); } else { resultBox.innerHTML = `

Error: ${data.detail}

`; } } catch (err) { resultBox.innerHTML = "

Gagal terhubung ke server Backend.

"; } }