| |
| const API_BASE_URL = window.location.origin; |
| const REFRESH_INTERVAL = 10000; |
| const MAX_LOGS = 100; |
|
|
| |
| let refreshInterval; |
| let autoScrollEnabled = true; |
| let currentFiles = []; |
| let selectedFile = null; |
| let apiConnected = false; |
|
|
| |
| 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') |
| }; |
|
|
| |
| document.addEventListener('DOMContentLoaded', function() { |
| initializeTheme(); |
| setupEventListeners(); |
| startAutoRefresh(); |
| fetchInitialData(); |
| }); |
|
|
| |
| 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'; |
| } |
|
|
| |
| 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); |
| |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'Escape') closeModal(); |
| if (e.key === 'F5') { |
| e.preventDefault(); |
| fetchAllData(); |
| } |
| }); |
| } |
|
|
| |
| 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 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) { |
| |
| 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) { |
| |
| 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'); |
| } |
| } |
|
|
| |
| function updateStatusDisplay(status) { |
| |
| 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; |
| } |
| |
| |
| 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; |
| |
| |
| 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}%`; |
| |
| |
| 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" style="font-size: 3rem; color: var(--text-muted); margin-bottom: 1rem;"></i> |
| <p style="color: var(--text-muted);">No cursor tracking files found yet.</p> |
| <p style="color: var(--text-muted); font-size: 0.875rem;">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; |
| } |
| } |
|
|
| |
| async function openFileModal(filename) { |
| selectedFile = filename; |
| elements.modalTitle.textContent = `File 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">Cursor Active:</span> |
| <span class="detail-value">${details.cursor_active_frames}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="detail-label">Cursor Inactive:</span> |
| <span class="detail-value">${details.cursor_inactive_frames}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="detail-label">Detection Rate:</span> |
| <span class="detail-value">${(details.cursor_detection_rate * 100).toFixed(1)}%</span> |
| </div> |
| </div> |
| </div> |
| |
| <div class="detail-section"> |
| <h4>Confidence Statistics</h4> |
| <div class="detail-grid"> |
| <div class="detail-item"> |
| <span class="detail-label">Average:</span> |
| <span class="detail-value">${details.confidence_stats.average.toFixed(3)}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="detail-label">Maximum:</span> |
| <span class="detail-value">${details.confidence_stats.maximum.toFixed(3)}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="detail-label">Minimum:</span> |
| <span class="detail-value">${details.confidence_stats.minimum.toFixed(3)}</span> |
| </div> |
| <div class="detail-item"> |
| <span class="detail-label">Measurements:</span> |
| <span class="detail-value">${details.confidence_stats.total_measurements}</span> |
| </div> |
| </div> |
| </div> |
| |
| <div class="detail-section"> |
| <h4>Templates Used</h4> |
| <div class="template-list"> |
| ${details.templates_used.length > 0 ? |
| details.templates_used.map(template => `<span class="template-tag">${template}</span>`).join('') : |
| '<span class="no-templates">No templates detected</span>' |
| } |
| </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; } |
| .template-list { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 0.5rem; |
| } |
| .template-tag { |
| background: var(--accent-primary); |
| color: white; |
| padding: 0.25rem 0.5rem; |
| border-radius: var(--radius); |
| font-size: 0.75rem; |
| } |
| .no-templates { |
| color: var(--text-muted); |
| font-style: italic; |
| } |
| </style> |
| `; |
| } else { |
| elements.modalBody.innerHTML = '<p>Failed to load file 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; |
| } |
|
|
| |
| 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'); |
| } |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| 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(), |
| fetchCursorData() |
| ]); |
| } catch (error) { |
| console.error('Failed to fetch data:', error); |
| } |
| } |
|
|
| |
| window.addEventListener('beforeunload', () => { |
| stopAutoRefresh(); |
| }); |
|
|
|
|