| | |
| | |
| |
|
| | |
| | |
| | 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'; |
| |
|
| | |
| | function getSessionId() { |
| | let sessionId = localStorage.getItem('deepguard_session_id'); |
| | if (!sessionId) { |
| | |
| | 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; |
| | } |
| |
|
| | |
| | |
| | async function checkBackendHealth(retries = 100) { |
| | const healthUrl = `${API_BASE_URL}/api/health`; |
| |
|
| | try { |
| | const response = await fetch(healthUrl, { method: 'GET' }); |
| | if (response.ok) { |
| | console.log('✅ Backend is ready!'); |
| | |
| | window.isBackendReady = true; |
| |
|
| | |
| | |
| | return true; |
| | } |
| | } catch (error) { |
| | console.warn('Backend sleeping or unreachable...'); |
| | } |
| |
|
| | if (retries > 0) { |
| | |
| | if (window.updateLoaderStatus) { |
| | window.updateLoaderStatus("STARTING SERVER..."); |
| | } else { |
| | |
| | console.log("Waiting for server..."); |
| | } |
| |
|
| | |
| | if (retries < 95) { |
| | |
| | |
| | } |
| |
|
| | |
| | await new Promise(resolve => setTimeout(resolve, 2000)); |
| | return checkBackendHealth(retries - 1); |
| | } |
| |
|
| | |
| | if (window.updateLoaderStatus) { |
| | window.updateLoaderStatus("SERVER ERROR"); |
| | } |
| | showToast('❌ Backend failed to start. Please refresh.', 'error'); |
| | return false; |
| | } |
| |
|
| | |
| | document.addEventListener('DOMContentLoaded', () => { |
| | |
| | |
| | |
| | checkBackendHealth(); |
| | }); |
| |
|
| |
|
| |
|
| | |
| | function showToast(message, type = 'info') { |
| | const container = document.getElementById('toastContainer'); |
| | if (!container) return; |
| |
|
| | const toast = document.createElement('div'); |
| | toast.className = `toast toast-${type}`; |
| |
|
| | |
| | if (type === 'error' || type === 'warning') { |
| | toast.setAttribute('role', 'alert'); |
| | toast.setAttribute('aria-live', 'assertive'); |
| | } else { |
| | toast.setAttribute('role', 'status'); |
| | toast.setAttribute('aria-live', 'polite'); |
| | } |
| |
|
| | |
| | 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); |
| |
|
| | |
| | if (type === 'success') playSound('success'); |
| | if (type === 'error') playSound('alert'); |
| |
|
| | |
| | setTimeout(() => { |
| | toast.classList.add('hiding'); |
| | toast.addEventListener('animationend', () => { |
| | if (toast.parentElement) toast.remove(); |
| | }); |
| | }, 4000); |
| | } |
| |
|
| | |
| | const MAX_FILE_SIZE = 100 * 1024 * 1024; |
| |
|
| | function validateFile(file) { |
| | if (file.size > MAX_FILE_SIZE) { |
| | showToast(`File "${file.name}" exceeds 100MB limit.`, 'error'); |
| | return false; |
| | } |
| | return true; |
| | } |
| |
|
| | |
| | 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') { |
| | |
| | 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') { |
| | |
| | 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') { |
| | |
| | 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); |
| | } |
| | }; |
| |
|
| | |
| | |
| | document.addEventListener('DOMContentLoaded', () => { |
| | |
| | const initTheme = () => { |
| | |
| | const themeToggleBtn = document.createElement('button'); |
| | themeToggleBtn.className = 'theme-toggle-btn'; |
| | themeToggleBtn.title = "Toggle Theme"; |
| |
|
| | |
| | const navContent = document.querySelector('.nav-content'); |
| | if (navContent) { |
| | |
| | |
| | const lastItem = navContent.lastElementChild; |
| |
|
| | |
| | |
| | if (lastItem && !lastItem.classList.contains('nav-menu') && lastItem.tagName !== 'SCRIPT') { |
| | const wrapper = document.createElement('div'); |
| | wrapper.style.display = 'flex'; |
| | wrapper.style.alignItems = 'center'; |
| |
|
| | |
| | navContent.insertBefore(wrapper, lastItem); |
| |
|
| | |
| | wrapper.appendChild(lastItem); |
| |
|
| | |
| | wrapper.appendChild(themeToggleBtn); |
| | } else { |
| | |
| | navContent.appendChild(themeToggleBtn); |
| | } |
| | } |
| |
|
| | |
| | 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'; |
| |
|
| | |
| | if (!document.startViewTransition) { |
| | document.documentElement.setAttribute('data-theme', newTheme); |
| | localStorage.setItem('theme', newTheme); |
| | updateThemeIcon(themeToggleBtn, newTheme); |
| | return; |
| | } |
| |
|
| | |
| | const x = e.clientX; |
| | const y = e.clientY; |
| |
|
| | |
| | const endRadius = Math.hypot( |
| | Math.max(x, innerWidth - x), |
| | Math.max(y, innerHeight - y) |
| | ); |
| |
|
| | |
| | const transition = document.startViewTransition(() => { |
| | document.documentElement.setAttribute('data-theme', newTheme); |
| | localStorage.setItem('theme', newTheme); |
| | updateThemeIcon(themeToggleBtn, newTheme); |
| | }); |
| |
|
| | |
| | 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, |
| | easing: 'ease-in-out', |
| | pseudoElement: '::view-transition-new(root)', |
| | } |
| | ); |
| | }); |
| | }); |
| | }; |
| |
|
| | const updateThemeIcon = (btn, theme) => { |
| | if (theme === 'light') { |
| | btn.innerHTML = '🌙'; |
| | btn.style.borderColor = 'var(--text-primary)'; |
| | btn.style.color = 'var(--text-primary)'; |
| | } else { |
| | btn.innerHTML = '☀'; |
| | btn.style.borderColor = 'rgba(255, 255, 255, 0.2)'; |
| | btn.style.color = '#fff'; |
| | } |
| | }; |
| |
|
| | initTheme(); |
| |
|
| | |
| | if (typeof AOS !== 'undefined') { |
| | AOS.init({ |
| | duration: 800, |
| | easing: 'ease-out-cubic', |
| | once: true, |
| | mirror: false, |
| | offset: 100 |
| | }); |
| | } |
| |
|
| | |
| | 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 |
| | }); |
| | } |
| |
|
| | |
| |
|
| | |
| | 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); |
| | }); |
| | }); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | 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', () => { |
| | |
| | card.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) scale(1)'; |
| | }); |
| | }); |
| |
|
| | |
| | const floatingCube = document.getElementById('floatingCube'); |
| | const floatingPyramid = document.getElementById('floatingPyramid'); |
| |
|
| | |
| | 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 |
| | }); |
| | } |
| |
|
| | |
| | let progress = 0; |
| | |
| | |
| | |
| | |
| |
|
| | if (floatingCube || floatingPyramid) { |
| | let mouseX = 0; |
| | let mouseY = 0; |
| | let currentX = 0; |
| | let currentY = 0; |
| |
|
| | |
| | document.addEventListener('mousemove', (e) => { |
| | mouseX = (e.clientX / window.innerWidth - 0.5) * 2; |
| | mouseY = (e.clientY / window.innerHeight - 0.5) * 2; |
| | }); |
| |
|
| | |
| | function animate3DObjects() { |
| | |
| | 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(); |
| | } |
| |
|
| | |
| | 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'); |
| |
|
| | |
| | const updateCanvasSize = () => { |
| | const rect = revealContainer.getBoundingClientRect(); |
| | revealCanvas.width = rect.width; |
| | revealCanvas.height = rect.height; |
| | }; |
| | updateCanvasSize(); |
| | window.addEventListener('resize', updateCanvasSize); |
| |
|
| | |
| | const physics = { |
| | mouseX: -1000, |
| | mouseY: -1000, |
| | targetX: -1000, |
| | targetY: -1000, |
| | velocityX: 0, |
| | velocityY: 0, |
| | prevMouseX: -1000, |
| | prevMouseY: -1000, |
| | damping: 0.18, |
| | stiffness: 0.08, |
| | isHovering: false |
| | }; |
| |
|
| | |
| | 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) { |
| | |
| | const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); |
| |
|
| | |
| | const noise = Math.sin(time * this.noiseSpeed + this.noiseOffset) * 30; |
| |
|
| | |
| | const velocityAngle = Math.atan2(velocityY, velocityX); |
| | const angleDiff = this.angle - velocityAngle; |
| |
|
| | |
| | const trailingFactor = Math.cos(angleDiff); |
| | const trailing = trailingFactor < 0 ? trailingFactor * speed * 60 : 0; |
| |
|
| | |
| | const perpFactor = Math.sin(angleDiff); |
| | const perpDeformation = perpFactor * speed * 15; |
| |
|
| | |
| | const velocityMorph = speed * 3; |
| |
|
| | |
| | this.targetRadius = this.baseRadius + noise + trailing + perpDeformation + velocityMorph; |
| |
|
| | |
| | this.currentRadius += (this.targetRadius - this.currentRadius) * 0.15; |
| |
|
| | |
| | this.x = centerX + Math.cos(this.angle) * this.currentRadius; |
| | this.y = centerY + Math.sin(this.angle) * this.currentRadius; |
| | } |
| | } |
| |
|
| | |
| | const controlPoints = []; |
| | const pointCount = 20; |
| | const baseRadius = 350; |
| |
|
| | for (let i = 0; i < pointCount; i++) { |
| | const angle = (i / pointCount) * Math.PI * 2; |
| | controlPoints.push(new ControlPoint(angle, baseRadius)); |
| | } |
| |
|
| | |
| | revealContainer.addEventListener('mouseenter', () => { |
| | physics.isHovering = true; |
| | }); |
| |
|
| | revealContainer.addEventListener('mouseleave', () => { |
| | physics.isHovering = false; |
| | |
| | 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; |
| | }); |
| |
|
| | |
| | function drawOrganicShape(centerX, centerY, time, velocityX, velocityY) { |
| | |
| | controlPoints.forEach(point => { |
| | point.update(centerX, centerY, time, velocityX, velocityY); |
| | }); |
| |
|
| | |
| | ctx.beginPath(); |
| |
|
| | |
| | ctx.moveTo(controlPoints[0].x, controlPoints[0].y); |
| |
|
| | |
| | for (let i = 0; i < pointCount; i++) { |
| | const current = controlPoints[i]; |
| | const next = controlPoints[(i + 1) % pointCount]; |
| |
|
| | |
| | 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(); |
| | } |
| |
|
| | |
| | let animationTime = 0; |
| | function animateReveal() { |
| | animationTime++; |
| |
|
| | |
| | const prevX = physics.mouseX; |
| | const prevY = physics.mouseY; |
| |
|
| | |
| | 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; |
| |
|
| | |
| | const actualVelocityX = physics.mouseX - prevX; |
| | const actualVelocityY = physics.mouseY - prevY; |
| |
|
| | |
| | ctx.clearRect(0, 0, revealCanvas.width, revealCanvas.height); |
| |
|
| | |
| | if (physics.isHovering || physics.mouseX > -500) { |
| | |
| | ctx.fillStyle = 'white'; |
| | drawOrganicShape(physics.mouseX, physics.mouseY, animationTime, actualVelocityX, actualVelocityY); |
| |
|
| | |
| | revealBottomImage.style.maskImage = `url(${revealCanvas.toDataURL()})`; |
| | revealBottomImage.style.webkitMaskImage = `url(${revealCanvas.toDataURL()})`; |
| | revealBottomImage.style.maskSize = 'cover'; |
| | revealBottomImage.style.webkitMaskSize = 'cover'; |
| | } else { |
| | |
| | revealBottomImage.style.maskImage = 'none'; |
| | revealBottomImage.style.webkitMaskImage = 'none'; |
| | } |
| |
|
| | requestAnimationFrame(animateReveal); |
| | } |
| |
|
| | animateReveal(); |
| | } |
| | }); |
| |
|
| | |
| | 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); |
| | }); |
| |
|
| | |
| | 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)'; |
| | }); |
| | }); |
| | }; |
| |
|
| | |
| | init3DTilt(); |
| |
|
| | |
| | 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' |
| | }); |
| | } |
| | }); |
| | }); |
| |
|
| | |
| | let lastScroll = 0; |
| | const navbar = document.querySelector('.navbar'); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | 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"; |
| | } |
| | } |
| |
|
| | |
| | window.addEventListener('load', initComparisons); |
| |
|
| | |
| | let scrollY = 0; |
| | let lastScrollY = 0; |
| | let ticking = false; |
| |
|
| | |
| | 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'); |
| |
|
| | |
| | textReveals.forEach(el => el.classList.add('scroll-reveal')); |
| |
|
| | |
| | window.addEventListener('scroll', () => { |
| | scrollY = window.scrollY; |
| | if (!ticking) { |
| | window.requestAnimationFrame(updateScrollStory); |
| | ticking = true; |
| | } |
| | }); |
| |
|
| | function updateScrollStory() { |
| | const windowHeight = window.innerHeight; |
| | const documentHeight = document.documentElement.scrollHeight; |
| |
|
| | |
| | const progress = (scrollY / (documentHeight - windowHeight)) * 100; |
| | if (progressBar) progressBar.style.width = `${progress}%`; |
| |
|
| | |
| | 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'; |
| | } |
| |
|
| | |
| | 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})`; |
| | } |
| | } |
| |
|
| | |
| | parallaxItems.forEach((item, index) => { |
| | const rect = item.getBoundingClientRect(); |
| | |
| | if (rect.top < windowHeight + 100 && rect.bottom > -100) { |
| | |
| | const speed = (index % 3 + 1) * 0.05; |
| | const offset = (scrollY * speed) * 0.5; |
| | |
| | |
| | |
| | |
| | |
| | |
| | } |
| | }); |
| |
|
| | |
| | textReveals.forEach(el => { |
| | const rect = el.getBoundingClientRect(); |
| | |
| | const centerOffset = (windowHeight / 2) - (rect.top + rect.height / 2); |
| |
|
| | |
| | if (rect.top < windowHeight * 0.85) { |
| | el.classList.add('active'); |
| | } else { |
| | |
| | |
| | } |
| | }); |
| |
|
| | |
| | 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; |
| | } |
| |
|
| | |
| | updateScrollStory(); |
| |
|
| | |
| |
|
| | |
| | document.addEventListener('DOMContentLoaded', () => { |
| | |
| | if (!document.querySelector('.page-transition-overlay')) { |
| | const overlay = document.createElement('div'); |
| | overlay.className = 'page-transition-overlay'; |
| | document.body.appendChild(overlay); |
| |
|
| | |
| | setTimeout(() => { |
| | overlay.classList.add('loaded'); |
| | }, 100); |
| | } |
| | }); |
| |
|
| | |
| | document.querySelectorAll('a').forEach(link => { |
| | link.addEventListener('click', e => { |
| | const href = link.getAttribute('href'); |
| |
|
| | |
| | if (href && href.startsWith('#') || href.includes('javascript:') || !href) return; |
| |
|
| | e.preventDefault(); |
| | const overlay = document.querySelector('.page-transition-overlay'); |
| | overlay.classList.remove('loaded'); |
| |
|
| | setTimeout(() => { |
| | window.location.href = href; |
| | }, 600); |
| | }); |
| | }); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | btn.style.transform = `translate(${x * 0.2}px, ${y * 0.2}px)`; |
| | }); |
| |
|
| | btn.addEventListener('mouseleave', () => { |
| | btn.style.transform = 'translate(0, 0)'; |
| | }); |
| | }); |
| |
|
| | |
| | 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); |
| |
|
| | |
| | window.addEventListener('scroll', () => { |
| | if (window.scrollY > 50) { |
| | logo.style.fontSize = '1.5rem'; |
| | } else { |
| | logo.style.fontSize = '1.8rem'; |
| | } |
| | }); |
| | } |
| |
|
| | 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) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | } |
| |
|
| | 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'); |
| |
|
| | |
| | const reader = new FileReader(); |
| | reader.onload = (e) => { |
| | if (isImage) { |
| | previewImage.src = e.target.result; |
| | previewImage.style.display = 'block'; |
| | |
| | } else { |
| | |
| | |
| | previewImage.style.display = 'none'; |
| | } |
| |
|
| | uploadArea.style.display = 'none'; |
| | previewArea.style.display = 'block'; |
| |
|
| | |
| | document.getElementById('heatmapToggle').style.display = 'none'; |
| | document.getElementById('heatmapOverlay').style.display = 'none'; |
| | document.getElementById('scanTimeDisplay').textContent = '--'; |
| | }; |
| | reader.readAsDataURL(file); |
| |
|
| | |
| | const analysisResults = document.querySelector('.analysis-results'); |
| | const emptyState = document.querySelector('.empty-state'); |
| |
|
| | emptyState.style.display = 'none'; |
| | analysisResults.style.display = 'none'; |
| |
|
| | |
| | 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); |
| | } |
| |
|
| | |
| | if (isVideo) { |
| | document.getElementById('loaderText').textContent = "Scanning Video Frames..."; |
| | document.getElementById('loaderSubText').textContent = "Processing frame-by-frame analysis"; |
| | } |
| |
|
| | loader.style.display = 'block'; |
| |
|
| | |
| | showProcessingOverlay(isVideo); |
| |
|
| | |
| | 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 { |
| | |
| | setProcessingStep('upload'); |
| |
|
| | |
| | const formData = new FormData(); |
| | formData.append('file', file); |
| |
|
| | const startTime = performance.now(); |
| |
|
| | const endpoint = isVideo ? '/api/predict_video' : '/api/predict'; |
| |
|
| | |
| | setProcessingStep('connect'); |
| |
|
| | const response = await fetch(`${API_BASE_URL}${endpoint}`, { |
| | method: 'POST', |
| | headers: { |
| | 'X-Session-ID': getSessionId() |
| | }, |
| | body: formData |
| | }); |
| |
|
| | |
| | setProcessingStep('analyze'); |
| |
|
| | if (!response.ok) throw new Error('Analysis failed'); |
| |
|
| | const result = await response.json(); |
| |
|
| | const endTime = performance.now(); |
| | const duration = ((endTime - startTime) / 1000).toFixed(2); |
| |
|
| | if (isVideo) { |
| | |
| | |
| | |
| | |
| | if (result.avg_fake_prob !== undefined) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| |
|
| | |
| | |
| | |
| | } |
| |
|
| | |
| | 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`; |
| | } |
| |
|
| | |
| | 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'; |
| | heatmapToggle.style.display = 'flex'; |
| | heatmapSwitch.checked = false; |
| | heatmapOverlay.style.opacity = '0'; |
| |
|
| | heatmapSwitch.onchange = (e) => { |
| | heatmapOverlay.style.opacity = e.target.checked ? '1' : '0'; |
| | }; |
| | } |
| |
|
| | |
| | if (result.scan_id) { |
| | currentScanId = result.scan_id; |
| | console.log('Scan ID stored:', currentScanId); |
| | } |
| |
|
| | |
| | updateAnalysisUI(result); |
| |
|
| | loader.style.display = 'none'; |
| | analysisResults.style.display = 'block'; |
| |
|
| | |
| | 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> |
| | `; |
| |
|
| | |
| | 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'); |
| |
|
| | |
| | verdictTitle.textContent = isFake ? 'FAKE DETECTED' : 'REAL IMAGE'; |
| | verdictTitle.className = `verdict-title ${isFake ? 'verdict-fake' : 'verdict-real'}`; |
| |
|
| | |
| | const badgeContainer = document.getElementById('detectionBadges'); |
| | if (badgeContainer) { |
| | badgeContainer.innerHTML = ''; |
| |
|
| | |
| | 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); |
| | } |
| |
|
| | |
| | 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); |
| | } |
| | } |
| |
|
| |
|
| | |
| | playSound(isFake ? 'alert' : 'success'); |
| |
|
| | |
| | setTimeout(() => { |
| | confidenceBar.style.width = `${confidence}%`; |
| |
|
| | |
| | confidenceBar.classList.remove('confidence-low', 'confidence-medium', 'confidence-high', 'confidence-very-high'); |
| |
|
| | |
| | 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`; |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | const ctx = document.getElementById('probabilityChart').getContext('2d'); |
| |
|
| | |
| | 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)', |
| | 'rgba(16, 185, 129, 0.9)' |
| | ], |
| | borderColor: [ |
| | 'rgba(227, 245, 20, 1)', |
| | 'rgba(16, 185, 129, 1)' |
| | ], |
| | borderWidth: 2, |
| | hoverOffset: 8 |
| | }] |
| | }, |
| | options: { |
| | responsive: true, |
| | maintainAspectRatio: false, |
| | cutout: '65%', |
| | 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' |
| | } |
| | } |
| | }); |
| |
|
| | |
| | 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'; |
| |
|
| | |
| | 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'; |
| | |
| | if (btnCorrect) btnCorrect.disabled = false; |
| | if (btnWrong) btnWrong.disabled = false; |
| | |
| | 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'; |
| | } |
| |
|
| | |
| | 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'); |
| |
|
| | |
| | const predictedLabel = verdictTitle.textContent.includes('FAKE') ? 'FAKE' : 'REAL'; |
| |
|
| | |
| | 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) { |
| | |
| | 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); |
| |
|
| | |
| | if (btnCorrect) btnCorrect.disabled = false; |
| | if (btnWrong) btnWrong.disabled = false; |
| |
|
| | |
| | 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'); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | 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)`; |
| | }); |
| | }); |
| |
|
| | |
| | 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)'; |
| | }); |
| | }); |
| |
|
| | |
| | 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(); |
| | } |
| |
|
| | |
| | document.querySelectorAll('.stat-value').forEach((stat, index) => { |
| | stat.style.animationDelay = `${index * 0.1}s`; |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | 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); |
| | } |
| |
|
| | |
| | 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'); |
| | |
| | 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!'); |
| | |
| | |
| | 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; |
| |
|
| | |
| | const colorBlack = [10, 10, 15]; |
| | const colorYellow = [227, 245, 20]; |
| | const colorGray = [128, 128, 128]; |
| | const colorRed = [220, 38, 38]; |
| | const colorGreen = [22, 163, 74]; |
| | const colorWhite = [255, 255, 255]; |
| |
|
| | |
| | doc.setFillColor(...colorBlack); |
| | doc.rect(0, 0, pageWidth, pageHeight, 'F'); |
| |
|
| | |
| | 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); |
| | } |
| |
|
| | |
| | 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); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | 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); |
| |
|
| | |
| | const previewImg = document.getElementById('previewImage'); |
| | const heatmapImg = document.getElementById('heatmapOverlay'); |
| |
|
| | let yPos = margin + 105; |
| |
|
| | |
| | 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; |
| | |
| | const maxImgWidth = (pageWidth - (margin * 3)) / 2; |
| | const imgHeight = Math.min(maxImgWidth * imgRatio, 80); |
| | 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); |
| | } |
| |
|
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | 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'); |
| | doc.setFillColor(20, 20, 25); |
| | doc.rect(margin + 60, yPos, pageWidth - (margin * 2) - 60, 10, 'F'); |
| |
|
| | 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; |
| | }); |
| |
|
| | |
| | doc.setFontSize(8); |
| | doc.setTextColor(...colorGray); |
| | doc.text('Generated by DeepGuard AI System. Authenticity verified by cryptographic signature.', margin, pageHeight - 15); |
| |
|
| | |
| | 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; |
| | }); |
| | } |
| |
|
| | |
| | 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> |
| | `; |
| | |
| | const btnContainer = document.createElement('div'); |
| | btnContainer.style.display = 'flex'; |
| | btnContainer.style.gap = '10px'; |
| | btnContainer.style.marginTop = '15px'; |
| |
|
| | |
| | const downloadBtn = document.createElement('button'); |
| | downloadBtn.className = 'btn-history-download'; |
| | downloadBtn.innerHTML = '📄 Download Report'; |
| | downloadBtn.onclick = () => generateHistoryPDF(item); |
| |
|
| | |
| | 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); |
| | }); |
| |
|
| | |
| |
|
| | } 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(); |
| |
|
| | |
| | doc.setFillColor(17, 17, 17); |
| | doc.rect(0, 0, 210, 297, 'F'); |
| |
|
| | |
| | doc.setFillColor(227, 245, 20); |
| | doc.rect(0, 0, 210, 40, 'F'); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | 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'); |
| | |
| | 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; |
| |
|
| | |
| | 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(); |
| | }; |
| | |
| | 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); |
| | } |
| |
|
| | |
| | doc.setTextColor(227, 245, 20); |
| | doc.setFontSize(16); |
| | doc.setFont('helvetica', 'bold'); |
| | doc.text("SCAN DETAILS", leftMargin, y); |
| | y += 10; |
| |
|
| | |
| | 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; |
| |
|
| | |
| | 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); |
| |
|
| | |
| | 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) { |
| | |
| | const targetCard = document.querySelector(`[data-scan-id="${scanId}"]`); |
| |
|
| | if (targetCard) { |
| | |
| | targetCard.style.transition = 'all 0.3s ease'; |
| | targetCard.style.opacity = '0'; |
| | targetCard.style.transform = 'scale(0.8)'; |
| |
|
| | |
| | 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) { |
| | |
| | if (targetCard) { |
| | targetCard.remove(); |
| | } |
| |
|
| | |
| | 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) { |
| | |
| | targetCard.style.opacity = '1'; |
| | targetCard.style.transform = 'scale(1)'; |
| | } |
| | } |
| | } catch (err) { |
| | console.error('Error deleting scan:', err); |
| | if (targetCard) { |
| | |
| | targetCard.style.opacity = '1'; |
| | targetCard.style.transform = 'scale(1)'; |
| | } |
| | } |
| | } |
| |
|
| | async function clearHistory() { |
| | |
| |
|
| |
|
| | try { |
| | await fetch(`${API_BASE_URL}/api/history`, { |
| | method: 'DELETE', |
| | headers: { |
| | 'X-Session-ID': getSessionId() |
| | } |
| | }); |
| | loadHistory(); |
| | } catch (err) { |
| | console.error('Failed to clear history:', err); |
| | } |
| | } |
| |
|
| | |
| | if (window.location.pathname.includes('history.html')) { |
| | window.addEventListener('load', loadHistory); |
| | } |
| |
|
| | |
| | let currentScanId = null; |
| | let uploadQueue = []; |
| | let isProcessingQueue = false; |
| | let currentUploadIndex = 0; |
| |
|
| | |
| | if (fileInput && uploadArea) { |
| |
|
| |
|
| | |
| | |
| | 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); |
| |
|
| | |
| | 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'); |
| | } |
| | }); |
| |
|
| | |
| | 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 = ''; |
| | }); |
| |
|
| | |
| | document.addEventListener('paste', (e) => { |
| | |
| | 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]; |
| |
|
| | |
| | if (item.type.startsWith('image/')) { |
| | const file = item.getAsFile(); |
| | if (file) { |
| | pastedFiles.push(file); |
| | } |
| | } |
| | } |
| |
|
| | if (pastedFiles.length > 0) { |
| | e.preventDefault(); |
| |
|
| | const validFiles = pastedFiles.filter(validateFile); |
| |
|
| | if (validFiles.length > 0) { |
| | addFilesToQueue(validFiles); |
| | showToast(`${validFiles.length} image(s) pasted successfully`, 'success'); |
| | } |
| | } |
| | }); |
| | } |
| |
|
| | function addFilesToQueue(files) { |
| | |
| | uploadArea.style.display = 'none'; |
| | document.getElementById('fileQueueContainer').style.display = 'block'; |
| |
|
| | files.forEach(file => { |
| | const fileObj = { |
| | file: file, |
| | id: Date.now() + Math.random(), |
| | status: 'pending', |
| | 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(); |
| | |
| | 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; |
| | } |
| |
|
| |
|
| | |
| | |
| | let processingTimerInterval; |
| | let warmupTimerTimeout; |
| | let currentProcessingStep = null; |
| |
|
| | |
| | |
| | |
| | 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' }; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | 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'); |
| |
|
| | |
| | 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'); |
| | } |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | 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; |
| |
|
| | |
| | overlay.style.display = 'flex'; |
| | if (warmupAlert) warmupAlert.style.display = 'none'; |
| |
|
| | |
| | setProcessingStep('upload'); |
| |
|
| | |
| | if (processingMainTitle) { |
| | processingMainTitle.textContent = isVideo ? 'Analyzing Video' : 'Analyzing Media'; |
| | } |
| |
|
| | |
| | if (modelStatusBadge) { |
| | modelStatusBadge.style.display = 'inline-flex'; |
| | if (modelStatusText) { |
| | modelStatusText.textContent = 'Checking...'; |
| | } |
| | } |
| |
|
| | |
| | clearTimeout(warmupTimerTimeout); |
| | warmupTimerTimeout = setTimeout(() => { |
| | if (warmupAlert && overlay.style.display === 'flex') { |
| | warmupAlert.style.display = 'flex'; |
| | if (processingMessage) { |
| | processingMessage.textContent = 'Model is initializing...'; |
| | } |
| |
|
| | |
| | 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; |
| | if (progress > 95) progress = 95; |
| |
|
| | bar.style.width = `${progress}%`; |
| | label.textContent = `${Math.round(progress)}%`; |
| | }, 400); |
| | } |
| | } |
| | }, 5000); |
| | } |
| |
|
| | |
| | |
| | |
| | function hideProcessingOverlay() { |
| | const overlay = document.getElementById('processingOverlay'); |
| | const processingMainTitle = document.getElementById('processingMainTitle'); |
| | const processingMessage = document.getElementById('processingMessage'); |
| |
|
| | if (!overlay) return; |
| |
|
| | |
| | |
| | clearTimeout(warmupTimerTimeout); |
| | if (window.warmupProgressInterval) clearInterval(window.warmupProgressInterval); |
| | clearInterval(processingTimerInterval); |
| |
|
| | |
| | setProcessingStep('generate'); |
| | if (processingMainTitle) { |
| | processingMainTitle.textContent = 'Analysis Complete'; |
| | } |
| | if (processingMessage) { |
| | processingMessage.textContent = 'Reviewing AI signals...'; |
| | } |
| |
|
| | |
| | setTimeout(() => { |
| | document.querySelectorAll('.progress-step').forEach(step => { |
| | step.classList.remove('active'); |
| | step.classList.add('completed'); |
| | }); |
| | }, 200); |
| |
|
| | |
| | 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'); |
| |
|
| | |
| | await loadStatisticsAndRecent(); |
| |
|
| | |
| | |
| | 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)); |
| |
|
| | |
| | if (videoFiles.length === 1 && uploadQueue.length === 1) { |
| | window.location.href = 'video_result.html'; |
| | return; |
| | } |
| |
|
| | |
| | if (imageFiles.length === 1 && uploadQueue.length === 1) { |
| | const fileObj = imageFiles[0]; |
| |
|
| | |
| | const reader = new FileReader(); |
| | reader.onload = (e) => { |
| | const previewImg = document.getElementById('previewImage'); |
| | if (previewImg) previewImg.src = e.target.result; |
| |
|
| | |
| | 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'; |
| |
|
| | |
| | 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'; |
| | } |
| |
|
| | if (heatmapToggle) heatmapToggle.style.display = 'flex'; |
| |
|
| | if (heatmapSwitch) { |
| | heatmapSwitch.checked = false; |
| | heatmapSwitch.onchange = (e) => { |
| | if (heatmapOverlay) heatmapOverlay.style.opacity = e.target.checked ? '1' : '0'; |
| | }; |
| | } |
| | } |
| |
|
| | |
| | if (fileObj.result.scan_id) { |
| | currentScanId = fileObj.result.scan_id; |
| | console.log('Scan ID stored from queue:', currentScanId); |
| | } |
| |
|
| | |
| | updateAnalysisUI(fileObj.result); |
| | }; |
| | reader.readAsDataURL(fileObj.file); |
| | return; |
| | } |
| |
|
| | |
| | |
| | showToast(`All files processed! ${completedFiles.length}/${uploadQueue.length} succeeded.`, 'success'); |
| |
|
| | if (uploadQueue.some(f => f.status !== 'completed')) { |
| | |
| | } else { |
| | clearQueue(); |
| | } |
| | }, 1000); |
| | } |
| |
|
| | async function uploadSingleFile(fileObj) { |
| | const item = document.getElementById(`file-${fileObj.id}`); |
| | if (!item) return; |
| |
|
| | |
| | fileObj.status = 'uploading'; |
| | item.classList.add('uploading'); |
| | const statusEl = item.querySelector('.file-status'); |
| | statusEl.className = 'file-status uploading'; |
| | statusEl.textContent = 'Uploading...'; |
| |
|
| | |
| | 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(); |
| |
|
| | |
| | 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; |
| | if (progressStatus) { |
| | progressStatus.textContent = `${speed.toFixed(1)} KB/s`; |
| | } |
| |
|
| | |
| | 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')); |
| | }); |
| |
|
| | |
| | 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); |
| | }); |
| |
|
| | |
| | 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'; |
| |
|
| | |
| | |
| | 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) { |
| | |
| | localStorage.setItem('video_analysis_result', JSON.stringify(result)); |
| |
|
| | |
| | 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) { |
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | async function loadStatisticsAndRecent() { |
| | try { |
| | const response = await fetch(`${API_BASE_URL}/api/history`); |
| | const history = await response.json(); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | document.getElementById('totalScans').textContent = total; |
| | document.getElementById('fakeCount').textContent = fake; |
| | document.getElementById('realCount').textContent = real; |
| | document.getElementById('avgConfidence').textContent = `${avgConf}%`; |
| |
|
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | if (window.location.pathname.includes('analysis.html')) { |
| | window.addEventListener('load', loadStatisticsAndRecent); |
| | } |
| |
|
| | |
| | let fullHistoryData = []; |
| | let filteredHistoryData = []; |
| | let currentSortColumn = 'timestamp'; |
| | let currentSortOrder = 'desc'; |
| | let currentPage = 1; |
| | const itemsPerPage = 8; |
| | let currentView = 'list'; |
| | let selectedIds = new Set(); |
| |
|
| | |
| | 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(); |
| | } |
| |
|
| | |
| |
|
| | 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); |
| | } |
| |
|
| | |
| | if (window.location.pathname.includes('history.html')) { |
| | window.addEventListener('load', loadEnhancedHistory); |
| | } |
| |
|
| | |
| | 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'); } |
| | }); |
| | } |
| | }); |
| |
|
| | |
| | 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); |
| | } |
| |
|