llgen / static /script.js
Fred808's picture
Upload 2 files
ec8cf7e verified
// 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 = `
<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;
// 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 = `
<span class="log-time">[${timestamp}]</span>
<span class="log-message">${escapeHtml(message)}</span>
`;
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 = `
<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;
}
// 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 = '<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);
// 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();
});