|
|
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();
|
|
|
});
|
|
|
}); |