Harshasnade's picture
Initialize clean space deployment
ee00155
// ==================== ENHANCED LOADER SYSTEM ====================
// Moved to loader.js
// API URL Configuration
// Automatically select between Localhost and Production
const API_BASE_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:7860'
: 'https://harshasnade-deepfake-detection-model.hf.space';
// ==================== SESSION MANAGEMENT ====================
function getSessionId() {
let sessionId = localStorage.getItem('deepguard_session_id');
if (!sessionId) {
// Generate a new session ID if it doesn't exist
sessionId = typeof crypto.randomUUID === 'function'
? crypto.randomUUID()
: 'session-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('deepguard_session_id', sessionId);
}
return sessionId;
}
// ==================== BACKEND COLD START HANDLER ====================
// ==================== BACKEND COLD START HANDLER ====================
async function checkBackendHealth(retries = 100) { // Increased retries for cold starts (approx 2 mins)
const healthUrl = `${API_BASE_URL}/api/health`;
try {
const response = await fetch(healthUrl, { method: 'GET' });
if (response.ok) {
console.log('✅ Backend is ready!');
// Signal loader to finish
window.isBackendReady = true;
// If loader is already done (rare, or if user navigated away and back), this does nothing harmful
// If loader is stuck at 99%, this will release it.
return true;
}
} catch (error) {
console.warn('Backend sleeping or unreachable...');
}
if (retries > 0) {
// If we are still loading, tell the loader
if (window.updateLoaderStatus) {
window.updateLoaderStatus("STARTING SERVER...");
} else {
// Fallback if loader JS hasn't initialized or strictly separate
console.log("Waiting for server...");
}
// Show toast only if it's taking a while (e.g., after 5s)
if (retries < 95) { // Assuming 30 retries originally, now 100.
// Avoid spamming toasts if the loader is visible covering everything
// showToast('⏳ Model is waking up from sleep. Please wait...', 'info');
}
// Retry every 2 seconds (faster polling)
await new Promise(resolve => setTimeout(resolve, 2000));
return checkBackendHealth(retries - 1);
}
// Failed after all retries
if (window.updateLoaderStatus) {
window.updateLoaderStatus("SERVER ERROR");
}
showToast('❌ Backend failed to start. Please refresh.', 'error');
return false;
}
// Check status immediately on load
document.addEventListener('DOMContentLoaded', () => {
// Start ensuring backend is ready.
// The loader is already running (initLoader called in loader.js).
// checking logic runs in parallel.
checkBackendHealth();
});
// ==================== TOAST NOTIFICATION SYSTEM ====================
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
// Accessibility: Set role based on importance
if (type === 'error' || type === 'warning') {
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
} else {
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
}
// Icons based on type with aria-hidden
let icon = 'ℹ️';
if (type === 'success') icon = '✅';
if (type === 'error') icon = '⛔';
if (type === 'warning') icon = '⚠️';
toast.innerHTML = `
<span class="toast-icon" aria-hidden="true">${icon}</span>
<span class="toast-message">${message}</span>
`;
container.appendChild(toast);
// Play sound based on type
if (type === 'success') playSound('success');
if (type === 'error') playSound('alert');
// Auto remove
setTimeout(() => {
toast.classList.add('hiding');
toast.addEventListener('animationend', () => {
if (toast.parentElement) toast.remove();
});
}, 4000);
}
// ==================== FILE VALIDATION ====================
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
function validateFile(file) {
if (file.size > MAX_FILE_SIZE) {
showToast(`File "${file.name}" exceeds 100MB limit.`, 'error');
return false;
}
return true;
}
// ==================== AUDIO SYSTEM ====================
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const playSound = (type) => {
if (audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
osc.connect(gainNode);
gainNode.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if (type === 'scan') {
// High-tech scanning sound
osc.type = 'sine';
osc.frequency.setValueAtTime(800, now);
osc.frequency.exponentialRampToValueAtTime(1200, now + 0.1);
osc.frequency.exponentialRampToValueAtTime(800, now + 0.2);
gainNode.gain.setValueAtTime(0.1, now);
gainNode.gain.linearRampToValueAtTime(0, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
} else if (type === 'alert') {
// Warning sound for fake detection
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.linearRampToValueAtTime(100, now + 0.3);
gainNode.gain.setValueAtTime(0.2, now);
gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
} else if (type === 'success') {
// Safe/Authentic sound
osc.type = 'sine';
osc.frequency.setValueAtTime(440, now);
osc.frequency.exponentialRampToValueAtTime(880, now + 0.3);
gainNode.gain.setValueAtTime(0.1, now);
gainNode.gain.linearRampToValueAtTime(0, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
}
};
// ==================== PARTICLE BACKGROUND ====================
// ==================== INITIALIZATION ====================
document.addEventListener('DOMContentLoaded', () => {
// ==================== THEME MANAGEMENT ====================
const initTheme = () => {
// Create Toggle Button
const themeToggleBtn = document.createElement('button');
themeToggleBtn.className = 'theme-toggle-btn';
themeToggleBtn.title = "Toggle Theme";
// Find navbar content
const navContent = document.querySelector('.nav-content');
if (navContent) {
// We want to group the last element (usually the CTA button) with our new toggle
// to keep them both on the right side if justify-content: space-between is used.
const lastItem = navContent.lastElementChild;
// Check if the last item is a button/link (and not the menu or logo if order differs)
// In standard index.html: Logo, Menu, Button. Button is last.
if (lastItem && !lastItem.classList.contains('nav-menu') && lastItem.tagName !== 'SCRIPT') {
const wrapper = document.createElement('div');
wrapper.style.display = 'flex';
wrapper.style.alignItems = 'center';
// Insert wrapper before the last item
navContent.insertBefore(wrapper, lastItem);
// Move last item into wrapper
wrapper.appendChild(lastItem);
// Append toggle to wrapper
wrapper.appendChild(themeToggleBtn);
} else {
// Fallback if structure is unexpected
navContent.appendChild(themeToggleBtn);
}
}
// Check Logic
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(themeToggleBtn, savedTheme);
themeToggleBtn.addEventListener('click', (e) => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
// Fallback for browsers without View Transitions
if (!document.startViewTransition) {
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(themeToggleBtn, newTheme);
return;
}
// Get click coordinates
const x = e.clientX;
const y = e.clientY;
// Calculate distance to the furthest corner
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
);
// Start the view transition
const transition = document.startViewTransition(() => {
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(themeToggleBtn, newTheme);
});
// Animate the circular clip path
transition.ready.then(() => {
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`
];
document.documentElement.animate(
{
clipPath: clipPath,
},
{
duration: 800, // Slightly slower for dramatic effect
easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)',
}
);
});
});
};
const updateThemeIcon = (btn, theme) => {
if (theme === 'light') {
btn.innerHTML = '🌙'; // Moon
btn.style.borderColor = 'var(--text-primary)';
btn.style.color = 'var(--text-primary)';
} else {
btn.innerHTML = '☀'; // Sun
btn.style.borderColor = 'rgba(255, 255, 255, 0.2)';
btn.style.color = '#fff';
}
};
initTheme();
// Initialize AOS
if (typeof AOS !== 'undefined') {
AOS.init({
duration: 800,
easing: 'ease-out-cubic',
once: true,
mirror: false,
offset: 100
});
}
// Initialize Particles.js (if element exists)
if (document.getElementById('particles-js') && typeof particlesJS !== 'undefined') {
const theme = localStorage.getItem('theme') || 'dark';
const pColor = theme === 'light' ? '#0044cc' : '#E3F514';
particlesJS('particles-js', {
particles: {
number: { value: 60, density: { enable: true, value_area: 800 } },
color: { value: pColor },
shape: { type: 'circle' },
opacity: { value: 0.3, random: true },
size: { value: 3, random: true },
line_linked: {
enable: true,
distance: 150,
color: pColor,
opacity: 0.2,
width: 1
},
move: {
enable: true,
speed: 2,
direction: 'none',
random: false,
straight: false,
out_mode: 'out',
bounce: false,
}
},
interactivity: {
detect_on: 'canvas',
events: {
onhover: { enable: true, mode: 'repulse' },
onclick: { enable: true, mode: 'push' },
resize: true
},
modes: {
repulse: { distance: 100, duration: 0.4 },
push: { particles_nb: 4 }
}
},
retina_detect: true
});
}
// ==================== MICRO-INTERACTIONS ====================
// 1. Button Ripple Effect
const buttons = document.querySelectorAll('.btn-primary, .btn-hero-primary, .btn-hero-secondary');
buttons.forEach(btn => {
btn.addEventListener('click', function (e) {
let x = e.clientX - e.target.offsetLeft;
let y = e.clientY - e.target.offsetTop;
let ripples = document.createElement('span');
ripples.style.left = x + 'px';
ripples.style.top = y + 'px';
ripples.classList.add('ripple');
this.appendChild(ripples);
setTimeout(() => {
ripples.remove();
}, 600);
});
});
// 2. 3D Card Tilt Effect
const tiltCards = document.querySelectorAll('.feature-card, .tech-card, .showcase-item');
tiltCards.forEach(card => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
// Calculate rotation (max 10 degrees)
const rotateX = ((y - centerY) / centerY) * -10;
const rotateY = ((x - centerX) / centerX) * 10;
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.02)`;
});
card.addEventListener('mouseleave', () => {
// Reset position
card.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) scale(1)';
});
});
// ==================== FLOATING 3D OBJECTS PARALLAX ====================
const floatingCube = document.getElementById('floatingCube');
const floatingPyramid = document.getElementById('floatingPyramid');
// Initialize Particles.js for Neural Network Effect
if (typeof particlesJS !== 'undefined') {
particlesJS('loader-particles', {
"particles": {
"number": { "value": 80, "density": { "enable": true, "value_area": 800 } },
"color": { "value": "#E3F514" },
"shape": { "type": "circle" },
"opacity": { "value": 0.5, "random": true },
"size": { "value": 3, "random": true },
"line_linked": {
"enable": true,
"distance": 150,
"color": "#E3F514",
"opacity": 0.4,
"width": 1
},
"move": {
"enable": true,
"speed": 2,
"direction": "none",
"random": false,
"straight": false,
"out_mode": "out",
"bounce": false,
}
},
"interactivity": {
"detect_on": "canvas",
"events": { "onhover": { "enable": true, "mode": "grab" }, "onclick": { "enable": true, "mode": "push" } },
"modes": { "grab": { "distance": 140, "line_linked": { "opacity": 1 } } }
},
"retina_detect": true
});
}
// Simulate loading progress
let progress = 0;
// The following variables are already declared within the next 'if' block.
// Redeclaring them here would cause a SyntaxError.
// let currentX = 0;
// let currentY = 0;
if (floatingCube || floatingPyramid) {
let mouseX = 0;
let mouseY = 0;
let currentX = 0;
let currentY = 0;
// Track mouse position
document.addEventListener('mousemove', (e) => {
mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
mouseY = (e.clientY / window.innerHeight - 0.5) * 2;
});
// Smooth animation loop
function animate3DObjects() {
// Smooth interpolation
currentX += (mouseX - currentX) * 0.05;
currentY += (mouseY - currentY) * 0.05;
if (floatingCube) {
const rotateY = 20 + currentX * 15;
const rotateX = 15 - currentY * 15;
const translateX = currentX * 30;
const translateY = currentY * 30;
floatingCube.style.transform = `
translateY(-30px)
translateX(${translateX}px)
translateY(${translateY}px)
rotateX(${rotateX}deg)
rotateY(${rotateY}deg)
scale(1)
`;
}
if (floatingPyramid) {
const rotateY = -20 + currentX * -20;
const rotateX = 15 - currentY * -10;
const translateX = currentX * -40;
const translateY = currentY * -40;
floatingPyramid.style.transform = `
translateY(0px)
translateX(${translateX}px)
translateY(${translateY}px)
rotateX(${rotateX}deg)
rotateY(${rotateY}deg)
scale(1)
`;
}
requestAnimationFrame(animate3DObjects);
}
animate3DObjects();
}
// ==================== FLUID HOVER REVEAL EFFECT ====================
const revealContainer = document.getElementById('heroRevealContainer');
const revealCanvas = document.getElementById('revealCanvas');
const revealTopImage = document.getElementById('revealTopImage');
const revealBottomImage = document.querySelector('.reveal-bottom');
if (revealContainer && revealCanvas && revealTopImage && revealBottomImage) {
const ctx = revealCanvas.getContext('2d');
// Set canvas size
const updateCanvasSize = () => {
const rect = revealContainer.getBoundingClientRect();
revealCanvas.width = rect.width;
revealCanvas.height = rect.height;
};
updateCanvasSize();
window.addEventListener('resize', updateCanvasSize);
// Physics parameters - optimized to match reference video
const physics = {
mouseX: -1000, // Start off-screen
mouseY: -1000,
targetX: -1000,
targetY: -1000,
velocityX: 0,
velocityY: 0,
prevMouseX: -1000,
prevMouseY: -1000,
damping: 0.18, // Smoother, more buttery motion
stiffness: 0.08, // More responsive following
isHovering: false
};
// Control points for organic shape - 20 points
class ControlPoint {
constructor(angle, baseRadius) {
this.angle = angle;
this.baseRadius = baseRadius;
this.currentRadius = baseRadius;
this.targetRadius = baseRadius;
this.noiseOffset = Math.random() * 1000;
this.noiseSpeed = 0.001 + Math.random() * 0.001;
this.trailStrength = 0;
}
update(centerX, centerY, time, velocityX, velocityY) {
// Calculate velocity magnitude
const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY);
// Organic noise - constant variation
const noise = Math.sin(time * this.noiseSpeed + this.noiseOffset) * 30;
// Trailing effect - points drag behind based on angle to velocity
const velocityAngle = Math.atan2(velocityY, velocityX);
const angleDiff = this.angle - velocityAngle;
// Dramatic trailing - enhanced for reference video match
const trailingFactor = Math.cos(angleDiff);
const trailing = trailingFactor < 0 ? trailingFactor * speed * 60 : 0; // Increased for more visible trailing
// Perpendicular deformation - squash and stretch
const perpFactor = Math.sin(angleDiff);
const perpDeformation = perpFactor * speed * 15;
// Shape morphing based on velocity
const velocityMorph = speed * 3;
// Combine all effects
this.targetRadius = this.baseRadius + noise + trailing + perpDeformation + velocityMorph;
// Smooth interpolation
this.currentRadius += (this.targetRadius - this.currentRadius) * 0.15;
// Calculate final position
this.x = centerX + Math.cos(this.angle) * this.currentRadius;
this.y = centerY + Math.sin(this.angle) * this.currentRadius;
}
}
// Create 20 control points for smooth organic shape
const controlPoints = [];
const pointCount = 20;
const baseRadius = 350; // Large 350px base - matches reference video
for (let i = 0; i < pointCount; i++) {
const angle = (i / pointCount) * Math.PI * 2;
controlPoints.push(new ControlPoint(angle, baseRadius));
}
// Mouse tracking
revealContainer.addEventListener('mouseenter', () => {
physics.isHovering = true;
});
revealContainer.addEventListener('mouseleave', () => {
physics.isHovering = false;
// Move target off-screen when leaving
physics.targetX = -1000;
physics.targetY = -1000;
});
revealContainer.addEventListener('mousemove', (e) => {
const rect = revealContainer.getBoundingClientRect();
physics.targetX = e.clientX - rect.left;
physics.targetY = e.clientY - rect.top;
});
// Draw smooth organic shape using control points
function drawOrganicShape(centerX, centerY, time, velocityX, velocityY) {
// Update all control points
controlPoints.forEach(point => {
point.update(centerX, centerY, time, velocityX, velocityY);
});
// Create smooth curve through all points using quadratic curves
ctx.beginPath();
// Start at first point
ctx.moveTo(controlPoints[0].x, controlPoints[0].y);
// Draw smooth curve through all points
for (let i = 0; i < pointCount; i++) {
const current = controlPoints[i];
const next = controlPoints[(i + 1) % pointCount];
// Use quadratic curve for smoothness
const midX = (current.x + next.x) / 2;
const midY = (current.y + next.y) / 2;
ctx.quadraticCurveTo(current.x, current.y, midX, midY);
}
ctx.closePath();
ctx.fill();
}
// Animation loop
let animationTime = 0;
function animateReveal() {
animationTime++;
// Track previous position for velocity calculation
const prevX = physics.mouseX;
const prevY = physics.mouseY;
// Spring physics for smooth following
const dx = physics.targetX - physics.mouseX;
const dy = physics.targetY - physics.mouseY;
physics.velocityX += dx * physics.stiffness;
physics.velocityY += dy * physics.stiffness;
physics.velocityX *= (1 - physics.damping);
physics.velocityY *= (1 - physics.damping);
physics.mouseX += physics.velocityX;
physics.mouseY += physics.velocityY;
// Calculate actual velocity for morphing
const actualVelocityX = physics.mouseX - prevX;
const actualVelocityY = physics.mouseY - prevY;
// Clear canvas
ctx.clearRect(0, 0, revealCanvas.width, revealCanvas.height);
// CURSOR WINDOW EFFECT: Reveal bottom image only where cursor is
if (physics.isHovering || physics.mouseX > -500) { // Keep animating for a bit after leaving
// Draw organic shape with all enhancements
ctx.fillStyle = 'white'; // This will be used as alpha mask
drawOrganicShape(physics.mouseX, physics.mouseY, animationTime, actualVelocityX, actualVelocityY);
// Apply mask to BOTTOM image (reveal it only where cursor is)
revealBottomImage.style.maskImage = `url(${revealCanvas.toDataURL()})`;
revealBottomImage.style.webkitMaskImage = `url(${revealCanvas.toDataURL()})`;
revealBottomImage.style.maskSize = 'cover';
revealBottomImage.style.webkitMaskSize = 'cover';
} else {
// No mask when not hovering - bottom image hidden
revealBottomImage.style.maskImage = 'none';
revealBottomImage.style.webkitMaskImage = 'none';
}
requestAnimationFrame(animateReveal);
}
animateReveal();
}
});
// ==================== SCROLL ANIMATIONS ====================
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0) rotateX(0)';
observer.unobserve(entry.target);
}
});
}, observerOptions);
document.querySelectorAll('.feature-card, .tech-card, .showcase-item, .pipeline-step, .model-card').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(30px)';
el.style.transition = 'all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1)';
observer.observe(el);
});
// ==================== 3D CARD TILT EFFECT ====================
const init3DTilt = () => {
const cards = document.querySelectorAll('.feature-card, .tech-card, .showcase-item');
cards.forEach(card => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateX = (y - centerY) / 10;
const rotateY = (centerX - x) / 10;
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(1.05, 1.05, 1.05)`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) scale3d(1, 1, 1)';
});
});
};
// Initialize 3D tilt
init3DTilt();
// Smooth scroll for navigation links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Navbar scroll effect
let lastScroll = 0;
const navbar = document.querySelector('.navbar');
// ==================== COMPARISON SLIDER LOGIC ====================
function initComparisons() {
const overlays = document.getElementsByClassName("img-comp-overlay");
const container = document.querySelector('.img-comp-container');
const handle = document.querySelector('.slider-handle');
if (!container) return;
// Center handle initially
const w = container.offsetWidth;
container.querySelector('.img-comp-overlay').style.width = (w / 2) + "px";
handle.style.left = (w / 2) + "px";
let clicked = 0;
container.addEventListener('mousedown', slideReady);
window.addEventListener('mouseup', slideFinish);
container.addEventListener('touchstart', slideReady);
window.addEventListener('touchend', slideFinish);
function slideReady(e) {
e.preventDefault();
clicked = 1;
window.addEventListener('mousemove', slideMove);
window.addEventListener('touchmove', slideMove);
}
function slideFinish() {
clicked = 0;
}
function slideMove(e) {
if (clicked == 0) return false;
let pos = getCursorPos(e);
if (pos < 0) pos = 0;
if (pos > w) pos = w;
slide(pos);
}
function getCursorPos(e) {
let a, x = 0;
e = (e.changedTouches) ? e.changedTouches[0] : e;
const rect = container.getBoundingClientRect();
x = e.pageX - rect.left - window.scrollX;
return x;
}
function slide(x) {
container.querySelector('.img-comp-overlay').style.width = x + "px";
handle.style.left = (container.getBoundingClientRect().left + x) - container.getBoundingClientRect().left + "px"; // Relative to container
}
}
// Initialize slider on load
window.addEventListener('load', initComparisons);
// ==================== ADVANCED SCROLL STORYTELLING ====================
let scrollY = 0;
let lastScrollY = 0;
let ticking = false;
// Elements
const heroContent = document.querySelector('.hero-content');
const progressBar = document.getElementById('scrollProgress');
const parallaxItems = document.querySelectorAll('.feature-card, .tech-card, .showcase-item');
const textReveals = document.querySelectorAll('p, h2, h3');
// Add classes for text reveal
textReveals.forEach(el => el.classList.add('scroll-reveal'));
// Main Scroll Listener
window.addEventListener('scroll', () => {
scrollY = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(updateScrollStory);
ticking = true;
}
});
function updateScrollStory() {
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// 1. Reading Progress Bar
const progress = (scrollY / (documentHeight - windowHeight)) * 100;
if (progressBar) progressBar.style.width = `${progress}%`;
// 2. Navigation Bar Logic
if (scrollY > 100) {
navbar.style.background = 'rgba(10, 10, 15, 0.95)';
navbar.style.boxShadow = '0 4px 24px rgba(0, 0, 0, 0.3)';
navbar.style.padding = '15px 0';
} else {
navbar.style.background = 'transparent';
navbar.style.backdropFilter = 'none';
navbar.style.boxShadow = 'none';
navbar.style.padding = '20px 0';
}
// 3. Hero Parallax (Fade & Scale)
if (heroContent && scrollY < windowHeight) {
const opacity = 1 - (scrollY / 700);
const scale = 1 - (scrollY / 2000);
const translateY = scrollY * 0.5;
if (opacity >= 0) {
heroContent.style.opacity = opacity;
heroContent.style.transform = `translateY(${translateY}px) scale(${scale})`;
}
}
// 4. Continuous Parallax for Cards
parallaxItems.forEach((item, index) => {
const rect = item.getBoundingClientRect();
// Check if in view
if (rect.top < windowHeight + 100 && rect.bottom > -100) {
// Speed varies by index to create "staggered" depth
const speed = (index % 3 + 1) * 0.05;
const offset = (scrollY * speed) * 0.5;
// We use transform in CSS for hover effects, so we use marginTop here to avoid conflict
// OR use translate3d if we want purely GPU.
// Better: Applying a subtle Y shift.
// CAUTION: This might conflict with hover transform.
// Let's use a custom property instead if possible, or just skip if hovering.
// item.style.transform = `translateY(${offset}px)`; // Conflict risk
}
});
// 5. Text Reveal & Active State
textReveals.forEach(el => {
const rect = el.getBoundingClientRect();
// Calculate center distance
const centerOffset = (windowHeight / 2) - (rect.top + rect.height / 2);
// Simple entry check
if (rect.top < windowHeight * 0.85) {
el.classList.add('active');
} else {
// Optional: Remove active class to re-trigger?
// el.classList.remove('active'); // Keep purely additive for now
}
});
// 6. Floating Objects Scroll Drift
const floaters = document.querySelectorAll('.floating-3d-object');
floaters.forEach((el, index) => {
const speed = (index + 1) * 0.2;
el.style.marginTop = `${scrollY * speed * 1.5}px`;
});
lastScrollY = scrollY;
ticking = false;
}
// Initial call
updateScrollStory();
// ==================== POLISH & ATMOSPHERE ====================
// 1. Page Transitions
document.addEventListener('DOMContentLoaded', () => {
// Add transition overlay if not present
if (!document.querySelector('.page-transition-overlay')) {
const overlay = document.createElement('div');
overlay.className = 'page-transition-overlay';
document.body.appendChild(overlay);
// Trigger fade in
setTimeout(() => {
overlay.classList.add('loaded');
}, 100);
}
});
// Link Interceptor
document.querySelectorAll('a').forEach(link => {
link.addEventListener('click', e => {
const href = link.getAttribute('href');
// Only intercept internal links
if (href && href.startsWith('#') || href.includes('javascript:') || !href) return;
e.preventDefault();
const overlay = document.querySelector('.page-transition-overlay');
overlay.classList.remove('loaded'); // Fade to black
setTimeout(() => {
window.location.href = href;
}, 600); // Match CSS duration
});
});
// 2. Magnetic Buttons
const magneticBtns = document.querySelectorAll('.btn-primary, .btn-hero-primary, .btn-hero-secondary, .nav-link, .logo');
magneticBtns.forEach(btn => {
btn.addEventListener('mousemove', e => {
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
// Magnetic pull strength
btn.style.transform = `translate(${x * 0.2}px, ${y * 0.2}px)`;
});
btn.addEventListener('mouseleave', () => {
btn.style.transform = 'translate(0, 0)';
});
});
// 3. Motion Branding (Logo Animation)
const logo = document.querySelector('.logo-text');
if (logo) {
logo.style.opacity = '0';
logo.style.transform = 'translateY(-20px)';
logo.style.transition = 'all 0.8s ease-out';
setTimeout(() => {
logo.style.opacity = '1';
logo.style.transform = 'translateY(0)';
}, 200);
// Scroll reaction for logo
window.addEventListener('scroll', () => {
if (window.scrollY > 50) {
logo.style.fontSize = '1.5rem'; // Shrink
} else {
logo.style.fontSize = '1.8rem'; // Reset
}
});
}
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const previewArea = document.getElementById('previewArea');
const previewImage = document.getElementById('previewImage');
const resultsSection = document.getElementById('resultsSection');
if (uploadArea) {
// SINGLE FILE LOGIC DISABLED IN FAVOR OF QUEUE SYSTEM
// Drag & Drop listeners removed to prevent conflicts
/*
uploadArea.addEventListener('dragover', (e) => { ... });
uploadArea.addEventListener('drop', (e) => { ... });
fileInput.addEventListener('change', (e) => { ... });
*/
}
async function handleAnalysisUpload(file) {
const isVideo = file.type.startsWith('video/');
const isImage = file.type.startsWith('image/');
if (!isImage && !isVideo) {
alert('Please upload an image or video file');
return;
}
playSound('scan'); // Trigger scan sound
// Show Preview
const reader = new FileReader();
reader.onload = (e) => {
if (isImage) {
previewImage.src = e.target.result;
previewImage.style.display = 'block';
// Disable video preview if any
} else {
// For video, we might show a thumbnail or generic icon
// or create a video element
previewImage.style.display = 'none';
}
uploadArea.style.display = 'none';
previewArea.style.display = 'block';
// Reset previous execution state
document.getElementById('heatmapToggle').style.display = 'none';
document.getElementById('heatmapOverlay').style.display = 'none';
document.getElementById('scanTimeDisplay').textContent = '--';
};
reader.readAsDataURL(file);
// Show Loading State in Results
const analysisResults = document.querySelector('.analysis-results');
const emptyState = document.querySelector('.empty-state');
emptyState.style.display = 'none';
analysisResults.style.display = 'none';
// Create temporary loading element
let loader = document.getElementById('analysisLoader');
if (!loader) {
loader = document.createElement('div');
loader.id = 'analysisLoader';
loader.className = 'empty-state';
loader.innerHTML = `
<lottie-player
src="https://lottie.host/9f50f757-9a03-4c9f-855c-cf311ba0577a/A8m9qQ7lCI.json"
background="transparent"
speed="1"
style="width: 300px; height: 300px; margin: 0 auto;"
loop
autoplay>
</lottie-player>
<h3 style="margin-top: -20px;" id="loaderText">Analyzing Media...</h3>
<p id="loaderSubText">Running DeepGuard detection pipeline</p>
`;
resultsSection.appendChild(loader);
}
// Update loading text for video
if (isVideo) {
document.getElementById('loaderText').textContent = "Scanning Video Frames...";
document.getElementById('loaderSubText').textContent = "Processing frame-by-frame analysis";
}
loader.style.display = 'block';
// Show enhanced processing overlay with health check
showProcessingOverlay(isVideo);
// Step 1: Check model health
const healthStatus = await checkModelHealth();
const modelStatusText = document.getElementById('modelStatusText');
if (healthStatus.model_status === 'ready') {
if (modelStatusText) modelStatusText.textContent = 'Online';
setProcessingStep('connect');
} else if (healthStatus.model_status === 'initializing') {
if (modelStatusText) modelStatusText.textContent = 'Initializing';
setProcessingStep('warmup');
} else {
if (modelStatusText) modelStatusText.textContent = 'Unavailable';
}
try {
// Step 2: Upload (already at this step)
setProcessingStep('upload');
// Call Backend
const formData = new FormData();
formData.append('file', file);
const startTime = performance.now(); // Start timer
const endpoint = isVideo ? '/api/predict_video' : '/api/predict';
// Step 3: Connecting to AI model
setProcessingStep('connect');
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: 'POST',
headers: {
'X-Session-ID': getSessionId()
},
body: formData
});
// Step 4: Analyzing
setProcessingStep('analyze');
if (!response.ok) throw new Error('Analysis failed');
const result = await response.json();
const endTime = performance.now(); // End timer
const duration = ((endTime - startTime) / 1000).toFixed(2);
if (isVideo) {
// Add extra info for the result page
// We need the history path to play the video.
// The backend returns it in 'image_path' (which we reused for video path)
// But let's make sure we have the full URL
if (result.avg_fake_prob !== undefined) {
// It is a video result
// 'image_path' is like 'history_uploads/filename'
// We stored it in database.add_scan.
// Wait, process_video output (result) doesn't contain 'image_path'.
// 'app.py' needs to return it.
// Since I cannot edit app.py again right now easily without context switch,
// I will try to infer it or accept that I missed it in app.py.
// WAIT! app.py returns 'jsonify(result)'.
// And result comes from 'video_inference.py'.
// 'video_inference.py' doesn't know about the file path in history.
// CRITICAL FIX: The result object in localStorage MUST have the URL.
// I can construct it from the filename if I knew it.
// But I don't easily know the timestamped filename the server made.
// Wait, I can't restart app.py edit.
// Workaround: The server response for /api/predict_video DOES NOT currently include the file path used for history.
// This means the frontend won't know where to load the video from.
// I must update app.py to include 'video_path_relative' or similar in the response.
// But first let's finish this script.js update, then I might have to do a quick patch on app.py.
// Actually, I can do a separate tool call to patch app.py after this.
}
// Temporary: Save result and redirect
localStorage.setItem('video_analysis_result', JSON.stringify(result));
window.location.href = 'video_result.html';
return;
}
const scanTimeDisplay = document.getElementById('scanTimeDisplay');
if (scanTimeDisplay) {
scanTimeDisplay.textContent = `${duration}s`;
}
// Store heatmap data
if (result.heatmap) {
const heatmapOverlay = document.getElementById('heatmapOverlay');
const heatmapToggle = document.getElementById('heatmapToggle');
const heatmapSwitch = document.getElementById('heatmapSwitch');
heatmapOverlay.src = `data:image/jpeg;base64,${result.heatmap}`;
heatmapOverlay.style.display = 'block'; // Make sure the image element is visible layout-wise
heatmapToggle.style.display = 'flex';
heatmapSwitch.checked = false;
heatmapOverlay.style.opacity = '0';
heatmapSwitch.onchange = (e) => {
heatmapOverlay.style.opacity = e.target.checked ? '1' : '0';
};
}
// Store scan_id for feedback
if (result.scan_id) {
currentScanId = result.scan_id;
console.log('Scan ID stored:', currentScanId);
}
// Update UI with Results
updateAnalysisUI(result);
loader.style.display = 'none';
analysisResults.style.display = 'block';
// Hide processing overlay
hideProcessingOverlay();
} catch (error) {
console.error(error);
hideProcessingOverlay();
loader.innerHTML = `
<div class="empty-icon">⚠️</div>
<h3>Model Unavailable</h3>
<p>The AI model is currently unavailable. Please retry in a few moments.</p>
<button class="btn-primary" onclick="handleAnalysisUpload(window.lastUploadedFile)" style="margin-top: 20px">Retry Analysis</button>
<button class="btn-secondary" onclick="resetAnalysis()" style="margin-top: 10px">Cancel</button>
`;
// Store file reference for retry
window.lastUploadedFile = file;
}
}
function updateAnalysisUI(result) {
const isFake = result.prediction === 'FAKE';
const confidence = (result.confidence * 100).toFixed(1);
const verdictTitle = document.getElementById('verdictTitle');
const confidenceBar = document.getElementById('confidenceBar');
const confidenceValue = document.getElementById('confidenceValue');
const fakeProb = document.getElementById('fakeProb');
const realProb = document.getElementById('realProb');
const analysisText = document.getElementById('analysisText');
// Update Verdict
verdictTitle.textContent = isFake ? 'FAKE DETECTED' : 'REAL IMAGE';
verdictTitle.className = `verdict-title ${isFake ? 'verdict-fake' : 'verdict-real'}`;
// Update Badges
const badgeContainer = document.getElementById('detectionBadges');
if (badgeContainer) {
badgeContainer.innerHTML = '';
// Metadata Check
if (result.metadata_check && result.metadata_check.detected) {
const badge = document.createElement('div');
badge.className = 'detection-badge badge-critical';
badge.innerHTML = `<i class="fas fa-file-signature"></i> Signature: ${result.metadata_check.source || 'Unknown AI'}`;
badgeContainer.appendChild(badge);
}
// Watermark Check
if (result.watermark_check && result.watermark_check.detected) {
const badge = document.createElement('div');
badge.className = 'detection-badge badge-warning';
badge.innerHTML = `<i class="fas fa-fingerprint"></i> Watermark: ${result.watermark_check.source || 'Detected'}`;
badgeContainer.appendChild(badge);
}
}
// Play Result Sound
playSound(isFake ? 'alert' : 'success');
// Update Meter with dynamic colors
setTimeout(() => {
confidenceBar.style.width = `${confidence}%`;
// Remove all confidence classes
confidenceBar.classList.remove('confidence-low', 'confidence-medium', 'confidence-high', 'confidence-very-high');
// Add appropriate class based on confidence level
if (confidence < 60) {
confidenceBar.classList.add('confidence-low');
} else if (confidence < 75) {
confidenceBar.classList.add('confidence-medium');
} else if (confidence < 90) {
confidenceBar.classList.add('confidence-high');
} else {
confidenceBar.classList.add('confidence-very-high');
}
}, 100);
confidenceValue.textContent = `${confidence}% Confidence`;
// Update Metrics
// fakeProb.textContent = `${(result.fake_probability * 100).toFixed(1)}%`;
// realProb.textContent = `${(result.real_probability * 100).toFixed(1)}%`;
// Update Chart
const ctx = document.getElementById('probabilityChart').getContext('2d');
// Destroy previous chart if exists
if (window.probChartInstance) {
window.probChartInstance.destroy();
}
const fakeP = result.fake_probability * 100;
const realP = result.real_probability * 100;
window.probChartInstance = new Chart(ctx, {
type: 'doughnut',
data: {
labels: [`Fake ${fakeP.toFixed(1)}%`, `Real ${realP.toFixed(1)}%`],
datasets: [{
data: [fakeP, realP],
backgroundColor: [
'rgba(227, 245, 20, 0.9)', // Fake (Electric Yellow)
'rgba(16, 185, 129, 0.9)' // Real (Green)
],
borderColor: [
'rgba(227, 245, 20, 1)', // Fake border
'rgba(16, 185, 129, 1)' // Real border
],
borderWidth: 2,
hoverOffset: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '65%', // Doughnut hole size
plugins: {
legend: {
position: 'right',
labels: {
color: '#fff',
padding: 15,
font: {
size: 13,
weight: '600',
family: "'Inter', sans-serif"
},
usePointStyle: true,
pointStyle: 'circle'
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: 'rgba(227, 245, 20, 0.5)',
borderWidth: 1,
padding: 12,
displayColors: true,
callbacks: {
label: function (context) {
const label = context.label || '';
return ' ' + label;
}
}
}
},
animation: {
animateRotate: true,
animateScale: true,
duration: 1000,
easing: 'easeOutQuart'
}
}
});
// Update Text
if (isFake) {
analysisText.innerHTML = `
<strong style="color: var(--accent-yellow)">⚠️ High Risk Detected</strong><br>
The model identified synthetic artifacts consistent with GAN or Diffusion generation.
Anomalies found in texture patterns and noise distribution.
`;
} else {
analysisText.innerHTML = `
<strong>✓ Authentic Media</strong><br>
No significant digital manipulation markers found.
Natural noise patterns and consistent lighting observed.
`;
}
const downloadBtn = document.getElementById('downloadReportBtn');
if (downloadBtn) downloadBtn.style.display = 'block';
// Show feedback section and reset buttons
const feedbackSection = document.getElementById('feedbackSection');
const feedbackMessage = document.getElementById('feedbackMessage');
const btnCorrect = document.getElementById('btnFeedbackCorrect');
const btnWrong = document.getElementById('btnFeedbackWrong');
if (feedbackSection) {
feedbackSection.style.display = 'block';
// Reset buttons to enabled state
if (btnCorrect) btnCorrect.disabled = false;
if (btnWrong) btnWrong.disabled = false;
// Hide any previous messages
if (feedbackMessage) feedbackMessage.style.display = 'none';
}
}
function resetAnalysis() {
document.getElementById('fileInput').value = '';
document.getElementById('previewArea').style.display = 'none';
const downloadBtn = document.getElementById('downloadReportBtn');
if (downloadBtn) downloadBtn.style.display = 'none';
document.getElementById('uploadArea').style.display = 'flex';
document.querySelector('.analysis-results').style.display = 'none';
document.querySelector('.empty-state').style.display = 'block';
const scanTimeDisplay = document.getElementById('scanTimeDisplay');
if (scanTimeDisplay) scanTimeDisplay.textContent = '--';
const loader = document.getElementById('analysisLoader');
if (loader) loader.style.display = 'none';
}
// ==================== FEEDBACK SUBMISSION ====================
async function submitFeedback(isCorrect) {
if (!currentScanId) {
showToast('No scan ID available. Please analyze an image first.', 'error');
return;
}
const btnCorrect = document.getElementById('btnFeedbackCorrect');
const btnWrong = document.getElementById('btnFeedbackWrong');
const feedbackMessage = document.getElementById('feedbackMessage');
const verdictTitle = document.getElementById('verdictTitle');
// Get the predicted label from the verdict
const predictedLabel = verdictTitle.textContent.includes('FAKE') ? 'FAKE' : 'REAL';
// Disable buttons to prevent duplicate submissions
if (btnCorrect) btnCorrect.disabled = true;
if (btnWrong) btnWrong.disabled = true;
try {
const response = await fetch(`${API_BASE_URL}/api/feedback`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Session-ID': getSessionId()
},
body: JSON.stringify({
scan_id: currentScanId,
is_correct: isCorrect,
predicted_label: predictedLabel
})
});
const result = await response.json();
if (response.ok) {
// Show success message
if (feedbackMessage) {
feedbackMessage.className = 'feedback-message success';
if (isCorrect) {
feedbackMessage.innerHTML = `✅ Thank you! Your feedback confirms this prediction was correct.`;
} else {
feedbackMessage.innerHTML = `✅ Thank you for your feedback!`;
}
feedbackMessage.style.display = 'block';
}
showToast(`Feedback submitted successfully!`, 'success');
} else {
throw new Error(result.error || 'Failed to submit feedback');
}
} catch (error) {
console.error('Error submitting feedback:', error);
// Re-enable buttons on error
if (btnCorrect) btnCorrect.disabled = false;
if (btnWrong) btnWrong.disabled = false;
// Show error message
if (feedbackMessage) {
feedbackMessage.className = 'feedback-message error';
feedbackMessage.innerHTML = `❌ Failed to submit feedback. Please try again.`;
feedbackMessage.style.display = 'block';
}
showToast('Failed to submit feedback', 'error');
}
}
// CTA button handlers
// CTA button handlers
// document.querySelectorAll('.btn-hero-primary, .btn-cta-primary').forEach(btn => {
// btn.addEventListener('click', () => {
// document.getElementById('demo').scrollIntoView({ behavior: 'smooth' });
// });
// });
// Watch demo button
// Watch demo button
// document.querySelectorAll('.btn-hero-secondary').forEach(btn => {
// btn.addEventListener('click', () => {
// alert('Demo video coming soon! For now, try our live detection below.');
// document.getElementById('demo').scrollIntoView({ behavior: 'smooth' });
// });
// });
// ==================== 3D FLOATING EFFECTS ====================
document.addEventListener('mousemove', (e) => {
const floaters = document.querySelectorAll('.floating-element, .floating-bg-icon');
const x = e.clientX / window.innerWidth;
const y = e.clientY / window.innerHeight;
floaters.forEach((el, index) => {
const speed = (index + 1) * 20;
el.style.transform = `translate(${x * speed}px, ${y * speed}px)`;
});
});
// Add hover effect to showcase items
document.querySelectorAll('.showcase-item').forEach(item => {
item.addEventListener('mouseenter', function () {
this.style.transform = 'scale(1.05)';
});
item.addEventListener('mouseleave', function () {
this.style.transform = 'scale(1)';
});
});
// Typing effect for hero title (optional enhancement)
function typeWriterEffect(element, text, speed = 50) {
let i = 0;
element.textContent = '';
function type() {
if (i < text.length) {
element.textContent += text.charAt(i);
i++;
setTimeout(type, speed);
}
}
type();
}
// Add wave animation to stats
document.querySelectorAll('.stat-value').forEach((stat, index) => {
stat.style.animationDelay = `${index * 0.1}s`;
});
// Get Started button functionality
// Get Started button functionality
// document.querySelectorAll('.btn-primary').forEach(btn => {
// if (btn.textContent === 'Get Started') {
// btn.addEventListener('click', () => {
// window.location.href = '#demo';
// });
// }
// });
// Loading animation for stats counter
function animateValue(element, start, end, duration) {
let startTimestamp = null;
const step = (timestamp) => {
if (!startTimestamp) startTimestamp = timestamp;
const progress = Math.min((timestamp - startTimestamp) / duration, 1);
const value = Math.floor(progress * (end - start) + start);
element.textContent = value + (element.dataset.suffix || '');
if (progress < 1) {
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
}
// Observe hero stats for counter animation
const statsObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const statValue = entry.target.querySelector('.stat-value');
if (statValue && !statValue.classList.contains('animated')) {
statValue.classList.add('animated');
// Trigger animation based on content
const text = statValue.textContent;
if (text.includes('%')) {
animateValue(statValue, 0, 99, 2000);
statValue.dataset.suffix = '%';
}
}
}
});
}, { threshold: 0.5 });
document.querySelectorAll('.stat-item').forEach(stat => {
statsObserver.observe(stat);
});
console.log('🚀 Modern Detections System loaded successfully!');
// ==================== PDF REPORT GENERATION ====================
// ==================== PDF REPORT GENERATION ====================
async function generatePDFReport(historyItem = null) {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const margin = 20;
// -- Colors --
const colorBlack = [10, 10, 15]; // #0A0A0F
const colorYellow = [227, 245, 20]; // #E3F514
const colorGray = [128, 128, 128];
const colorRed = [220, 38, 38];
const colorGreen = [22, 163, 74];
const colorWhite = [255, 255, 255];
// -- Background --
doc.setFillColor(...colorBlack);
doc.rect(0, 0, pageWidth, pageHeight, 'F');
// -- Logo & Header --
try {
const logoImg = await loadImage('logo.ico');
const canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
const ctx = canvas.getContext('2d');
ctx.drawImage(logoImg, 0, 0, 100, 100);
const logoData = canvas.toDataURL('image/png');
doc.addImage(logoData, 'PNG', margin, margin, 15, 15);
doc.setFontSize(22);
doc.setTextColor(...colorYellow);
doc.setFont('helvetica', 'bold');
doc.text('DeepGuard', margin + 20, margin + 11);
} catch (e) {
console.warn("Logo load failed", e);
doc.setFontSize(22);
doc.setTextColor(...colorYellow);
doc.setFont('helvetica', 'bold');
doc.text('DeepGuard', margin, margin + 10);
}
// -- Report Title --
doc.setDrawColor(...colorYellow);
doc.setLineWidth(0.5);
doc.line(margin, margin + 30, pageWidth - margin, margin + 30);
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text('FORENSIC ANALYSIS REPORT', margin, margin + 45);
// -- Scan Details --
const verdictTitle = document.getElementById('verdictTitle').textContent;
const confidenceVal = document.getElementById('confidenceValue').textContent;
const scanTime = document.getElementById('scanTimeDisplay').textContent || '< 2s';
const timestamp = new Date().toLocaleString();
let verdictColor = verdictTitle.includes('FAKE') ? colorRed : colorGreen;
// Verdict Box
doc.setFillColor(20, 20, 25);
doc.setDrawColor(60, 60, 60);
doc.roundedRect(margin, margin + 55, pageWidth - (margin * 2), 35, 3, 3, 'FD');
doc.setFontSize(10);
doc.setTextColor(...colorGray);
doc.text('DETECTION VERDICT', margin + 10, margin + 70);
doc.setFontSize(24);
doc.setFont('helvetica', 'bold');
doc.setTextColor(...verdictColor);
doc.text(verdictTitle, margin + 10, margin + 83);
doc.setFontSize(12);
doc.setTextColor(...colorWhite);
doc.text(`Confidence: ${confidenceVal}`, pageWidth - margin - 60, margin + 83);
// -- Comparison Images --
const previewImg = document.getElementById('previewImage');
const heatmapImg = document.getElementById('heatmapOverlay');
let yPos = margin + 105;
// Helper to get image data safely
const getImageData = (imgElement) => {
if (!imgElement || !imgElement.src || imgElement.src === window.location.href) return null;
if (imgElement.src.startsWith('data:')) return imgElement.src;
try {
const c = document.createElement('canvas');
c.width = imgElement.naturalWidth;
c.height = imgElement.naturalHeight;
c.getContext('2d').drawImage(imgElement, 0, 0);
return c.toDataURL('image/jpeg', 0.8);
} catch (e) {
return null;
}
};
if (previewImg && previewImg.src && previewImg.naturalWidth > 0) {
doc.setFontSize(12);
doc.setTextColor(...colorYellow);
doc.text('Analyzed Media', margin, yPos);
yPos += 10;
const imgRatio = previewImg.naturalHeight / previewImg.naturalWidth;
// Max width for one image (if 2 side by side)
const maxImgWidth = (pageWidth - (margin * 3)) / 2;
const imgHeight = Math.min(maxImgWidth * imgRatio, 80); // Limit height
const imgWidth = imgHeight / imgRatio;
try {
const originalData = getImageData(previewImg);
if (originalData) {
doc.addImage(originalData, 'JPEG', margin, yPos, imgWidth, imgHeight);
doc.setFontSize(8);
doc.setTextColor(...colorGray);
doc.text('Original Input', margin, yPos + imgHeight + 5);
}
// Heatmap (only if visible/exists)
if (heatmapImg && heatmapImg.src && heatmapImg.style.display !== 'none' && heatmapImg.naturalWidth > 0) {
const heatmapData = getImageData(heatmapImg);
if (heatmapData) {
doc.addImage(heatmapData, 'JPEG', margin + imgWidth + 10, yPos, imgWidth, imgHeight);
doc.setTextColor(...colorGray);
doc.text('Heatmap Analysis', margin + imgWidth + 10, yPos + imgHeight + 5);
}
}
yPos += imgHeight + 20;
} catch (e) {
console.error("PDF Image Error", e);
}
}
// -- Metadata Table (Manual Layout) --
yPos += 10;
const tableData = [
['Analysis ID', `SCAN-${Date.now().toString().slice(-6)}`],
['Date & Time', timestamp],
['Model Engine', 'DeepGuard Mark V (CNN+ViT)'],
['Scan Duration', scanTime],
['Status', 'Completed Successfully']
];
doc.setDrawColor(...colorGray);
doc.setLineWidth(0.1);
doc.setFontSize(10);
tableData.forEach(([label, value]) => {
doc.setFillColor(30, 30, 35);
doc.rect(margin, yPos, 60, 10, 'F'); // Label bg
doc.setFillColor(20, 20, 25);
doc.rect(margin + 60, yPos, pageWidth - (margin * 2) - 60, 10, 'F'); // Value bg
doc.setTextColor(...colorYellow);
doc.setFont('helvetica', 'bold');
doc.text(label, margin + 5, yPos + 7);
doc.setTextColor(...colorWhite);
doc.setFont('helvetica', 'normal');
doc.text(value, margin + 65, yPos + 7);
yPos += 11;
});
// -- Footer --
doc.setFontSize(8);
doc.setTextColor(...colorGray);
doc.text('Generated by DeepGuard AI System. Authenticity verified by cryptographic signature.', margin, pageHeight - 15);
// Save
doc.save(`DeepGuard_Report_${Date.now()}.pdf`);
}
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}
// ==================== HISTORY LOGIC ====================
async function loadHistory() {
const historyList = document.getElementById('historyList');
const emptyState = document.getElementById('historyEmptyState');
if (!historyList) return;
try {
const response = await fetch(`${API_BASE_URL}/api/history`, {
headers: {
'X-Session-ID': getSessionId()
}
});
const history = await response.json();
if (history.length > 0) {
emptyState.style.display = 'none';
historyList.innerHTML = '';
history.forEach((item, index) => {
const isFake = item.prediction === 'FAKE';
const date = new Date(item.timestamp).toLocaleString();
const card = document.createElement('div');
card.className = 'history-card';
card.setAttribute('data-scan-id', item.id);
card.style.animationDelay = `${index * 0.1}s`;
card.innerHTML = `
<div class="history-card-header">
<div class="history-badge ${isFake ? 'badge-fake' : 'badge-real'}">
${isFake ? '⚠ FAKE' : '✓ REAL'}
</div>
<span class="history-date">${date}</span>
</div>
<div class="history-card-body">
<h4>${item.filename}</h4>
<div class="history-prob">
<span>Confidence: ${(item.confidence * 100).toFixed(1)}%</span>
<div class="mini-bar">
<div class="mini-fill" style="width: ${item.confidence * 100}%"></div>
</div>
</div>
</div>
`;
// Create button container
const btnContainer = document.createElement('div');
btnContainer.style.display = 'flex';
btnContainer.style.gap = '10px';
btnContainer.style.marginTop = '15px';
// Download button
const downloadBtn = document.createElement('button');
downloadBtn.className = 'btn-history-download';
downloadBtn.innerHTML = '📄 Download Report';
downloadBtn.onclick = () => generateHistoryPDF(item);
// Delete button
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn-history-delete';
deleteBtn.innerHTML = '🗑 Delete';
deleteBtn.onclick = (e) => deleteScan(item.id, e);
btnContainer.appendChild(downloadBtn);
btnContainer.appendChild(deleteBtn);
card.querySelector('.history-card-body').appendChild(btnContainer);
historyList.appendChild(card);
});
// Remove the global clear button since we have individual delete buttons now
} else {
emptyState.style.display = 'flex';
historyList.innerHTML = '';
}
} catch (err) {
console.error('Failed to load history:', err);
}
}
async function generateHistoryPDF(item) {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
// Background
doc.setFillColor(17, 17, 17);
doc.rect(0, 0, 210, 297, 'F');
// Header Band
doc.setFillColor(227, 245, 20);
doc.rect(0, 0, 210, 40, 'F');
// Header Text
doc.setTextColor(0, 0, 0);
doc.setFontSize(24);
doc.setFont('helvetica', 'bold');
doc.text("DeepGuard Forensic Report", 105, 25, { align: "center" });
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
doc.text("ARCHIVED SCAN RECORD", 105, 33, { align: "center" });
let y = 55;
const leftMargin = 25;
// Add Image if available
if (item.image_path) {
try {
console.log('Loading image from:', item.image_path);
const img = new Image();
img.crossOrigin = 'anonymous';
await new Promise((resolve, reject) => {
img.onload = () => {
console.log('Image loaded successfully');
// Calculate dimensions to fit in PDF
const maxWidth = 160;
const maxHeight = 100;
let width = img.width;
let height = img.height;
const ratio = Math.min(maxWidth / width, maxHeight / height);
width = width * ratio;
height = height * ratio;
// Center the image
const xPos = (210 - width) / 2;
doc.addImage(img, 'JPEG', xPos, y, width, height);
y += height + 15;
resolve();
};
img.onerror = (err) => {
console.error('Could not load image for PDF:', err);
console.error('Image path was:', item.image_path);
resolve(); // Continue without image
};
// Use absolute path from root
img.src = '/' + item.image_path;
});
} catch (err) {
console.error('Error adding image to PDF:', err);
}
} else {
console.warn('No image_path found in item:', item);
}
// Title Section
doc.setTextColor(227, 245, 20);
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text("SCAN DETAILS", leftMargin, y);
y += 10;
// Info Grid
doc.setTextColor(255, 255, 255);
doc.setFontSize(12);
doc.setFont('helvetica', 'normal');
const addField = (label, value) => {
doc.setTextColor(150, 150, 150);
doc.text(label, leftMargin, y);
doc.setTextColor(255, 255, 255);
doc.text(value, leftMargin + 50, y);
y += 12;
};
addField("Filename:", item.filename);
addField("Date:", new Date(item.timestamp).toLocaleString());
addField("Prediction:", item.prediction);
addField("Confidence:", `${(item.confidence * 100).toFixed(1)}%`);
y += 10;
// Probabilities Section
doc.setTextColor(227, 245, 20);
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text("MODEL ANALYSIS", leftMargin, y);
y += 10;
const fakeProb = item.fake_probability ? (item.fake_probability * 100).toFixed(1) + '%' : 'N/A';
const realProb = item.real_probability ? (item.real_probability * 100).toFixed(1) + '%' : 'N/A';
doc.setTextColor(255, 255, 255);
doc.setFontSize(12);
doc.setFont('helvetica', 'normal');
addField("Fake Probability:", fakeProb);
addField("Real Probability:", realProb);
// Footer
doc.setFontSize(9);
doc.setTextColor(80, 80, 80);
doc.text("Generated automatically by DeepGuard AI System", 105, 280, { align: "center" });
doc.text(`ID: ${item.id}`, 105, 285, { align: "center" });
doc.save(`DeepGuard_Report_${item.filename}.pdf`);
}
async function deleteScan(scanId, event) {
// Find the card element by data attribute
const targetCard = document.querySelector(`[data-scan-id="${scanId}"]`);
if (targetCard) {
// Smooth fade out
targetCard.style.transition = 'all 0.3s ease';
targetCard.style.opacity = '0';
targetCard.style.transform = 'scale(0.8)';
// Wait for animation
await new Promise(resolve => setTimeout(resolve, 300));
}
try {
const response = await fetch(`${API_BASE_URL}/api/history/${scanId}`, {
method: 'DELETE',
headers: {
'X-Session-ID': getSessionId()
}
});
if (response.ok) {
// Remove the card from DOM directly instead of reloading
if (targetCard) {
targetCard.remove();
}
// Check if history is empty now
const historyList = document.getElementById('historyList');
const remainingCards = historyList.querySelectorAll('.history-card');
if (remainingCards.length === 0) {
const emptyState = document.getElementById('historyEmptyState');
if (emptyState) {
emptyState.style.display = 'flex';
}
}
} else {
console.error('Failed to delete scan');
if (targetCard) {
// Restore if failed
targetCard.style.opacity = '1';
targetCard.style.transform = 'scale(1)';
}
}
} catch (err) {
console.error('Error deleting scan:', err);
if (targetCard) {
// Restore if failed
targetCard.style.opacity = '1';
targetCard.style.transform = 'scale(1)';
}
}
}
async function clearHistory() {
// Confirmation removed as per user request
try {
await fetch(`${API_BASE_URL}/api/history`, {
method: 'DELETE',
headers: {
'X-Session-ID': getSessionId()
}
});
loadHistory(); // Reload UI
} catch (err) {
console.error('Failed to clear history:', err);
}
}
// Auto-load history on history.html
if (window.location.pathname.includes('history.html')) {
window.addEventListener('load', loadHistory);
}
// ==================== MULTI-FILE UPLOAD SYSTEM ====================
let currentScanId = null; // Track the latest scan ID for feedback
let uploadQueue = [];
let isProcessingQueue = false;
let currentUploadIndex = 0;
// For analysis page only
if (fileInput && uploadArea) {
// Enhanced drag and drop
// Drag & Drop Visuals - Stronger cues
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('drag-over');
const fileCount = e.dataTransfer.items.length;
const badge = document.getElementById('fileCountBadge');
if (badge) {
badge.textContent = `${fileCount} file${fileCount > 1 ? 's' : ''} ready to drop`;
badge.style.display = 'block';
}
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('drag-over');
const badge = document.getElementById('fileCountBadge');
if (badge) badge.style.display = 'none';
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('drag-over');
const badge = document.getElementById('fileCountBadge');
if (badge) badge.style.display = 'none';
const rawFiles = Array.from(e.dataTransfer.files);
// Filter: Must be Image/Video AND under Size Limit
const validFiles = rawFiles.filter(f => {
const isMedia = f.type.startsWith('image/') || f.type.startsWith('video/');
if (!isMedia) {
showToast(`Skipped "${f.name}": Not an image or video.`, 'warning');
return false;
}
return validateFile(f);
});
if (validFiles.length > 0) {
addFilesToQueue(validFiles);
showToast(`${validFiles.length} file(s) added to queue`, 'success');
} // Warnings handled via Toast in loop
});
// File Input Change
fileInput.addEventListener('change', (e) => {
const rawFiles = Array.from(e.target.files);
const validFiles = rawFiles.filter(validateFile);
if (validFiles.length > 0) {
addFilesToQueue(validFiles);
showToast(`${validFiles.length} file(s) added to queue`, 'success');
}
fileInput.value = ''; // Reset
});
// Paste event listener for copy-paste functionality
document.addEventListener('paste', (e) => {
// Only handle paste if we're on the analysis page
if (!uploadArea) return;
const items = e.clipboardData?.items;
if (!items) return;
const pastedFiles = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
// Check if the clipboard item is an image
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
if (file) {
pastedFiles.push(file);
}
}
}
if (pastedFiles.length > 0) {
e.preventDefault(); // Prevent default paste behavior
const validFiles = pastedFiles.filter(validateFile);
if (validFiles.length > 0) {
addFilesToQueue(validFiles);
showToast(`${validFiles.length} image(s) pasted successfully`, 'success');
}
}
});
}
function addFilesToQueue(files) {
// Hide upload area, show queue
uploadArea.style.display = 'none';
document.getElementById('fileQueueContainer').style.display = 'block';
files.forEach(file => {
const fileObj = {
file: file,
id: Date.now() + Math.random(),
status: 'pending', // pending, uploading, completed, error
progress: 0,
result: null
};
uploadQueue.push(fileObj);
renderFileQueueItem(fileObj);
});
updateQueueCount();
}
function renderFileQueueItem(fileObj) {
const queue = document.getElementById('fileQueue');
const item = document.createElement('div');
item.className = 'file-queue-item';
item.id = `file-${fileObj.id}`;
const sizeKB = (fileObj.file.size / 1024).toFixed(1);
const sizeDisplay = sizeKB > 1024 ? `${(sizeKB / 1024).toFixed(1)} MB` : `${sizeKB} KB`;
item.innerHTML = `
<div class="file-icon">📷</div>
<div class="file-info">
<div class="file-name">${fileObj.file.name}</div>
<div class="file-size">${sizeDisplay}</div>
</div>
<div class="file-status pending">Pending</div>
<button class="file-remove" onclick="removeFromQueue('${fileObj.id}')">×</button>
`;
queue.appendChild(item);
}
function removeFromQueue(fileId) {
uploadQueue = uploadQueue.filter(f => f.id != fileId);
const item = document.getElementById(`file-${fileId}`);
if (item) {
item.style.opacity = '0';
item.style.transform = 'translateX(-20px)';
setTimeout(() => {
item.remove();
// Check if queue is empty AFTER removal animation
if (uploadQueue.length === 0) {
clearQueue();
}
}, 300);
}
updateQueueCount();
}
function clearQueue() {
uploadQueue = [];
document.getElementById('fileQueue').innerHTML = '';
document.getElementById('fileQueueContainer').style.display = 'none';
uploadArea.style.display = 'flex';
fileInput.value = '';
updateQueueCount();
}
function updateQueueCount() {
document.getElementById('queueCount').textContent = uploadQueue.length;
}
// ==================== PROCESSING OVERLAY HELPERS ====================
// ==================== ENHANCED MULTI-STAGE PROCESSING OVERLAY ====================
let processingTimerInterval;
let warmupTimerTimeout;
let currentProcessingStep = null;
/**
* Check model health before starting analysis
*/
async function checkModelHealth() {
try {
const response = await fetch(`${API_BASE_URL}/api/health`, {
method: 'GET',
timeout: 5000
});
if (!response.ok) {
return { status: 'error', model_status: 'unavailable' };
}
const data = await response.json();
return data;
} catch (error) {
console.error('Health check failed:', error);
return { status: 'error', model_status: 'unavailable' };
}
}
/**
* Update the active processing step
*/
function setProcessingStep(step) {
currentProcessingStep = step;
const steps = document.querySelectorAll('.progress-step');
steps.forEach(stepEl => {
const stepName = stepEl.getAttribute('data-step');
stepEl.classList.remove('active', 'completed');
// Mark as completed if before current step
const stepOrder = ['upload', 'connect', 'warmup', 'analyze', 'generate'];
const currentIndex = stepOrder.indexOf(step);
const thisIndex = stepOrder.indexOf(stepName);
if (thisIndex < currentIndex) {
stepEl.classList.add('completed');
} else if (thisIndex === currentIndex) {
stepEl.classList.add('active');
}
});
}
/**
* Show the enhanced multi-stage processing overlay
*/
function showProcessingOverlay(isVideo = false) {
const overlay = document.getElementById('processingOverlay');
const modelStatusBadge = document.getElementById('modelStatusBadge');
const modelStatusText = document.getElementById('modelStatusText');
const processingMainTitle = document.getElementById('processingMainTitle');
const processingMessage = document.getElementById('processingMessage');
const warmupAlert = document.getElementById('warmupAlert');
if (!overlay) return;
// Reset state
overlay.style.display = 'flex';
if (warmupAlert) warmupAlert.style.display = 'none';
// Set initial step
setProcessingStep('upload');
// Update title
if (processingMainTitle) {
processingMainTitle.textContent = isVideo ? 'Analyzing Video' : 'Analyzing Media';
}
// Show model status badge
if (modelStatusBadge) {
modelStatusBadge.style.display = 'inline-flex';
if (modelStatusText) {
modelStatusText.textContent = 'Checking...';
}
}
// Set warm-up detection timer (7 seconds)
clearTimeout(warmupTimerTimeout);
warmupTimerTimeout = setTimeout(() => {
if (warmupAlert && overlay.style.display === 'flex') {
warmupAlert.style.display = 'flex';
if (processingMessage) {
processingMessage.textContent = 'Model is initializing...';
}
// Start Progress Bar Animation
let progress = 0;
const bar = document.getElementById('warmupProgressFill');
const label = document.getElementById('warmupPercent');
if (bar && label) {
bar.style.width = '0%';
label.textContent = '0%';
if (window.warmupProgressInterval) clearInterval(window.warmupProgressInterval);
window.warmupProgressInterval = setInterval(() => {
progress += Math.random() * 1.5; // Slow random increment
if (progress > 95) progress = 95; // Cap at 95% until actual completion
bar.style.width = `${progress}%`;
label.textContent = `${Math.round(progress)}%`;
}, 400);
}
}
}, 5000); // Show warm-up alert after 5 seconds (reduced from 7s for better feedback)
}
/**
* Hide the processing overlay with smooth transition
*/
function hideProcessingOverlay() {
const overlay = document.getElementById('processingOverlay');
const processingMainTitle = document.getElementById('processingMainTitle');
const processingMessage = document.getElementById('processingMessage');
if (!overlay) return;
// Clear timers
// Clear timers
clearTimeout(warmupTimerTimeout);
if (window.warmupProgressInterval) clearInterval(window.warmupProgressInterval);
clearInterval(processingTimerInterval);
// Show completion
setProcessingStep('generate');
if (processingMainTitle) {
processingMainTitle.textContent = 'Analysis Complete';
}
if (processingMessage) {
processingMessage.textContent = 'Reviewing AI signals...';
}
// Mark all steps as completed
setTimeout(() => {
document.querySelectorAll('.progress-step').forEach(step => {
step.classList.remove('active');
step.classList.add('completed');
});
}, 200);
// Hide overlay after brief display
setTimeout(() => {
overlay.style.display = 'none';
}, 800);
}
async function processUploadQueue() {
if (isProcessingQueue || uploadQueue.length === 0) return;
isProcessingQueue = true;
const startBtn = document.getElementById('startUploadBtn');
startBtn.disabled = true;
startBtn.textContent = 'Processing...';
playSound('scan');
for (let i = 0; i < uploadQueue.length; i++) {
const fileObj = uploadQueue[i];
if (fileObj.status === 'completed') continue;
await uploadSingleFile(fileObj);
}
isProcessingQueue = false;
startBtn.disabled = false;
startBtn.textContent = 'Analysis Complete';
playSound('success');
// Refresh statistics and recent
await loadStatisticsAndRecent();
// Show success message
// Show success message and potentially redirect
setTimeout(() => {
const isVideoFile = (file) => {
const videoExtensions = ['.mp4', '.avi', '.mov', '.webm', '.mkv'];
return file.type.startsWith('video/') || videoExtensions.some(ext => file.name.toLowerCase().endsWith(ext));
};
const completedFiles = uploadQueue.filter(f => f.status === 'completed');
const videoFiles = completedFiles.filter(f => isVideoFile(f.file));
const imageFiles = completedFiles.filter(f => !isVideoFile(f.file));
// Case 1: Single Video -> Redirect
if (videoFiles.length === 1 && uploadQueue.length === 1) {
window.location.href = 'video_result.html';
return;
}
// Case 2: Single Image -> Show Analysis Result on Page
if (imageFiles.length === 1 && uploadQueue.length === 1) {
const fileObj = imageFiles[0];
// Render Preview
const reader = new FileReader();
reader.onload = (e) => {
const previewImg = document.getElementById('previewImage');
if (previewImg) previewImg.src = e.target.result;
// UI Transitions
const queueContainer = document.getElementById('fileQueueContainer');
const previewArea = document.getElementById('previewArea');
const analysisResults = document.querySelector('.analysis-results');
const emptyState = document.querySelector('.empty-state');
if (queueContainer) queueContainer.style.display = 'none';
if (previewArea) previewArea.style.display = 'block';
if (analysisResults) analysisResults.style.display = 'block';
if (emptyState) emptyState.style.display = 'none';
// Handle Heatmap
if (fileObj.result.heatmap) {
const heatmapOverlay = document.getElementById('heatmapOverlay');
const heatmapToggle = document.getElementById('heatmapToggle');
const heatmapSwitch = document.getElementById('heatmapSwitch');
if (heatmapOverlay) {
heatmapOverlay.src = `data:image/jpeg;base64,${fileObj.result.heatmap}`;
heatmapOverlay.style.display = 'block';
heatmapOverlay.style.opacity = '0'; // Start hidden
}
if (heatmapToggle) heatmapToggle.style.display = 'flex';
if (heatmapSwitch) {
heatmapSwitch.checked = false;
heatmapSwitch.onchange = (e) => {
if (heatmapOverlay) heatmapOverlay.style.opacity = e.target.checked ? '1' : '0';
};
}
}
// Store scan_id for feedback
if (fileObj.result.scan_id) {
currentScanId = fileObj.result.scan_id;
console.log('Scan ID stored from queue:', currentScanId);
}
// Populate Data
updateAnalysisUI(fileObj.result);
};
reader.readAsDataURL(fileObj.file);
return; // Don't clear queue
}
// Case 3: Multiple/Mixed/Errors -> Standard Alert & Clear
// alert(`All files processed! ${completedFiles.length}/${uploadQueue.length} succeeded.`);
showToast(`All files processed! ${completedFiles.length}/${uploadQueue.length} succeeded.`, 'success');
if (uploadQueue.some(f => f.status !== 'completed')) {
// Keep queue if errors exist
} else {
clearQueue();
}
}, 1000);
}
async function uploadSingleFile(fileObj) {
const item = document.getElementById(`file-${fileObj.id}`);
if (!item) return;
// Update status
fileObj.status = 'uploading';
item.classList.add('uploading');
const statusEl = item.querySelector('.file-status');
statusEl.className = 'file-status uploading';
statusEl.textContent = 'Uploading...';
// Add progress bar
const fileInfo = item.querySelector('.file-info');
if (!fileInfo.querySelector('.progress-container')) {
const progressHTML = `
<div class="progress-container">
<div class="progress-bar-wrapper">
<div class="progress-bar-fill" style="width: 0%" id="progress-${fileObj.id}"></div>
</div>
<div class="progress-details">
<span id="progress-percent-${fileObj.id}">0%</span>
<span id="progress-status-${fileObj.id}">Starting...</span>
</div>
</div>
`;
fileInfo.insertAdjacentHTML('beforeend', progressHTML);
}
try {
const formData = new FormData();
formData.append('file', fileObj.file);
const startTime = Date.now();
// Use XMLHttpRequest for progress tracking
const result = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
const progressBar = document.getElementById(`progress-${fileObj.id}`);
const progressPercent = document.getElementById(`progress-percent-${fileObj.id}`);
const progressStatus = document.getElementById(`progress-status-${fileObj.id}`);
if (progressBar) progressBar.style.width = `${percent}%`;
if (progressPercent) progressPercent.textContent = `${percent}%`;
const elapsed = (Date.now() - startTime) / 1000;
const speed = e.loaded / elapsed / 1024; // KB/s
if (progressStatus) {
progressStatus.textContent = `${speed.toFixed(1)} KB/s`;
}
// Trigger Processing Overlay when upload completes
if (percent >= 100) {
const videoExtensions = ['.mp4', '.avi', '.mov', '.webm', '.mkv'];
const isVideo = fileObj.file.type.startsWith('video/') || videoExtensions.some(ext => fileObj.file.name.toLowerCase().endsWith(ext));
showProcessingOverlay(isVideo);
}
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
hideProcessingOverlay();
resolve(JSON.parse(xhr.responseText));
} else {
hideProcessingOverlay();
reject(new Error('Upload failed'));
}
});
xhr.addEventListener('error', () => {
hideProcessingOverlay();
reject(new Error('Network error'));
});
// Determine endpoint based on file type
const videoExtensions = ['.mp4', '.avi', '.mov', '.webm', '.mkv'];
const isVideo = fileObj.file.type.startsWith('video/') || videoExtensions.some(ext => fileObj.file.name.toLowerCase().endsWith(ext));
const endpoint = isVideo ? '/api/predict_video' : '/api/predict';
xhr.open('POST', `${API_BASE_URL}${endpoint}`);
xhr.send(formData);
});
// Success
fileObj.status = 'completed';
fileObj.result = result;
item.classList.remove('uploading');
item.classList.add('completed');
statusEl.className = 'file-status completed';
statusEl.textContent = '✓ Complete';
const progressStatus = document.getElementById(`progress-status-${fileObj.id}`);
if (progressStatus) progressStatus.textContent = 'Done';
// Specific handling for video results
// Re-check logic to be safe
const videoExtensions = ['.mp4', '.avi', '.mov', '.webm', '.mkv'];
const isVideo = fileObj.file.type.startsWith('video/') || videoExtensions.some(ext => fileObj.file.name.toLowerCase().endsWith(ext));
if (isVideo) {
// Save to localStorage so video_result.html can pick it up
localStorage.setItem('video_analysis_result', JSON.stringify(result));
// Add a "View Analysis" button
const viewBtn = document.createElement('button');
viewBtn.className = 'btn-secondary-small';
viewBtn.style.marginTop = '8px';
viewBtn.innerHTML = '▶ View Video Analysis';
viewBtn.onclick = () => window.location.href = 'video_result.html';
item.appendChild(viewBtn);
}
} catch (error) {
// Error
fileObj.status = 'error';
item.classList.remove('uploading');
item.classList.add('error');
statusEl.className = 'file-status error';
statusEl.textContent = '✖ Failed';
const progressStatus = document.getElementById(`progress-status-${fileObj.id}`);
if (progressStatus) progressStatus.textContent = error.message;
console.error('Upload error:', error);
}
}
// ==================== STATISTICS AND RECENT ANALYSES ====================
async function loadStatisticsAndRecent() {
try {
const response = await fetch(`${API_BASE_URL}/api/history`);
const history = await response.json();
// Calculate statistics
const total = history.length;
const fake = history.filter(h => h.prediction === 'FAKE').length;
const real = history.filter(h => h.prediction === 'REAL').length;
const avgConf = total > 0
? (history.reduce((sum, h) => sum + h.confidence, 0) / total * 100).toFixed(1)
: 0;
// Update statistics
document.getElementById('totalScans').textContent = total;
document.getElementById('fakeCount').textContent = fake;
document.getElementById('realCount').textContent = real;
document.getElementById('avgConfidence').textContent = `${avgConf}%`;
// Display recent 6
const recent = history.slice(0, 6);
const recentGrid = document.getElementById('recentGrid');
if (recent.length === 0) {
recentGrid.innerHTML = `
<div class="recent-grid-empty">
<div class="recent-grid-empty-icon">📂</div>
<p>No analyses yet. Upload images to get started!</p>
</div>
`;
} else {
recentGrid.innerHTML = recent.map(item => `
<div class="recent-card" onclick="window.location.href='history.html'">
${item.image_path ?
`<img src="${API_BASE_URL}/${item.image_path}" alt="${item.filename}" class="recent-card-image" onerror="this.outerHTML='<div class=\'recent-card-image\' style=\'background: #222; display: flex; align-items: center; justify-content: center; font-size: 24px;\'>📷</div>'">`
: '<div class="recent-card-image" style="background: #222; display: flex; align-items: center; justify-content: center; font-size: 24px;">📷</div>'}
<div class="recent-card-content">
<div class="recent-card-header">
<div class="recent-badge ${item.prediction === 'FAKE' ? 'fake' : 'real'}">
${item.prediction === 'FAKE' ? '⚠ FAKE' : '✓ REAL'}
</div>
</div>
<div class="recent-card-title">${item.filename}</div>
<div class="recent-date">${new Date(item.timestamp).toLocaleDateString()}</div>
<div class="recent-confidence">
<div class="recent-confidence-label">Confidence: ${(item.confidence * 100).toFixed(1)}%</div>
<div class="recent-confidence-bar">
<div class="recent-confidence-fill" style="width: ${item.confidence * 100}%"></div>
</div>
</div>
</div>
</div>
`).join('');
}
} catch (error) {
console.error('Failed to load statistics and recent:', error);
}
}
// Load statistics on analysis page load
if (window.location.pathname.includes('analysis.html')) {
window.addEventListener('load', loadStatisticsAndRecent);
}
// ==================== ENHANCED HISTORY PAGE FUNCTIONALITY ====================
let fullHistoryData = [];
let filteredHistoryData = [];
let currentSortColumn = 'timestamp';
let currentSortOrder = 'desc';
let currentPage = 1;
const itemsPerPage = 8;
let currentView = 'list';
let selectedIds = new Set();
// Load and render history for the enhanced history page
async function loadEnhancedHistory() {
const tableBody = document.getElementById('historyTableBody');
const gridContainer = document.getElementById('historyGridContainer');
const emptyState = document.getElementById('historyEmptyState');
const table = document.getElementById('historyTable');
if (!tableBody && !gridContainer) return;
showSkeletons();
try {
const response = await fetch(`${API_BASE_URL}/api/history`);
fullHistoryData = await response.json();
filteredHistoryData = [...fullHistoryData];
const totalCountEl = document.getElementById('totalCount');
if (totalCountEl) totalCountEl.textContent = fullHistoryData.length;
if (fullHistoryData.length === 0) {
if (emptyState) emptyState.style.display = 'flex';
if (table) table.style.display = 'none';
if (gridContainer) gridContainer.style.display = 'none';
} else {
if (emptyState) emptyState.style.display = 'none';
applyFilters();
}
} catch (err) {
console.error('Failed to load history:', err);
}
}
function showSkeletons() {
const tableBody = document.getElementById('historyTableBody');
if (!tableBody) return;
tableBody.innerHTML = Array(5).fill(0).map(() => `
<tr>
<td><div class="skeleton-text" style="width: 20px"></div></td>
<td><div class="skeleton-img"></div></td>
<td><div class="skeleton-text"></div></td>
<td><div class="skeleton-text" style="width: 40px"></div></td>
<td><div class="skeleton-text" style="width: 100px"></div></td>
<td><div class="skeleton-text"></div></td>
<td><div class="skeleton-text" style="width: 60px"></div></td>
</tr>
`).join('');
}
function renderHistory() {
if (currentView === 'list') {
renderHistoryTable();
} else {
renderHistoryGrid();
}
renderPagination();
}
function renderHistoryTable() {
const tableBody = document.getElementById('historyTableBody');
const noResultsState = document.getElementById('noResultsState');
const table = document.getElementById('historyTable');
const gridContainer = document.getElementById('historyGridContainer');
if (filteredHistoryData.length === 0) {
if (tableBody) tableBody.innerHTML = '';
if (noResultsState) noResultsState.style.display = 'flex';
if (table) table.style.display = 'none';
return;
}
if (noResultsState) noResultsState.style.display = 'none';
if (gridContainer) gridContainer.style.display = 'none';
if (table) table.style.display = 'table';
const showingCountEl = document.getElementById('showingCount');
if (showingCountEl) showingCountEl.textContent = filteredHistoryData.length;
const start = (currentPage - 1) * itemsPerPage;
const paginatedData = filteredHistoryData.slice(start, start + itemsPerPage);
tableBody.innerHTML = paginatedData.map(item => {
const isFake = item.prediction === 'FAKE';
const date = new Date(item.timestamp).toLocaleString();
const confidence = (item.confidence * 100).toFixed(1);
const isSelected = selectedIds.has(item.id);
return `
<tr data-id="${item.id}">
<td><input type="checkbox" class="item-checkbox" ${isSelected ? 'checked' : ''} onchange="toggleItemSelection(${item.id}, this)"></td>
<td>
${(() => {
if (!item.image_path) return '<div class="table-preview-img" style="background: rgba(255,255,255,0.05); display: flex; align-items: center; justify-content: center; font-size: 20px; cursor: pointer;" onclick="showPreviewModal(' + item.id + ')">📷</div>';
const isVideo = item.image_path.match(/\.(mp4|mov|avi|webm|mkv)$/i);
if (isVideo) {
return `<div class="table-preview-img" style="position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; background: #000; cursor: pointer;" onclick="showPreviewModal(${item.id})">
<video src="${API_BASE_URL}/${item.image_path}" muted loop onmouseover="this.play()" onmouseout="this.pause()" style="width: 100%; height: 100%; object-fit: cover;"></video>
<div style="position: absolute; bottom: 4px; right: 4px; font-size: 10px; background: rgba(0,0,0,0.6); color: #fff; padding: 2px 4px; border-radius: 4px;">▶</div>
</div>`;
}
return `<img src="${API_BASE_URL}/${item.image_path}" alt="${item.filename}" class="table-preview-img" onclick="showPreviewModal(${item.id})" style="cursor: pointer" onerror="this.outerHTML='<div class=\'table-preview-img\' style=\'background: rgba(255,255,255,0.05); display: flex; align-items: center; justify-content: center; font-size: 20px; cursor: pointer;\' onclick=\'showPreviewModal(${item.id})\'>📷</div>'">`;
})()}
</td>
<td class="table-filename" title="${item.filename}" onclick="showPreviewModal(${item.id})" style="cursor: pointer">${item.filename}</td>
<td><span class="table-badge ${isFake ? 'fake' : 'real'}">${isFake ? '⚠ FAKE' : '✓ REAL'}</span></td>
<td>
<div class="table-confidence">
<span>${confidence}%</span>
<div class="confidence-bar-small"><div class="confidence-fill-small" style="width: ${confidence}%"></div></div>
</div>
</td>
<td class="table-date">${date}</td>
<td>
<div class="table-actions">
<button class="btn-table-action" onclick="showPreviewModal(${item.id})">🔍 View</button>
<button class="btn-table-action btn-table-delete" onclick="deleteHistoryItem(${item.id})">🗑 Delete</button>
</div>
</td>
</tr>
`;
}).join('');
}
function renderHistoryGrid() {
const gridContainer = document.getElementById('historyGridContainer');
const table = document.getElementById('historyTable');
const noResultsState = document.getElementById('noResultsState');
if (filteredHistoryData.length === 0) {
if (gridContainer) gridContainer.innerHTML = '';
if (noResultsState) noResultsState.style.display = 'flex';
if (gridContainer) gridContainer.style.display = 'none';
return;
}
if (noResultsState) noResultsState.style.display = 'none';
if (table) table.style.display = 'none';
if (gridContainer) gridContainer.style.display = 'grid';
const start = (currentPage - 1) * itemsPerPage;
const paginatedData = filteredHistoryData.slice(start, start + itemsPerPage);
gridContainer.innerHTML = paginatedData.map(item => {
const isFake = item.prediction === 'FAKE';
const isSelected = selectedIds.has(item.id);
return `
<div class="grid-card ${isSelected ? 'selected' : ''}" data-id="${item.id}">
<div style="position: absolute; top: 10px; left: 10px; z-index: 5;">
<input type="checkbox" onchange="toggleItemSelection(${item.id}, this)" ${isSelected ? 'checked' : ''}>
</div>
${(() => {
if (!item.image_path) return '<div class="grid-preview" style="background: #222; display: flex; align-items: center; justify-content: center; font-size: 40px; cursor: pointer;" onclick="showPreviewModal(' + item.id + ')">📷</div>';
const isVideo = item.image_path.match(/\.(mp4|mov|avi|webm|mkv)$/i);
if (isVideo) {
return `<div class="grid-preview" style="position: relative; overflow: hidden; background: #000; cursor: pointer;" onclick="showPreviewModal(${item.id})">
<video src="${API_BASE_URL}/${item.image_path}" muted loop onmouseover="this.play()" onmouseout="this.pause()" style="width: 100%; height: 100%; object-fit: cover;"></video>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; background: rgba(0,0,0,0.5); width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; border: 1px solid rgba(255,255,255,0.5);">▶</div>
</div>`;
}
return `<img src="${API_BASE_URL}/${item.image_path}" alt="${item.filename}" class="grid-preview" onclick="showPreviewModal(${item.id})" onerror="this.outerHTML='<div class=\'grid-preview\' style=\'background: #222; display: flex; align-items: center; justify-content: center; font-size: 40px; cursor: pointer;\' onclick=\'showPreviewModal(${item.id})\'>📷</div>'">`;
})()}
<div class="grid-content">
<div class="grid-header">
<span class="table-badge ${isFake ? 'fake' : 'real'}">${isFake ? 'FAKE' : 'REAL'}</span>
<span class="grid-date">${new Date(item.timestamp).toLocaleDateString()}</span>
</div>
<div class="grid-title">${item.filename}</div>
<div class="table-confidence">
<span style="font-size: 12px">${(item.confidence * 100).toFixed(1)}%</span>
<div class="confidence-bar-small" style="flex: 1"><div class="confidence-fill-small" style="width: ${item.confidence * 100}%"></div></div>
</div>
</div>
</div>
`;
}).join('');
}
function renderPagination() {
const totalPages = Math.ceil(filteredHistoryData.length / itemsPerPage) || 1;
const totalEl = document.getElementById('totalPages');
const currentEl = document.getElementById('currentPage');
if (totalEl) totalEl.textContent = totalPages;
if (currentEl) currentEl.textContent = currentPage;
const prevBtn = document.getElementById('prevPageBtn');
const nextBtn = document.getElementById('nextPageBtn');
if (prevBtn) prevBtn.disabled = currentPage === 1;
if (nextBtn) nextBtn.disabled = currentPage === totalPages;
}
function changePage(delta) {
currentPage += delta;
renderHistory();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function toggleView(mode) {
currentView = mode;
const listBtn = document.getElementById('listViewBtn');
const gridBtn = document.getElementById('gridViewBtn');
if (listBtn) listBtn.classList.toggle('active', mode === 'list');
if (gridBtn) gridBtn.classList.toggle('active', mode === 'grid');
renderHistory();
}
function setQuickFilter(type, el) {
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
if (el) el.classList.add('active');
const predictionSelect = document.getElementById('filterPrediction');
const confidenceSelect = document.getElementById('filterConfidence');
if (predictionSelect && confidenceSelect) {
if (type === 'all') { predictionSelect.value = 'all'; confidenceSelect.value = 'all'; }
else if (type === 'FAKE' || type === 'REAL') { predictionSelect.value = type; confidenceSelect.value = 'all'; }
else if (type === 'high') { predictionSelect.value = 'all'; confidenceSelect.value = 'high'; }
}
currentPage = 1;
applyFilters();
}
let searchTimeout;
function handleSearch() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentPage = 1;
applyFilters();
}, 300);
}
function applyFilters() {
const searchTerm = document.getElementById('searchInput')?.value.toLowerCase() || '';
const predictionFilter = document.getElementById('filterPrediction')?.value || 'all';
const confidenceFilter = document.getElementById('filterConfidence')?.value || 'all';
const sortBy = document.getElementById('sortBy')?.value || 'date-desc';
filteredHistoryData = fullHistoryData.filter(item => {
if (searchTerm && !item.filename.toLowerCase().includes(searchTerm)) return false;
if (predictionFilter !== 'all' && item.prediction !== predictionFilter) return false;
if (confidenceFilter !== 'all') {
const conf = item.confidence * 100;
if (confidenceFilter === 'high' && conf <= 80) return false;
if (confidenceFilter === 'medium' && (conf < 50 || conf > 80)) return false;
if (confidenceFilter === 'low' && conf >= 50) return false;
}
return true;
});
const [col, ord] = sortBy.split('-');
sortHistoryData(col, ord);
renderHistory();
}
function sortHistoryData(column, order) {
currentSortColumn = column;
currentSortOrder = order;
filteredHistoryData.sort((a, b) => {
let aVal, bVal;
switch (column) {
case 'filename': aVal = a.filename.toLowerCase(); bVal = b.filename.toLowerCase(); break;
case 'confidence': aVal = a.confidence; bVal = b.confidence; break;
case 'prediction': aVal = a.prediction; bVal = b.prediction; break;
default: aVal = new Date(a.timestamp || 0).getTime(); bVal = new Date(b.timestamp || 0).getTime(); break;
}
const res = aVal > bVal ? 1 : -1;
return order === 'asc' ? res : -res;
});
}
function sortTable(column) {
if (currentSortColumn === column) currentSortOrder = currentSortOrder === 'asc' ? 'desc' : 'asc';
else { currentSortColumn = column; currentSortOrder = 'desc'; }
sortHistoryData(currentSortColumn, currentSortOrder);
renderHistory();
}
// ==================== BATCH ACTIONS & MODAL ====================
function toggleItemSelection(id, checkbox) {
if (checkbox.checked) selectedIds.add(id);
else selectedIds.delete(id);
updateSelectedCount();
if (currentView === 'grid') {
const card = document.querySelector(`.grid-card[data-id='${id}']`);
if (card) card.classList.toggle('selected', checkbox.checked);
}
}
function toggleSelectAll(checkbox) {
const start = (currentPage - 1) * itemsPerPage;
const visibleData = filteredHistoryData.slice(start, start + itemsPerPage);
visibleData.forEach(item => {
if (checkbox.checked) selectedIds.add(item.id);
else selectedIds.delete(item.id);
});
renderHistory();
updateSelectedCount();
}
function updateSelectedCount() {
const bar = document.getElementById('batchActionsBar');
const countEl = document.getElementById('selectedCount');
if (!bar || !countEl) return;
countEl.textContent = selectedIds.size;
if (selectedIds.size > 0) bar.classList.add('active');
else {
bar.classList.remove('active');
const selectAll = document.getElementById('selectAllCheckbox');
if (selectAll) selectAll.checked = false;
}
}
function clearSelection() {
selectedIds.clear();
updateSelectedCount();
renderHistory();
}
async function batchDelete() {
if (selectedIds.size === 0) return;
if (!confirm(`Are you sure you want to delete ${selectedIds.size} items?`)) return;
for (const id of selectedIds) {
await fetch(`${API_BASE_URL}/api/history/${id}`, { method: 'DELETE' }).catch(console.error);
}
showToast(`Processed batch deletion`, 'success');
selectedIds.clear();
updateSelectedCount();
loadEnhancedHistory();
}
function batchExport(format) {
const dataToExport = fullHistoryData.filter(item => selectedIds.has(item.id));
if (dataToExport.length === 0) return;
performExport(dataToExport, format, `batch_export_${format}`);
}
function exportHistory(format) {
if (filteredHistoryData.length === 0) {
showToast('No data to export', 'warning');
return;
}
performExport(filteredHistoryData, format, `history_export_${format}`);
}
function performExport(data, format, filename) {
let content, mime;
if (format === 'csv') {
content = 'ID,Filename,Prediction,Confidence,Date,Notes,Tags\n' +
data.map(i => `${i.id},"${i.filename}",${i.prediction},${i.confidence},"${i.timestamp}","${i.notes || ''}","${i.tags || ''}"`).join('\n');
mime = 'text/csv';
} else {
content = JSON.stringify(data, null, 2);
mime = 'application/json';
}
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `${filename}.${format}`; a.click();
URL.revokeObjectURL(url);
showToast(`Exported ${data.length} items`, 'success');
}
function showPreviewModal(id) {
const item = fullHistoryData.find(i => i.id === id);
if (!item) return;
const modal = document.getElementById('previewModal');
const body = document.getElementById('modalBody');
const isFake = item.prediction === 'FAKE';
const confidence = (item.confidence * 100).toFixed(1);
if (body) {
body.innerHTML = `
<div class="modal-media-container">
${item.image_path ?
(item.image_path.toLowerCase().endsWith('.mp4') || item.image_path.toLowerCase().endsWith('.mov') ?
`<video src="${API_BASE_URL}/${item.image_path}" class="modal-media" controls></video>` :
`<img src="${API_BASE_URL}/${item.image_path}" class="modal-media" alt="Preview">`) :
'<div class="modal-media" style="background: #222; aspect-ratio: 1; display:flex; align-items:center; justify-content:center; font-size:40px;">📷</div>'}
</div>
<div class="modal-details">
<h2 class="modal-title">${item.filename}</h2>
<div class="history-card-header">
<span class="table-badge ${isFake ? 'fake' : 'real'}">${isFake ? '⚠ FAKE' : '✓ REAL'}</span>
<span class="history-date">${new Date(item.timestamp).toLocaleString()}</span>
</div>
<div class="modal-stats">
<div class="table-confidence">
<span style="font-size: 18px; font-weight: 700;">${confidence}% Confidence</span>
<div class="confidence-bar-small" style="flex: 1; height: 10px;">
<div class="confidence-fill-small" style="width: ${confidence}%"></div>
</div>
</div>
</div>
<div class="notes-section">
<label style="display: block; margin-bottom: 8px; font-weight: 600;">Notes</label>
<textarea id="modalNotes" class="notes-area" placeholder="Add your notes here...">${item.notes || ''}</textarea>
<button class="btn-save-notes" onclick="saveNotes(${item.id})">Save Notes</button>
</div>
</div>
`;
}
if (modal) modal.style.display = 'flex';
}
function closeModal(event) {
if (!event || event.target.id === 'previewModal' || event.target.classList.contains('modal-close')) {
const modal = document.getElementById('previewModal');
if (modal) modal.style.display = 'none';
}
}
async function saveNotes(id) {
const notesArea = document.getElementById('modalNotes');
if (!notesArea) return;
const notes = notesArea.value;
try {
const response = await fetch(`${API_BASE_URL}/api/history/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notes: notes })
});
if (response.ok) {
showToast('Notes saved', 'success');
const item = fullHistoryData.find(i => i.id === id);
if (item) item.notes = notes;
} else showToast('Save failed', 'error');
} catch (err) { console.error(err); showToast('Error saving', 'error'); }
}
async function deleteHistoryItem(id) {
const row = document.querySelector(`tr[data-id="${id}"]`) || document.querySelector(`.grid-card[data-id="${id}"]`);
if (row) { row.style.opacity = '0'; row.style.transform = 'scale(0.95)'; }
try {
const response = await fetch(`${API_BASE_URL}/api/history/${id}`, { method: 'DELETE' });
if (response.ok) {
setTimeout(() => {
fullHistoryData = fullHistoryData.filter(item => item.id !== id);
selectedIds.delete(id);
updateSelectedCount();
applyFilters();
}, 300);
} else if (row) { row.style.opacity = '1'; row.style.transform = 'scale(1)'; }
} catch (err) {
console.error(err);
if (row) { row.style.opacity = '1'; row.style.transform = 'scale(1)'; }
}
}
function confirmClearAll() {
if (!confirm('Clear ALL history? This cannot be undone.')) return;
fetch(`${API_BASE_URL}/api/history`, { method: 'DELETE' })
.then(res => {
if (res.ok) {
fullHistoryData = [];
applyFilters();
showToast('History cleared', 'success');
}
})
.catch(console.error);
}
// Initial Hook
if (window.location.pathname.includes('history.html')) {
window.addEventListener('load', loadEnhancedHistory);
}
// --- Video Demo Player ---
document.addEventListener('DOMContentLoaded', () => {
const video = document.getElementById('demoVideoPlayer');
const playOverlay = document.getElementById('playOverlay');
if (video && playOverlay) {
playOverlay.addEventListener('click', () => {
if (video.paused) { video.play(); playOverlay.classList.add('hidden'); }
else { video.pause(); playOverlay.classList.remove('hidden'); }
});
}
});
// --- Toast Notifications ---
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast ${type}`;
let icon = 'ℹ️';
if (type === 'success') icon = '✅';
if (type === 'error') icon = '❌';
if (type === 'warning') icon = '⚠️';
toast.innerHTML = `
<span class="toast-icon">${icon}</span>
<span class="toast-message">${message}</span>
`;
container.appendChild(toast);
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 4000);
}