| document.addEventListener('DOMContentLoaded', function() {
|
|
|
| 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'),
|
| 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')
|
| };
|
|
|
|
|
| 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.viewSummary.addEventListener('click', viewSummary);
|
|
|
|
|
| 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 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) {
|
|
|
| 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) {
|
|
|
| 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');
|
| }
|
| }
|
|
|
|
|
| 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.analyzedFrames.textContent = status.analyzed_frames_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"></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;
|
| }
|
| }
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
| 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');
|
| }
|
| }
|
|
|
|
|
| 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(),
|
| fetchAnalysisData()
|
| ]);
|
| } catch (error) {
|
| console.error('Failed to fetch data:', error);
|
| }
|
| }
|
|
|
|
|
| window.openFileModal = openFileModal;
|
| window.downloadFile = downloadFile;
|
|
|
|
|
| window.addEventListener('beforeunload', () => {
|
| stopAutoRefresh();
|
| });
|
| }); |