Alstears commited on
Commit
c5ecf13
·
verified ·
1 Parent(s): 3facd30

Upload 77 files

Browse files
.gitattributes CHANGED
@@ -69,3 +69,5 @@ feedback/real/real9.jpg filter=lfs diff=lfs merge=lfs -text
69
  feedback/real/Screenshot_20260430_064350.jpg filter=lfs diff=lfs merge=lfs -text
70
  feedback/real/WhatsApp[[:space:]]Image[[:space:]]2026-05-12[[:space:]]at[[:space:]]14.58.23.jpeg filter=lfs diff=lfs merge=lfs -text
71
  feedback/real/WhatsApp[[:space:]]Image[[:space:]]2026-05-17[[:space:]]at[[:space:]]10.20.34.jpeg filter=lfs diff=lfs merge=lfs -text
 
 
 
69
  feedback/real/Screenshot_20260430_064350.jpg filter=lfs diff=lfs merge=lfs -text
70
  feedback/real/WhatsApp[[:space:]]Image[[:space:]]2026-05-12[[:space:]]at[[:space:]]14.58.23.jpeg filter=lfs diff=lfs merge=lfs -text
71
  feedback/real/WhatsApp[[:space:]]Image[[:space:]]2026-05-17[[:space:]]at[[:space:]]10.20.34.jpeg filter=lfs diff=lfs merge=lfs -text
72
+ feedback/fake/ai9.png filter=lfs diff=lfs merge=lfs -text
73
+ feedback/pending/695d9e42_Gemini_Generated_Image_[[:space:]](3).png filter=lfs diff=lfs merge=lfs -text
app_database.db CHANGED
Binary files a/app_database.db and b/app_database.db differ
 
backend.py CHANGED
@@ -157,11 +157,19 @@ def api_scan_image(file: UploadFile = File(...), username: str = Form(...)):
157
 
158
  result = resp.json()
159
  prediction = result.get("prediction", "REAL")
160
- confidence = result.get("confidence", 0.0)
 
 
 
 
 
 
 
 
161
 
162
  is_ai = prediction.upper() in ("AI", "FAKE")
163
  source = "Pollinations AI (Stable Diffusion)" if is_ai else "Kamera/Foto Digital Asli"
164
- accuracy = round(confidence * 100, 1)
165
 
166
  feedback_path = f"{PENDING_DIR}/{uid}_{safe_name}"
167
  save_compressed_image(temp_path, feedback_path)
@@ -177,7 +185,9 @@ def api_scan_image(file: UploadFile = File(...), username: str = Form(...)):
177
  is_outlier = 1 if similarity < 0.88 else 0
178
 
179
  # Detect low-light and monochrome conditions
180
- is_dark, is_grayscale, avg_brightness = analyze_image_conditions(temp_path)
 
 
181
 
182
  # Check for Trap image (Solusi 3)
183
  is_trap = 1 if "trap" in file.filename.lower() else 0
@@ -229,7 +239,9 @@ def api_scan_image(file: UploadFile = File(...), username: str = Form(...)):
229
  "trust_score": trust_score,
230
  "is_dark": is_dark,
231
  "is_grayscale": is_grayscale,
232
- "avg_brightness": avg_brightness
 
 
233
  }
234
  finally:
235
  if os.path.exists(temp_path): os.remove(temp_path)
@@ -288,9 +300,18 @@ async def api_batch_scan(files: list[UploadFile] = File(...), username: str = Fo
288
 
289
  result = resp.json()
290
  prediction = result.get("prediction", "REAL")
291
- confidence = result.get("confidence", 0.0)
 
 
 
 
 
 
 
 
 
292
  prediction_label = "AI" if prediction.upper() in ("AI", "FAKE") else "REAL"
293
- confidence_pct = round(confidence * 100, 1)
294
 
295
  is_mismatch = 0
296
  if folder_label:
@@ -489,6 +510,26 @@ def api_correction_single(data: dict):
489
  "new_trust": new_trust
490
  }
491
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  @app.get("/api/history/{username}")
493
  def api_get_history(username: str):
494
  return {"history": database.get_user_history(username)}
 
157
 
158
  result = resp.json()
159
  prediction = result.get("prediction", "REAL")
160
+ confidence_str = result.get("confidence", "0.0%")
161
+
162
+ try:
163
+ if isinstance(confidence_str, str):
164
+ confidence_val = float(confidence_str.replace("%", "").strip()) / 100.0
165
+ else:
166
+ confidence_val = float(confidence_str)
167
+ except Exception:
168
+ confidence_val = 0.85 # fallback
169
 
170
  is_ai = prediction.upper() in ("AI", "FAKE")
171
  source = "Pollinations AI (Stable Diffusion)" if is_ai else "Kamera/Foto Digital Asli"
172
+ accuracy = round(confidence_val * 100, 1)
173
 
174
  feedback_path = f"{PENDING_DIR}/{uid}_{safe_name}"
175
  save_compressed_image(temp_path, feedback_path)
 
185
  is_outlier = 1 if similarity < 0.88 else 0
186
 
187
  # Detect low-light and monochrome conditions
188
+ is_dark, is_grayscale_local, avg_brightness = analyze_image_conditions(temp_path)
189
+ is_monochrome_detected = bool(result.get("is_monochrome_detected", False))
190
+ is_grayscale = is_monochrome_detected
191
 
192
  # Check for Trap image (Solusi 3)
193
  is_trap = 1 if "trap" in file.filename.lower() else 0
 
239
  "trust_score": trust_score,
240
  "is_dark": is_dark,
241
  "is_grayscale": is_grayscale,
242
+ "is_monochrome_detected": is_monochrome_detected,
243
+ "avg_brightness": avg_brightness,
244
+ "forensic_analysis_logs": result.get("forensic_analysis_logs", {})
245
  }
246
  finally:
247
  if os.path.exists(temp_path): os.remove(temp_path)
 
300
 
301
  result = resp.json()
302
  prediction = result.get("prediction", "REAL")
303
+ confidence_str = result.get("confidence", "0.0%")
304
+
305
+ try:
306
+ if isinstance(confidence_str, str):
307
+ confidence_val = float(confidence_str.replace("%", "").strip()) / 100.0
308
+ else:
309
+ confidence_val = float(confidence_str)
310
+ except Exception:
311
+ confidence_val = 0.85 # fallback
312
+
313
  prediction_label = "AI" if prediction.upper() in ("AI", "FAKE") else "REAL"
314
+ confidence_pct = round(confidence_val * 100, 1)
315
 
316
  is_mismatch = 0
317
  if folder_label:
 
510
  "new_trust": new_trust
511
  }
512
 
513
+ @app.post("/save-feedback")
514
+ async def save_feedback_direct(file: UploadFile = File(...), correct_label: str = Form(...)):
515
+ # Standardize correct label to folder target
516
+ folder = "real" if correct_label.lower() in ("real", "true", "asli") else "fake"
517
+ target_dir = os.path.join(FEEDBACK_DIR, folder)
518
+ os.makedirs(target_dir, exist_ok=True)
519
+
520
+ safe_name = file.filename.replace("\\", "/").split("/")[-1]
521
+ target_path = os.path.join(target_dir, safe_name)
522
+
523
+ # Save uploaded file content directly
524
+ contents = await file.read()
525
+ with open(target_path, "wb") as f:
526
+ f.write(contents)
527
+
528
+ return {
529
+ "status": "saved",
530
+ "path": f"feedback/{folder}/{safe_name}"
531
+ }
532
+
533
  @app.get("/api/history/{username}")
534
  def api_get_history(username: str):
535
  return {"history": database.get_user_history(username)}
feedback/fake/ChatGPT Image May 17, 2026, 06_10_56 PM.png CHANGED

Git LFS Details

  • SHA256: 7c94554902c84e02041606dbc1e0326483d61fda0dfdd4d2c373f3dce0bbaa73
  • Pointer size: 131 Bytes
  • Size of remote file: 878 kB

Git LFS Details

  • SHA256: a243d612aac6dd00ac29bfc7b903efbe7a00af956d4cf73e74e448b40129cd99
  • Pointer size: 131 Bytes
  • Size of remote file: 187 kB
feedback/fake/Gemini_Generated_Image_ (1).png CHANGED

Git LFS Details

  • SHA256: 0023266e0307bb2aba23c2c9524c6097f90c8d17ff8d61ce2792fff3a97db9a9
  • Pointer size: 131 Bytes
  • Size of remote file: 812 kB

Git LFS Details

  • SHA256: eceffb92fa1ac6ea173903b13f5abc4439b950ab31885f2a4c9200521bf9dde1
  • Pointer size: 131 Bytes
  • Size of remote file: 181 kB
feedback/fake/Gemini_Generated_Image_ (3).png CHANGED

Git LFS Details

  • SHA256: 695ba7d8de243e836fc90468d321c41faccc2964dcc74c6c29268090a548dbb3
  • Pointer size: 131 Bytes
  • Size of remote file: 986 kB

Git LFS Details

  • SHA256: 62d3b78d60920fd7697c92c213c305226ace898c3ea8c96e5e3673dbe031bcd1
  • Pointer size: 131 Bytes
  • Size of remote file: 120 kB
feedback/fake/Gemini_Generated_Image_.png CHANGED

Git LFS Details

  • SHA256: 73e1ec29f5940267e04f07b84657b13b091d5de58ce36a7dc8820e4e80d59e46
  • Pointer size: 132 Bytes
  • Size of remote file: 2.31 MB

Git LFS Details

  • SHA256: 4b07bf1171fbd1bd0a9c89b2bb3ac228071960005c2dd718ae6f417f6c0ff481
  • Pointer size: 131 Bytes
  • Size of remote file: 465 kB
feedback/fake/ai-2.jpeg CHANGED
feedback/fake/ai-3.jpeg CHANGED
feedback/fake/ai.jpeg CHANGED

Git LFS Details

  • SHA256: 1421bb549966ac72e5d0f20f68d67d667b5447e0b3cb63105c05d12ce2a22e7f
  • Pointer size: 131 Bytes
  • Size of remote file: 140 kB

Git LFS Details

  • SHA256: 2e3754d768a55199315ab1048abb8a9fcbc64fb935a25de3e58705b17df47c97
  • Pointer size: 130 Bytes
  • Size of remote file: 37 kB
feedback/fake/ai9.png ADDED

Git LFS Details

  • SHA256: 9048ae9efd757bceb7d5ed2eff28ecee971fa6a56e86295f871b7a6ccbc8a0f7
  • Pointer size: 131 Bytes
  • Size of remote file: 599 kB
feedback/pending/695d9e42_Gemini_Generated_Image_ (3).png ADDED

Git LFS Details

  • SHA256: 62d3b78d60920fd7697c92c213c305226ace898c3ea8c96e5e3673dbe031bcd1
  • Pointer size: 131 Bytes
  • Size of remote file: 120 kB
feedback/pending/6d2b097f_ai.jpeg ADDED
feedback/real/Joko.jpg CHANGED
feedback/real/real-2.jpeg CHANGED
feedback/real/real-3.jpg CHANGED

Git LFS Details

  • SHA256: ecd4589280eda64ee4f84fcd29b484ef068d839f93a69b90d53c17a2ed16d04d
  • Pointer size: 131 Bytes
  • Size of remote file: 121 kB

Git LFS Details

  • SHA256: a2fe8e46bf2905d022e585623d5da473d4f64e41b0b81477d72d6e38d93b277b
  • Pointer size: 130 Bytes
  • Size of remote file: 52 kB
feedback/real/real-4.jpg CHANGED
feedback/real/real-5.jpg CHANGED

Git LFS Details

  • SHA256: 22a4209b7ae86e128c621f7a338f3fca1d7890e2e2c934bf8223982b49530b37
  • Pointer size: 131 Bytes
  • Size of remote file: 410 kB

Git LFS Details

  • SHA256: 00f68b42eb6fd42d5cbf520405de2d38636066484a835b231483309b5544fb27
  • Pointer size: 130 Bytes
  • Size of remote file: 23 kB
feedback/real/real-6.jpg CHANGED

Git LFS Details

  • SHA256: 64bc664eb172b4f640859736fae9ac62d288063586a963be5234a74d4e7467bc
  • Pointer size: 131 Bytes
  • Size of remote file: 515 kB

Git LFS Details

  • SHA256: 36f6e2642bf92aeeca3e6316acede8b0b201625954d17780d42356b5fde61d51
  • Pointer size: 130 Bytes
  • Size of remote file: 20.9 kB
feedback/real/real.jpeg CHANGED

Git LFS Details

  • SHA256: f981fb84cd88ba83e93db7bfff04652a0ff539221c3f48f9bdc3af38e950095e
  • Pointer size: 131 Bytes
  • Size of remote file: 106 kB

Git LFS Details

  • SHA256: 420bfacdcd0dbd9d7d351938c7869552c9a0a8e615571cc1858d6ae81117fb63
  • Pointer size: 130 Bytes
  • Size of remote file: 18.6 kB
index.html CHANGED
@@ -110,6 +110,9 @@
110
  </div>
111
  <button class="btn-scan" style="width: 100%; margin-top: 15px;" onclick="scanLandingFile()">🔍 PINDAI GAMBAR</button>
112
  </div>
 
 
 
113
 
114
  <!-- Result Box for Landing Page -->
115
  <div id="landing-result-image" class="result-box hidden" style="margin-top: 20px;"></div>
@@ -226,6 +229,9 @@
226
  </div>
227
  <button class="btn-scan" onclick="scanFile()">🔍 SCAN GAMBAR</button>
228
  </div>
 
 
 
229
  <div id="result-image" class="result-box hidden"></div>
230
  </section>
231
 
 
110
  </div>
111
  <button class="btn-scan" style="width: 100%; margin-top: 15px;" onclick="scanLandingFile()">🔍 PINDAI GAMBAR</button>
112
  </div>
113
+ <div id="landing-warning-banner-monokrom" style="display: none; background: rgba(230,126,34,0.15); border: 1px dashed rgba(230,126,34,0.3); border-radius: 8px; padding: 12px; margin-top: 15px; font-size: 12px; color: #f39c12; line-height: 1.5; text-align: left;">
114
+ <b>⚠️ Peringatan Citra Monokrom:</b> Gambar hitam-putih terdeteksi. Kehilangan informasi warna RGB dapat menurunkan keakuratan klasifikasi model AI. Disarankan menggunakan foto penuh warna.
115
+ </div>
116
 
117
  <!-- Result Box for Landing Page -->
118
  <div id="landing-result-image" class="result-box hidden" style="margin-top: 20px;"></div>
 
229
  </div>
230
  <button class="btn-scan" onclick="scanFile()">🔍 SCAN GAMBAR</button>
231
  </div>
232
+ <div id="warning-banner-monokrom" style="display: none; background: rgba(230,126,34,0.15); border: 1px dashed rgba(230,126,34,0.3); border-radius: 8px; padding: 12px; margin-top: 15px; font-size: 12px; color: #f39c12; line-height: 1.5; text-align: left;">
233
+ <b>⚠️ Peringatan Citra Monokrom:</b> Gambar hitam-putih terdeteksi. Kehilangan informasi warna RGB dapat menurunkan keakuratan klasifikasi model AI. Disarankan menggunakan foto penuh warna.
234
+ </div>
235
  <div id="result-image" class="result-box hidden"></div>
236
  </section>
237
 
script.js CHANGED
@@ -323,6 +323,18 @@ async function simulateForensicAnalysis(resultBox, fetchPromise) {
323
  container.scrollTop = container.scrollHeight;
324
  }
325
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  async function scanFile() {
327
  const fileInput = document.getElementById("input-image");
328
  const resultBox = document.getElementById("result-image");
@@ -353,6 +365,16 @@ async function scanFile() {
353
  const res = await fetchPromise;
354
  const data = await res.json();
355
 
 
 
 
 
 
 
 
 
 
 
356
  if (res.ok) {
357
  if (currentUser === "__guest__") {
358
  let count = parseInt(localStorage.getItem("guest_scan_count") || "0");
@@ -365,6 +387,26 @@ async function scanFile() {
365
  const statusText = data.is_ai ? "⚠️ GAMBAR PALSU (AI)" : "✅ GAMBAR ASLI (REAL)";
366
  const statusColor = data.is_ai ? "var(--danger)" : "var(--success)";
367
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  resultBox.className = `result-box ${statusClass}`;
369
  resultBox.innerHTML = `
370
  <div class="result-title" style="color: ${statusColor}">
@@ -399,12 +441,14 @@ async function scanFile() {
399
  <b>⚠️ Rekomendasi Kondisi Deteksi:</b><br/>
400
  ${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/>' : ''}
401
  ${data.is_grayscale ? '• Gambar monokrom/hitam-putih terdeteksi. Kehilangan informasi kromatik (saluran warna RGB) secara drastis dapat menurunkan performa klasifikasi model AI.<br/>' : ''}
402
- <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>
403
  </div>
404
  `
405
  : ''
406
  }
407
 
 
 
408
  <div class="details-grid">
409
  <div class="detail-item">
410
  <div class="detail-label">Nama File</div>
@@ -1010,6 +1054,16 @@ async function scanLandingFile() {
1010
  const res = await fetchPromise;
1011
  const data = await res.json();
1012
 
 
 
 
 
 
 
 
 
 
 
1013
  if (res.ok) {
1014
  let count = parseInt(localStorage.getItem("guest_scan_count") || "0");
1015
  count++;
@@ -1020,6 +1074,26 @@ async function scanLandingFile() {
1020
  const statusText = data.is_ai ? "⚠️ GAMBAR PALSU (AI)" : "✅ GAMBAR ASLI (REAL)";
1021
  const statusColor = data.is_ai ? "var(--danger)" : "var(--success)";
1022
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
  resultBox.className = `result-box ${statusClass}`;
1024
  resultBox.innerHTML = `
1025
  <div class="result-title" style="color: ${statusColor}">
@@ -1060,6 +1134,8 @@ async function scanLandingFile() {
1060
  : ''
1061
  }
1062
 
 
 
1063
  <div class="details-grid">
1064
  <div class="detail-item">
1065
  <div class="detail-label">Nama File</div>
 
323
  container.scrollTop = container.scrollHeight;
324
  }
325
 
326
+ function toggleForensicLogs(btn) {
327
+ const content = btn.nextElementSibling;
328
+ const arrow = btn.querySelector("span:last-child");
329
+ if (content.classList.contains("hidden")) {
330
+ content.classList.remove("hidden");
331
+ arrow.innerText = "▲";
332
+ } else {
333
+ content.classList.add("hidden");
334
+ arrow.innerText = "▼";
335
+ }
336
+ }
337
+
338
  async function scanFile() {
339
  const fileInput = document.getElementById("input-image");
340
  const resultBox = document.getElementById("result-image");
 
365
  const res = await fetchPromise;
366
  const data = await res.json();
367
 
368
+ // CEK OTOMATIS GAMBAR HITAM PUTIH UNTUK TAMPILKAN BANNER ALERT
369
+ const warningBanner = document.getElementById("warning-banner-monokrom");
370
+ if (warningBanner) {
371
+ if (data.is_monochrome_detected === true) {
372
+ warningBanner.style.display = "block";
373
+ } else {
374
+ warningBanner.style.display = "none";
375
+ }
376
+ }
377
+
378
  if (res.ok) {
379
  if (currentUser === "__guest__") {
380
  let count = parseInt(localStorage.getItem("guest_scan_count") || "0");
 
387
  const statusText = data.is_ai ? "⚠️ GAMBAR PALSU (AI)" : "✅ GAMBAR ASLI (REAL)";
388
  const statusColor = data.is_ai ? "var(--danger)" : "var(--success)";
389
 
390
+ let logsHtml = '';
391
+ if (data.forensic_analysis_logs && Object.keys(data.forensic_analysis_logs).length > 0) {
392
+ logsHtml = `
393
+ <div class="forensic-logs-detail" style="margin-top: 15px; margin-bottom: 15px; text-align: left;">
394
+ <button class="btn-refresh" style="width: 100%; text-align: left; padding: 10px 15px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,215,0,0.25); border-radius: 8px; color: var(--yellow-main); cursor: pointer; font-size: 13px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 0;" onclick="toggleForensicLogs(this)">
395
+ <span>🔍 Lihat Log Detil Forensik (12 Langkah)</span>
396
+ <span style="font-size:10px">▼</span>
397
+ </button>
398
+ <div class="logs-content hidden" style="background: rgba(0,0,0,0.5); border: 1px solid rgba(255,215,0,0.15); border-top: none; border-radius: 0 0 8px 8px; padding: 15px; font-family: monospace; font-size: 12px; color: #2ed573; line-height: 1.6; max-height: 250px; overflow-y: auto;">
399
+ `;
400
+ for (let i = 1; i <= 12; i++) {
401
+ const stepText = data.forensic_analysis_logs[`step_${i}`] || `[Step ${i}/12] Analisis selesai.`;
402
+ logsHtml += `<div style="margin-bottom: 6px; border-bottom: 1px dashed rgba(255,255,255,0.05); padding-bottom: 4px;">${stepText}</div>`;
403
+ }
404
+ logsHtml += `
405
+ </div>
406
+ </div>
407
+ `;
408
+ }
409
+
410
  resultBox.className = `result-box ${statusClass}`;
411
  resultBox.innerHTML = `
412
  <div class="result-title" style="color: ${statusColor}">
 
441
  <b>⚠️ Rekomendasi Kondisi Deteksi:</b><br/>
442
  ${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/>' : ''}
443
  ${data.is_grayscale ? '• Gambar monokrom/hitam-putih terdeteksi. Kehilangan informasi kromatik (saluran warna RGB) secara drastis dapat menurunkan performa klasifikasi model AI.<br/>' : ''}
444
+ <i style="display:block;margin-top:6px;color:rgba(255,255,255,0.7)">Disarankan untuk melakukan scan ulang menggunakan foto dengan pencahayaan cukup and penuh warna (Full RGB).</i>
445
  </div>
446
  `
447
  : ''
448
  }
449
 
450
+ ${logsHtml}
451
+
452
  <div class="details-grid">
453
  <div class="detail-item">
454
  <div class="detail-label">Nama File</div>
 
1054
  const res = await fetchPromise;
1055
  const data = await res.json();
1056
 
1057
+ // CEK OTOMATIS GAMBAR HITAM PUTIH UNTUK TAMPILKAN BANNER ALERT
1058
+ const warningBanner = document.getElementById("landing-warning-banner-monokrom");
1059
+ if (warningBanner) {
1060
+ if (data.is_monochrome_detected === true) {
1061
+ warningBanner.style.display = "block";
1062
+ } else {
1063
+ warningBanner.style.display = "none";
1064
+ }
1065
+ }
1066
+
1067
  if (res.ok) {
1068
  let count = parseInt(localStorage.getItem("guest_scan_count") || "0");
1069
  count++;
 
1074
  const statusText = data.is_ai ? "⚠️ GAMBAR PALSU (AI)" : "✅ GAMBAR ASLI (REAL)";
1075
  const statusColor = data.is_ai ? "var(--danger)" : "var(--success)";
1076
 
1077
+ let logsHtml = '';
1078
+ if (data.forensic_analysis_logs && Object.keys(data.forensic_analysis_logs).length > 0) {
1079
+ logsHtml = `
1080
+ <div class="forensic-logs-detail" style="margin-top: 15px; margin-bottom: 15px; text-align: left;">
1081
+ <button class="btn-refresh" style="width: 100%; text-align: left; padding: 10px 15px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,215,0,0.25); border-radius: 8px; color: var(--yellow-main); cursor: pointer; font-size: 13px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 0;" onclick="toggleForensicLogs(this)">
1082
+ <span>🔍 Lihat Log Detil Forensik (12 Langkah)</span>
1083
+ <span style="font-size:10px">▼</span>
1084
+ </button>
1085
+ <div class="logs-content hidden" style="background: rgba(0,0,0,0.5); border: 1px solid rgba(255,215,0,0.15); border-top: none; border-radius: 0 0 8px 8px; padding: 15px; font-family: monospace; font-size: 12px; color: #2ed573; line-height: 1.6; max-height: 250px; overflow-y: auto;">
1086
+ `;
1087
+ for (let i = 1; i <= 12; i++) {
1088
+ const stepText = data.forensic_analysis_logs[`step_${i}`] || `[Step ${i}/12] Analisis selesai.`;
1089
+ logsHtml += `<div style="margin-bottom: 6px; border-bottom: 1px dashed rgba(255,255,255,0.05); padding-bottom: 4px;">${stepText}</div>`;
1090
+ }
1091
+ logsHtml += `
1092
+ </div>
1093
+ </div>
1094
+ `;
1095
+ }
1096
+
1097
  resultBox.className = `result-box ${statusClass}`;
1098
  resultBox.innerHTML = `
1099
  <div class="result-title" style="color: ${statusColor}">
 
1134
  : ''
1135
  }
1136
 
1137
+ ${logsHtml}
1138
+
1139
  <div class="details-grid">
1140
  <div class="detail-item">
1141
  <div class="detail-label">Nama File</div>