Spaces:
Runtime error
Runtime error
| document.addEventListener('DOMContentLoaded', function() { | |
| // Configuration | |
| const API_BASE_URL = window.location.origin; | |
| const REFRESH_INTERVAL = 10000; // 10 seconds | |
| 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'), | |
| analyzedFrames: document.getElementById('analyzedFrames'), | |
| 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'), | |
| viewSummary: document.getElementById('viewSummary'), | |
| loadingOverlay: document.getElementById('loadingOverlay'), | |
| toastContainer: document.getElementById('toastContainer') | |
| }; | |
| // Initialize the application | |
| 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.viewSummary.addEventListener('click', viewSummary); | |
| // 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; | |
| if (error.name === 'TypeError' && error.message.includes('fetch')) { | |
| showToast('API server not running. Please start the video analysis 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) { | |
| // 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, | |
| analyzed_frames_count: 12000, | |
| 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 fetchAnalysisData() { | |
| try { | |
| const data = await apiRequest('/analysis-data'); | |
| currentFiles = data.files || []; | |
| updateFilesDisplay(currentFiles); | |
| return data; | |
| } catch (error) { | |
| // Demo files when API is not connected | |
| const demoFiles = [ | |
| { | |
| filename: 'course_1_video_1_mp4_analysis.json', | |
| size_bytes: 45678, | |
| modified_time: 'Sun Jul 13 19:30:15 2025' | |
| }, | |
| { | |
| filename: 'course_2_video_3_mp4_analysis.json', | |
| size_bytes: 67890, | |
| modified_time: 'Sun Jul 13 18:45:22 2025' | |
| }, | |
| { | |
| filename: 'course_3_video_2_mp4_analysis.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(`/analysis-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.analyzedFrames.textContent = status.analyzed_frames_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 = ` | |
| <div class="no-files"> | |
| <i class="fas fa-folder-open"></i> | |
| <p>No analysis files found yet.</p> | |
| <p>Files will appear here after processing completes.</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| elements.filesGrid.innerHTML = files.map(file => ` | |
| <div class="file-card" onclick="openFileModal('${file.filename}')"> | |
| <div class="file-header"> | |
| <div class="file-name">${file.filename}</div> | |
| <div class="file-size">${formatFileSize(file.size_bytes)}</div> | |
| </div> | |
| <div class="file-stats"> | |
| <div class="file-stat"> | |
| <span class="file-stat-label">Modified:</span> | |
| <span class="file-stat-value">${formatDate(file.modified_time)}</span> | |
| </div> | |
| </div> | |
| <div class="file-actions"> | |
| <button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); downloadFile('${file.filename}')"> | |
| <i class="fas fa-download"></i> | |
| Download | |
| </button> | |
| <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); openFileModal('${file.filename}')"> | |
| <i class="fas fa-eye"></i> | |
| Details | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function updateLogs(logs) { | |
| const container = elements.logsContainer; | |
| if (logs.length > 0) { | |
| container.innerHTML = ''; | |
| } | |
| logs.slice(-MAX_LOGS).forEach(log => { | |
| const logEntry = document.createElement('div'); | |
| logEntry.className = 'log-entry'; | |
| 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); | |
| } | |
| const timestampMatch = log.match(/^\[([^\]]+)\]/); | |
| const timestamp = timestampMatch ? timestampMatch[1] : new Date().toLocaleTimeString(); | |
| const message = timestampMatch ? log.substring(timestampMatch[0].length).trim() : log; | |
| logEntry.innerHTML = ` | |
| <span class="log-time">[${timestamp}]</span> | |
| <span class="log-message">${escapeHtml(message)}</span> | |
| `; | |
| container.appendChild(logEntry); | |
| }); | |
| if (autoScrollEnabled) { | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| } | |
| // Modal Functions | |
| async function openFileModal(filename) { | |
| selectedFile = filename; | |
| elements.modalTitle.textContent = `Analysis Details: ${filename}`; | |
| showModal(); | |
| const details = await fetchFileDetails(filename); | |
| if (details) { | |
| elements.modalBody.innerHTML = ` | |
| <div class="file-details"> | |
| <div class="detail-section"> | |
| <h4>File Information</h4> | |
| <div class="detail-grid"> | |
| <div class="detail-item"> | |
| <span class="detail-label">File Size:</span> | |
| <span class="detail-value">${formatFileSize(details.file_size_bytes)}</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Modified:</span> | |
| <span class="detail-value">${details.modified_time}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="detail-section"> | |
| <h4>Frame Statistics</h4> | |
| <div class="detail-grid"> | |
| <div class="detail-item"> | |
| <span class="detail-label">Total Frames:</span> | |
| <span class="detail-value">${details.total_frames}</span> | |
| </div> | |
| <div class="detail-item"> | |
| <span class="detail-label">Frames Analyzed:</span> | |
| <span class="detail-value">${details.frames_with_descriptions}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="detail-section"> | |
| <h4>Analysis Summary</h4> | |
| <div class="summary-content"> | |
| <p><strong>High Level Goal:</strong> ${details.high_level_goal || 'Not available'}</p> | |
| <p><strong>Final Goal:</strong> ${details.final_goal || 'Not available'}</p> | |
| <h5>Key Steps:</h5> | |
| <ul class="steps-list"> | |
| ${details.steps.map(step => ` | |
| <li> | |
| <strong>${step.action}:</strong> ${step.description} | |
| </li> | |
| `).join('')} | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <style> | |
| .file-details { font-size: 0.875rem; } | |
| .detail-section { margin-bottom: 1.5rem; } | |
| .detail-section h4 { | |
| margin-bottom: 0.75rem; | |
| color: var(--accent-primary); | |
| font-weight: 600; | |
| } | |
| .detail-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 0.5rem; | |
| } | |
| .detail-item { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 0.5rem; | |
| background: var(--bg-secondary); | |
| border-radius: var(--radius); | |
| } | |
| .detail-label { color: var(--text-secondary); } | |
| .detail-value { font-weight: 500; } | |
| .summary-content { padding: 0.5rem; } | |
| .steps-list { | |
| margin-top: 0.5rem; | |
| padding-left: 1.5rem; | |
| } | |
| .steps-list li { | |
| margin-bottom: 0.5rem; | |
| line-height: 1.4; | |
| } | |
| </style> | |
| `; | |
| } else { | |
| elements.modalBody.innerHTML = '<p>Failed to load analysis details.</p>'; | |
| } | |
| } | |
| 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}/analysis-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 viewSummary() { | |
| if (selectedFile) { | |
| window.open(`${API_BASE_URL}/analysis-data/${selectedFile}/summary`, '_blank'); | |
| } | |
| } | |
| // 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 = '<div class="log-entry"><span class="log-time">[' + | |
| new Date().toLocaleTimeString() + ']</span><span class="log-message">Logs cleared</span></div>'; | |
| 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 = ` | |
| <i class="toast-icon ${icons[type]}"></i> | |
| <div class="toast-content"> | |
| <div class="toast-message">${escapeHtml(message)}</div> | |
| </div> | |
| <button class="toast-close"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| `; | |
| const closeBtn = toast.querySelector('.toast-close'); | |
| closeBtn.addEventListener('click', () => removeToast(toast)); | |
| elements.toastContainer.appendChild(toast); | |
| 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(); | |
| 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(), | |
| fetchAnalysisData() | |
| ]); | |
| } catch (error) { | |
| console.error('Failed to fetch data:', error); | |
| } | |
| } | |
| // Make functions available globally for HTML onclick handlers | |
| window.openFileModal = openFileModal; | |
| window.downloadFile = downloadFile; | |
| // Cleanup on page unload | |
| window.addEventListener('beforeunload', () => { | |
| stopAutoRefresh(); | |
| }); | |
| }); |