// ==================== VOICEGUARD APPLICATION ==================== // Main application controller import { api, APIError } from './api.js'; import { CONFIG } from './config.js'; // ==================== STATE ==================== const state = { // UI State isLoading: false, // File State selectedFile: null, fileDuration: null, // Language State selectedLanguage: 'English', // Results State hasResults: false, results: null, // API State apiStatus: 'checking' }; // ==================== DOM ELEMENTS ==================== const elements = { // API Status apiStatus: document.getElementById('apiStatus'), // File Upload dropzone: document.getElementById('dropzone'), fileInput: document.getElementById('fileInput'), browseBtn: document.getElementById('browseBtn'), filePreview: document.getElementById('filePreview'), fileName: document.getElementById('fileName'), fileMeta: document.getElementById('fileMeta'), removeBtn: document.getElementById('removeBtn'), // Language languageSelect: document.getElementById('languageSelect'), // Analyze Button analyzeBtn: document.getElementById('analyzeBtn'), // Results resultsEmpty: document.getElementById('resultsEmpty'), resultsContent: document.getElementById('resultsContent'), verdictCard: document.getElementById('verdictCard'), verdictIcon: document.getElementById('verdictIcon'), verdictLabel: document.getElementById('verdictLabel'), confidenceValue: document.getElementById('confidenceValue'), confidenceFill: document.getElementById('confidenceFill'), resultLanguage: document.getElementById('resultLanguage'), explanationText: document.getElementById('explanationText'), // Toast toastContainer: document.getElementById('toastContainer') }; // ==================== INITIALIZATION ==================== document.addEventListener('DOMContentLoaded', () => { console.log('🛡️ VoiceGuard UI initialized'); initFileUpload(); initLanguageSelector(); initAnalyzeButton(); checkAPIHealth(); // Periodic health check setInterval(checkAPIHealth, 30000); }); // ==================== API HEALTH ==================== async function checkAPIHealth() { const result = await api.checkHealth(); state.apiStatus = result.status; updateAPIStatus(); } function updateAPIStatus() { const statusEl = elements.apiStatus; const textEl = statusEl.querySelector('.status-indicator__text'); statusEl.classList.remove('is-online', 'is-offline'); // Backend returns 'healthy' which matches our 'online' requirement if (state.apiStatus === 'online' || state.apiStatus === 'healthy') { statusEl.classList.add('is-online'); textEl.textContent = 'System Online'; } else if (state.apiStatus === 'offline') { statusEl.classList.add('is-offline'); textEl.textContent = 'System Offline'; } else { textEl.textContent = 'Checking...'; } } // ==================== FILE UPLOAD ==================== function initFileUpload() { const { dropzone, fileInput, browseBtn, removeBtn } = elements; // Browse button click browseBtn.addEventListener('click', () => fileInput.click()); // Dropzone click dropzone.addEventListener('click', (e) => { if (e.target === dropzone || e.target.closest('.file-upload__icon')) { fileInput.click(); } }); // File input change fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) handleFileSelect(file); }); // Drag and drop dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('is-dragging'); }); dropzone.addEventListener('dragleave', (e) => { e.preventDefault(); dropzone.classList.remove('is-dragging'); }); dropzone.addEventListener('drop', (e) => { e.preventDefault(); dropzone.classList.remove('is-dragging'); const file = e.dataTransfer.files[0]; if (file) handleFileSelect(file); }); // Remove button removeBtn.addEventListener('click', handleFileRemove); } async function handleFileSelect(file) { console.log('📁 File selected:', file.name, file.type, file.size); // Validate file type if (!CONFIG.FILE.ALLOWED_TYPES.includes(file.type)) { showToast('error', 'Invalid File', 'Only MP3 files are supported'); shakeDropzone(); return; } // Validate file size if (file.size > CONFIG.FILE.MAX_SIZE) { showToast('error', 'File Too Large', 'Maximum file size is 10MB'); shakeDropzone(); return; } // Get audio duration try { const duration = await getAudioDuration(file); console.log('⏱️ Duration:', duration, 'seconds'); // Validate duration if (duration < CONFIG.FILE.MIN_DURATION) { showToast('error', 'Audio Too Short', 'Minimum duration is 1 second'); shakeDropzone(); return; } if (duration > CONFIG.FILE.MAX_DURATION) { showToast('error', 'Audio Too Long', 'Maximum duration is 60 seconds'); shakeDropzone(); return; } // Store file state.selectedFile = file; state.fileDuration = duration; // Update UI updateFilePreview(); updateAnalyzeButton(); } catch (error) { console.error('Error reading file:', error); showToast('error', 'Invalid File', 'Could not read audio file'); shakeDropzone(); } } function handleFileRemove() { state.selectedFile = null; state.fileDuration = null; elements.fileInput.value = ''; updateFilePreview(); updateAnalyzeButton(); } function updateFilePreview() { const { dropzone, filePreview, fileName, fileMeta } = elements; if (state.selectedFile) { dropzone.hidden = true; filePreview.hidden = false; fileName.textContent = state.selectedFile.name; fileMeta.textContent = `${state.fileDuration.toFixed(1)}s • ${formatFileSize(state.selectedFile.size)}`; } else { dropzone.hidden = false; filePreview.hidden = true; } } function shakeDropzone() { elements.dropzone.classList.add('has-error'); setTimeout(() => { elements.dropzone.classList.remove('has-error'); }, 500); } // ==================== LANGUAGE SELECTOR ==================== function initLanguageSelector() { const { languageSelect } = elements; if (languageSelect) { // Set initial state from dropdown state.selectedLanguage = languageSelect.value; // Listen for changes languageSelect.addEventListener('change', (e) => { state.selectedLanguage = e.target.value; console.log('🌐 Language changed to:', state.selectedLanguage); }); } } // ==================== ANALYZE BUTTON ==================== function initAnalyzeButton() { elements.analyzeBtn.addEventListener('click', handleAnalyze); } function updateAnalyzeButton() { elements.analyzeBtn.disabled = !state.selectedFile || state.isLoading; } async function handleAnalyze() { if (!state.selectedFile || state.isLoading) return; console.log('🔍 Starting analysis...'); // Set loading state state.isLoading = true; setButtonLoading(true); updateAnalyzeButton(); try { // Make API request const result = await api.detectVoice(state.selectedFile, state.selectedLanguage); // Store results state.hasResults = true; state.results = result; // Show results showResults(result); console.log('✅ Analysis complete:', result); } catch (error) { console.error('❌ Analysis failed:', error); const message = error instanceof APIError ? error.message : 'An unexpected error occurred'; showToast('error', 'Analysis Failed', message); } finally { // Reset loading state state.isLoading = false; setButtonLoading(false); updateAnalyzeButton(); } } function setButtonLoading(isLoading) { const btn = elements.analyzeBtn; const idle = btn.querySelector('.analyze-btn__idle'); const loading = btn.querySelector('.analyze-btn__loading'); if (isLoading) { btn.classList.add('is-loading'); idle.hidden = true; loading.hidden = false; } else { btn.classList.remove('is-loading'); idle.hidden = false; loading.hidden = true; } } // ==================== RESULTS ==================== function showResults(result) { const { resultsEmpty, resultsContent, verdictCard, verdictIcon, verdictLabel, confidenceValue, confidenceFill, resultLanguage, explanationText } = elements; // Hide empty state, show content resultsEmpty.hidden = true; resultsContent.hidden = false; // Determine if AI or Human const isAI = result.classification === 'AI_GENERATED'; // Update verdict card verdictCard.classList.remove('is-ai', 'is-human'); verdictCard.classList.add(isAI ? 'is-ai' : 'is-human'); verdictIcon.textContent = isAI ? '🤖' : '👤'; verdictLabel.textContent = isAI ? 'AI GENERATED' : 'HUMAN'; // Update confidence const confidencePercent = Math.round(result.confidenceScore * 100); confidenceValue.textContent = `${confidencePercent}%`; // Animate confidence bar setTimeout(() => { confidenceFill.style.width = `${confidencePercent}%`; }, 100); // Update language resultLanguage.textContent = result.language; // Update explanation explanationText.textContent = result.explanation || 'No detailed explanation available.'; } // ==================== TOAST NOTIFICATIONS ==================== function showToast(type, title, message, duration = 5000) { const toast = document.createElement('div'); toast.className = `toast toast--${type}`; toast.innerHTML = `
${type === 'error' ? ` ` : ` `}

${title}

${message}

`; // Add close handler const closeBtn = toast.querySelector('.toast__close'); closeBtn.addEventListener('click', () => removeToast(toast)); // Add to container elements.toastContainer.appendChild(toast); // Auto remove after duration setTimeout(() => removeToast(toast), duration); } function removeToast(toast) { toast.style.animation = 'fadeOut 0.2s ease-out forwards'; setTimeout(() => toast.remove(), 200); } // ==================== UTILITIES ==================== function getAudioDuration(file) { return new Promise((resolve, reject) => { const audio = new Audio(); const url = URL.createObjectURL(file); let resolved = false; // Timeout fallback: if browser can't read metadata in 2 seconds, assume it's valid const timeout = setTimeout(() => { if (!resolved) { URL.revokeObjectURL(url); console.warn('⚠️ Could not read audio metadata, assuming valid duration'); resolve(10); // Assume 10 seconds as fallback resolved = true; } }, 2000); audio.addEventListener('loadedmetadata', () => { if (!resolved) { clearTimeout(timeout); URL.revokeObjectURL(url); resolve(audio.duration); resolved = true; } }); audio.addEventListener('error', (e) => { if (!resolved) { clearTimeout(timeout); URL.revokeObjectURL(url); console.warn('⚠️ Could not load audio metadata, assuming valid duration'); resolve(10); // Assume 10 seconds as fallback instead of rejecting resolved = true; } }); audio.src = url; }); } function formatFileSize(bytes) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } // Add fadeOut animation const style = document.createElement('style'); style.textContent = ` @keyframes fadeOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(10px); } } `; document.head.appendChild(style);