// Configuration const API_BASE_URL = window.location.origin; // Use same origin as the UI const REFRESH_INTERVAL = 10000; // 10 seconds (increased for demo) const MAX_LOGS = 100; // Global state let refreshInterval; let autoScrollEnabled = true; let currentFiles = []; let selectedFile = null; let apiConnected = false; // DOM Elements const elements = { statusIndicator: document.getElementById('statusIndicator'), totalFiles: document.getElementById('totalFiles'), processedFiles: document.getElementById('processedFiles'), extractedCourses: document.getElementById('extractedCourses'), extractedVideos: document.getElementById('extractedVideos'), extractedFrames: document.getElementById('extractedFrames'), trackedCursors: document.getElementById('trackedCursors'), currentFile: document.getElementById('currentFile'), progressFill: document.getElementById('progressFill'), progressText: document.getElementById('progressText'), startIndex: document.getElementById('startIndex'), startProcessing: document.getElementById('startProcessing'), stopProcessing: document.getElementById('stopProcessing'), refreshBtn: document.getElementById('refreshBtn'), themeToggle: document.getElementById('themeToggle'), fileCount: document.getElementById('fileCount'), filesGrid: document.getElementById('filesGrid'), logsContainer: document.getElementById('logsContainer'), clearLogs: document.getElementById('clearLogs'), autoScroll: document.getElementById('autoScroll'), fileModal: document.getElementById('fileModal'), modalTitle: document.getElementById('modalTitle'), modalBody: document.getElementById('modalBody'), modalClose: document.getElementById('modalClose'), downloadFile: document.getElementById('downloadFile'), viewFrames: document.getElementById('viewFrames'), loadingOverlay: document.getElementById('loadingOverlay'), toastContainer: document.getElementById('toastContainer') }; // Initialize the application document.addEventListener('DOMContentLoaded', function() { initializeTheme(); setupEventListeners(); startAutoRefresh(); fetchInitialData(); }); // Theme Management function initializeTheme() { const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); updateThemeIcon(savedTheme); } function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); updateThemeIcon(newTheme); } function updateThemeIcon(theme) { const icon = elements.themeToggle.querySelector('i'); icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon'; } // Event Listeners function setupEventListeners() { elements.themeToggle.addEventListener('click', toggleTheme); elements.refreshBtn.addEventListener('click', () => { showToast('Refreshing data...', 'info'); fetchAllData(); }); elements.startProcessing.addEventListener('click', startProcessing); elements.stopProcessing.addEventListener('click', stopProcessing); elements.clearLogs.addEventListener('click', clearLogs); elements.autoScroll.addEventListener('click', toggleAutoScroll); elements.modalClose.addEventListener('click', closeModal); elements.fileModal.addEventListener('click', (e) => { if (e.target === elements.fileModal) closeModal(); }); elements.downloadFile.addEventListener('click', downloadSelectedFile); elements.viewFrames.addEventListener('click', viewFrames); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); if (e.key === 'F5') { e.preventDefault(); fetchAllData(); } }); } // API Functions async function apiRequest(endpoint, options = {}) { try { showLoading(); const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: { 'Content-Type': 'application/json', ...options.headers }, ...options }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } apiConnected = true; return await response.json(); } catch (error) { console.error('API request failed:', error); apiConnected = false; // Show different messages based on error type if (error.name === 'TypeError' && error.message.includes('fetch')) { showToast('API server not running. Please start the cursor tracking API on port 8000.', 'warning'); } else { showToast(`API Error: ${error.message}`, 'error'); } throw error; } finally { hideLoading(); } } async function fetchStatus() { try { const data = await apiRequest('/status'); updateStatusDisplay(data.processing_status); return data; } catch (error) { // Show demo data when API is not connected const demoStatus = { is_running: false, total_files: 150, processed_files: 45, extracted_courses: 12, extracted_videos: 89, extracted_frames_count: 15420, tracked_cursors_count: 8934, current_file: null, logs: [ '[Demo Mode] API server not connected', '[Demo Mode] This is a demonstration of the UI', '[Demo Mode] Start the API server on port 8000 to see real data' ] }; updateStatusDisplay(demoStatus); } } async function fetchCursorData() { try { const data = await apiRequest('/cursor-data'); currentFiles = data.files || []; updateFilesDisplay(currentFiles); return data; } catch (error) { // Show demo files when API is not connected const demoFiles = [ { filename: 'course_1_video_1_mp4_cursor_data.json', size_bytes: 45678, modified_time: 'Sun Jul 13 19:30:15 2025' }, { filename: 'course_2_video_3_mp4_cursor_data.json', size_bytes: 67890, modified_time: 'Sun Jul 13 18:45:22 2025' }, { filename: 'course_3_video_2_mp4_cursor_data.json', size_bytes: 34567, modified_time: 'Sun Jul 13 17:20:10 2025' } ]; currentFiles = demoFiles; updateFilesDisplay(demoFiles); } } async function fetchFileDetails(filename) { try { const data = await apiRequest(`/cursor-data/${filename}/summary`); return data; } catch (error) { showToast(`Failed to fetch details for ${filename}`, 'error'); return null; } } async function startProcessing() { try { const startIndex = parseInt(elements.startIndex.value) || 0; const data = await apiRequest('/start-processing', { method: 'POST', body: JSON.stringify({ start_index: startIndex }) }); showToast(data.message, data.status === 'started' ? 'success' : 'info'); if (data.status === 'started') { elements.startProcessing.disabled = true; elements.stopProcessing.disabled = false; } } catch (error) { showToast('Failed to start processing', 'error'); } } async function stopProcessing() { try { const data = await apiRequest('/stop-processing', { method: 'POST' }); showToast(data.message, 'info'); elements.startProcessing.disabled = false; elements.stopProcessing.disabled = true; } catch (error) { showToast('Failed to stop processing', 'error'); } } // Display Update Functions function updateStatusDisplay(status) { // Update status indicator const statusDot = elements.statusIndicator.querySelector('.status-dot'); const statusText = elements.statusIndicator.querySelector('.status-text'); if (status.is_running) { statusDot.className = 'status-dot running'; statusText.textContent = 'Processing'; elements.startProcessing.disabled = true; elements.stopProcessing.disabled = false; } else { statusDot.className = 'status-dot stopped'; statusText.textContent = 'Idle'; elements.startProcessing.disabled = false; elements.stopProcessing.disabled = true; } // Update statistics elements.totalFiles.textContent = status.total_files || 0; elements.processedFiles.textContent = status.processed_files || 0; elements.extractedCourses.textContent = status.extracted_courses || 0; elements.extractedVideos.textContent = status.extracted_videos || 0; elements.extractedFrames.textContent = status.extracted_frames_count || 0; elements.trackedCursors.textContent = status.tracked_cursors_count || 0; // Update current file and progress const currentFile = status.current_file || 'None'; elements.currentFile.textContent = currentFile; const progress = status.total_files > 0 ? Math.round((status.processed_files / status.total_files) * 100) : 0; elements.progressFill.style.width = `${progress}%`; elements.progressText.textContent = `${progress}%`; // Update logs if (status.logs && status.logs.length > 0) { updateLogs(status.logs); } } function updateFilesDisplay(files) { elements.fileCount.textContent = `${files.length} files`; if (files.length === 0) { elements.filesGrid.innerHTML = `

No cursor tracking files found yet.

Files will appear here after processing completes.

`; return; } elements.filesGrid.innerHTML = files.map(file => `
${file.filename}
${formatFileSize(file.size_bytes)}
Modified: ${formatDate(file.modified_time)}
`).join(''); } function updateLogs(logs) { const container = elements.logsContainer; // Clear existing logs if we have new ones if (logs.length > 0) { container.innerHTML = ''; } logs.slice(-MAX_LOGS).forEach(log => { const logEntry = document.createElement('div'); logEntry.className = 'log-entry'; // Determine log type based on content let logType = ''; if (log.includes('❌') || log.includes('ERROR') || log.includes('Failed')) { logType = 'error'; } else if (log.includes('✅') || log.includes('SUCCESS') || log.includes('Successfully')) { logType = 'success'; } else if (log.includes('⚠️') || log.includes('WARN')) { logType = 'warning'; } if (logType) { logEntry.classList.add(logType); } // Extract timestamp and message const timestampMatch = log.match(/^\[([^\]]+)\]/); const timestamp = timestampMatch ? timestampMatch[1] : new Date().toLocaleTimeString(); const message = timestampMatch ? log.substring(timestampMatch[0].length).trim() : log; logEntry.innerHTML = ` [${timestamp}] ${escapeHtml(message)} `; container.appendChild(logEntry); }); // Auto-scroll to bottom if enabled if (autoScrollEnabled) { container.scrollTop = container.scrollHeight; } } // Modal Functions async function openFileModal(filename) { selectedFile = filename; elements.modalTitle.textContent = `File Details: ${filename}`; showModal(); const details = await fetchFileDetails(filename); if (details) { elements.modalBody.innerHTML = `

File Information

File Size: ${formatFileSize(details.file_size_bytes)}
Modified: ${details.modified_time}

Frame Statistics

Total Frames: ${details.total_frames}
Cursor Active: ${details.cursor_active_frames}
Cursor Inactive: ${details.cursor_inactive_frames}
Detection Rate: ${(details.cursor_detection_rate * 100).toFixed(1)}%

Confidence Statistics

Average: ${details.confidence_stats.average.toFixed(3)}
Maximum: ${details.confidence_stats.maximum.toFixed(3)}
Minimum: ${details.confidence_stats.minimum.toFixed(3)}
Measurements: ${details.confidence_stats.total_measurements}

Templates Used

${details.templates_used.length > 0 ? details.templates_used.map(template => `${template}`).join('') : 'No templates detected' }
`; } else { elements.modalBody.innerHTML = '

Failed to load file details.

'; } } function showModal() { elements.fileModal.classList.add('show'); document.body.style.overflow = 'hidden'; } function closeModal() { elements.fileModal.classList.remove('show'); document.body.style.overflow = ''; selectedFile = null; } // File Operations async function downloadFile(filename) { try { const response = await fetch(`${API_BASE_URL}/cursor-data/${filename}`); if (!response.ok) throw new Error('Download failed'); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); showToast(`Downloaded ${filename}`, 'success'); } catch (error) { showToast(`Failed to download ${filename}`, 'error'); } } function downloadSelectedFile() { if (selectedFile) { downloadFile(selectedFile); } } function viewFrames() { if (selectedFile) { showToast('Frame viewer feature coming soon!', 'info'); } } // Utility Functions function formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function formatDate(dateString) { try { return new Date(dateString).toLocaleDateString(); } catch { return dateString; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Log Management function clearLogs() { elements.logsContainer.innerHTML = '
[' + new Date().toLocaleTimeString() + ']Logs cleared
'; showToast('Logs cleared', 'info'); } function toggleAutoScroll() { autoScrollEnabled = !autoScrollEnabled; elements.autoScroll.classList.toggle('active', autoScrollEnabled); if (autoScrollEnabled) { elements.logsContainer.scrollTop = elements.logsContainer.scrollHeight; } } // Loading and Toast Functions function showLoading() { elements.loadingOverlay.classList.add('show'); } function hideLoading() { elements.loadingOverlay.classList.remove('show'); } function showToast(message, type = 'info', duration = 5000) { const toast = document.createElement('div'); toast.className = `toast ${type}`; const icons = { success: 'fas fa-check-circle', error: 'fas fa-exclamation-circle', warning: 'fas fa-exclamation-triangle', info: 'fas fa-info-circle' }; toast.innerHTML = `
${escapeHtml(message)}
`; const closeBtn = toast.querySelector('.toast-close'); closeBtn.addEventListener('click', () => removeToast(toast)); elements.toastContainer.appendChild(toast); // Auto-remove after duration setTimeout(() => removeToast(toast), duration); } function removeToast(toast) { if (toast && toast.parentNode) { toast.style.animation = 'slideInRight 0.3s ease reverse'; setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); } } // Auto-refresh Management function startAutoRefresh() { fetchAllData(); // Initial fetch refreshInterval = setInterval(fetchAllData, REFRESH_INTERVAL); } function stopAutoRefresh() { if (refreshInterval) { clearInterval(refreshInterval); refreshInterval = null; } } async function fetchInitialData() { await fetchAllData(); } async function fetchAllData() { try { await Promise.all([ fetchStatus(), fetchCursorData() ]); } catch (error) { console.error('Failed to fetch data:', error); } } // Cleanup on page unload window.addEventListener('beforeunload', () => { stopAutoRefresh(); });