Spaces:
Sleeping
Sleeping
// ==================== Constants & Global State ==================== | |
const API_BASE_URL = window.location.origin; // Base URL for API calls, using current origin | |
let currentFile = null; // Holds the currently selected video file (Blob or File) | |
let analysisAbortController = null; // Controller to abort video analysis requests | |
let spCredentials = {}; // Stores SharePoint credentials after connection | |
let isSharePointFile = false; // Flag indicating if the file came from SharePoint | |
let progressSource = null; // EventSource for server-sent events during processing | |
// ==================== Initialization ==================== | |
document.addEventListener('DOMContentLoaded', () => { | |
initApp(); // Kick off app setup | |
}); | |
function initApp() { | |
initEventListeners(); // Attach all UI event handlers | |
showScreen('initialScreen'); // Display the upload/timestamp screen | |
} | |
function initEventListeners() { | |
// File upload via click | |
document.getElementById('dropZone').addEventListener('click', () => { | |
document.getElementById('videoInput').click(); // Trigger hidden file input | |
}); | |
// File input change handler | |
document.getElementById('videoInput').addEventListener('change', handleFileSelect); | |
// Drag-and-drop handlers | |
const dropZone = document.getElementById('dropZone'); | |
dropZone.addEventListener('dragover', handleDragOver); // Highlight zone on drag over | |
dropZone.addEventListener('drop', handleDrop); // Handle file drop | |
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); // Remove highlight | |
// Navigation buttons to switch screens | |
document.querySelectorAll('[data-screen]').forEach(btn => { | |
btn.addEventListener('click', () => { | |
if (analysisAbortController) { | |
// If analysis in flight, cancel then switch | |
cancelAnalysis().finally(() => showScreen(btn.dataset.screen)); | |
} else { | |
showScreen(btn.dataset.screen); | |
} | |
}); | |
}); | |
// "New Analysis" button on results screen | |
document.querySelector('#resultsScreen .btn.secondary').addEventListener('click', handleNewAnalysis); | |
// Analysis control buttons | |
document.getElementById('analyzeBtn').addEventListener('click', startAnalysis); // Start processing | |
document.getElementById('cancelBtn').addEventListener('click', cancelAnalysis); // Cancel processing | |
document.getElementById('downloadBtn').addEventListener('click', () => { | |
// Download handled dynamically in setupDownload() | |
}); | |
// Timestamp input validation handlers | |
document.getElementById('timestamp1').addEventListener('input', validateTimestamps); | |
document.getElementById('timestamp2').addEventListener('input', validateTimestamps); | |
document.getElementById('timestamp3').addEventListener('input', validateTimestamps); | |
} | |
// ==================== High-Level Workflows ==================== | |
// Start a brand new analysis (from results screen) | |
async function handleNewAnalysis() { | |
try { | |
await cancelAnalysis(); // Abort any running job | |
resetApp(); // Clear form and state | |
resetAnalyzeButton(); // Restore Analyze button | |
showScreen('initialScreen'); // Go back to upload | |
} catch (error) { | |
showError(`Failed to start new analysis: ${error.message}`); // Show error | |
} | |
} | |
// Handle SharePoint credentials submission and file listing | |
async function handleSpCredSubmit(event) { | |
event.preventDefault(); | |
const submitBtn = event.target.querySelector('button[type="submit"]'); | |
const originalText = submitBtn.innerHTML; | |
submitBtn.disabled = true; // Prevent double submits | |
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Connecting...'; // Show spinner | |
// Collect credentials from form | |
spCredentials = { | |
siteUrl: document.getElementById('spSiteUrl').value.trim(), | |
clientId: document.getElementById('spClientId').value.trim(), | |
clientSecret: document.getElementById('spClientSecret').value.trim(), | |
docLibrary: document.getElementById('spDocLibrary').value.trim() | |
}; | |
try { | |
const response = await fetch(`${API_BASE_URL}/api/sharepoint/files`, { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | |
body: new URLSearchParams({ | |
...spCredentials, | |
doc_library: spCredentials.docLibrary | |
}) | |
}); | |
if (!response.ok) { | |
const errorData = await response.json().catch(() => ({})); | |
throw new Error(errorData.detail || response.statusText); | |
} | |
const files = await response.json(); // Array of SharePoint files | |
renderSpFileList(files); // Populate file list UI | |
showScreen('sharepointFileScreen'); // Switch to file selection | |
} catch (error) { | |
showError(`SharePoint connection failed: ${error.message}`); | |
} finally { | |
submitBtn.disabled = false; // Restore button | |
submitBtn.innerHTML = originalText; | |
} | |
} | |
// Render list of SharePoint files with Select buttons | |
function renderSpFileList(files) { | |
const fileList = document.getElementById('spFileList'); | |
if (!fileList) return; | |
fileList.innerHTML = files.map(file => ` | |
<div class="sp-file-item"> | |
<span>${file.name}</span> | |
<button class="btn" onclick="handleSpFile('${file.id}')"> | |
<i class="fas fa-play"></i> Select | |
</button> | |
</div> | |
`).join(''); | |
} | |
// Handle selecting and downloading a file from SharePoint | |
async function handleSpFile(fileId) { | |
const selectBtn = event.target; | |
const originalText = selectBtn.innerHTML; | |
selectBtn.disabled = true; | |
selectBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading...'; | |
try { | |
const formData = new URLSearchParams({ | |
...spCredentials, | |
file_id: fileId | |
}); | |
const response = await fetch(`${API_BASE_URL}/api/sharepoint/download`, { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | |
body: formData | |
}); | |
if (!response.ok) { | |
const errorData = await response.json().catch(() => ({})); | |
throw new Error(errorData.detail || response.statusText); | |
} | |
currentFile = await response.blob(); // Store the downloaded blob | |
isSharePointFile = true; // Mark as SharePoint source | |
await startAnalysis(); // Begin processing | |
} catch (error) { | |
showError(`File download failed: ${error.message}`); | |
} finally { | |
selectBtn.disabled = false; // Restore button | |
selectBtn.innerHTML = originalText; | |
} | |
} | |
// Kick off video analysis by sending file and timestamps to backend | |
async function startAnalysis() { | |
const analyzeBtn = document.getElementById('analyzeBtn'); | |
analyzeBtn.disabled = true; // Prevent re-click | |
analyzeBtn.onclick = null; | |
analyzeBtn.innerText = 'Analyzing…'; // Update label | |
if (!currentFile) { | |
showError('Please select a file first!'); | |
resetAnalyzeButton(); | |
return; | |
} | |
const t1 = document.getElementById('timestamp1').value; | |
const t2 = document.getElementById('timestamp2').value; | |
const t3 = document.getElementById('timestamp3').value; | |
if (!validateTimeOrder(t1, t2, t3)) { | |
showError('Timestamps must be in ascending order'); | |
resetAnalyzeButton(); | |
return; | |
} | |
showScreen('progressScreen'); // Show progress UI | |
analysisAbortController = new AbortController(); // New controller | |
try { | |
const formData = new FormData(); | |
formData.append('video', currentFile); | |
formData.append('timestamp1', t1); | |
formData.append('timestamp2', t2); | |
formData.append('timestamp3', t3); | |
const response = await fetch(`${API_BASE_URL}/api/process-video`, { | |
method: 'POST', | |
body: formData, | |
signal: analysisAbortController.signal | |
}); | |
if (!response.ok) { | |
const errorData = await response.json().catch(() => ({})); | |
throw new Error(errorData.detail || response.statusText); | |
} | |
const { process_id } = await response.json(); | |
setupProgressTracker(process_id); | |
} catch (error) { | |
if (error.name !== 'AbortError') { | |
showError(`Analysis failed: ${error.message}`); | |
showScreen('initialScreen'); | |
} | |
} | |
} | |
// ==================== File Upload/Selection Handlers ==================== | |
function handleFileSelect(e) { | |
const file = e.target.files[0]; | |
if (file) handleFile(file); | |
} | |
function handleDragOver(e) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
e.currentTarget.classList.add('dragover'); | |
} | |
function handleDrop(e) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
e.currentTarget.classList.remove('dragover'); | |
const file = e.dataTransfer.files[0]; | |
if (file) handleFile(file); | |
} | |
function handleFile(file) { | |
if (!file || !file.type.startsWith('video/')) { | |
showError('Please upload a valid video file (MP4, MOV, or AVI)'); | |
return; | |
} | |
currentFile = file; | |
isSharePointFile = false; | |
const preview = document.getElementById('videoPreview'); | |
const analyzeBtn = document.getElementById('analyzeBtn'); | |
if (preview.src) URL.revokeObjectURL(preview.src); | |
preview.src = URL.createObjectURL(file); | |
preview.classList.remove('hidden'); | |
analyzeBtn.disabled = false; | |
document.getElementById('timestamp1').value = ''; | |
document.getElementById('timestamp2').value = ''; | |
document.getElementById('timestamp3').value = ''; | |
validateTimestamps(); | |
} | |
function showUploadScreen(type) { | |
if (type === 'sharepoint') { | |
showScreen('sharepointCredScreen'); | |
} else { | |
showScreen('localUploadScreen'); | |
} | |
} | |
// ==================== Progress Tracking ==================== | |
function setupProgressTracker(processId) { | |
if (progressSource) progressSource.close(); | |
progressSource = new EventSource(`${API_BASE_URL}/api/progress/${processId}`); | |
progressSource.onmessage = (event) => { | |
try { | |
const data = JSON.parse(event.data); | |
if (data.status === 'completed') { | |
handleAnalysisComplete(processId); | |
progressSource.close(); | |
} else if (data.status === 'error') { | |
showError(data.error || 'Analysis failed'); | |
progressSource.close(); | |
showScreen('initialScreen'); | |
} else { | |
updateProgressUI(data); | |
} | |
} catch (error) { | |
console.error('Error parsing progress:', error); | |
} | |
}; | |
progressSource.onerror = () => { | |
console.log('SSE error - attempting reconnect'); | |
setTimeout(() => setupProgressTracker(processId), 2000); | |
}; | |
} | |
function updateProgressUI(progress) { | |
const progressBar = document.getElementById('progressBar'); | |
const progressMessage = document.getElementById('progressMessage'); | |
progressBar.style.width = `${progress.percent}%`; | |
progressMessage.textContent = progress.message; | |
if (progress.current && progress.total) { | |
document.getElementById('frameCounter').textContent = `${progress.current}/${progress.total} seconds processed`; | |
} | |
} | |
async function handleAnalysisComplete(processId) { | |
try { | |
const response = await fetch(`${API_BASE_URL}/api/results/${processId}`); | |
const blob = await response.blob(); | |
setupDownload(blob); | |
showScreen('resultsScreen'); | |
} catch (error) { | |
showError('Failed to retrieve results'); | |
} | |
} | |
// ==================== Utilities ==================== | |
function showScreen(screenId) { | |
document.querySelectorAll('.card').forEach(el => el.classList.add('hidden')); | |
const targetScreen = document.getElementById(screenId); | |
if (targetScreen) { | |
targetScreen.classList.remove('hidden'); | |
window.scrollTo(0, 0); | |
} else { | |
console.error(`Screen with ID ${screenId} not found`); | |
} | |
} | |
function showError(message) { | |
const errorDiv = document.createElement('div'); | |
errorDiv.className = 'error-message'; | |
errorDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i><span>${message}</span>`; | |
document.body.prepend(errorDiv); | |
setTimeout(() => { errorDiv.classList.add('fade-out'); setTimeout(() => errorDiv.remove(), 500); }, 5000); | |
} | |
function validateTimestamps() { | |
const t1 = document.getElementById('timestamp1'); | |
const t2 = document.getElementById('timestamp2'); | |
const t3 = document.getElementById('timestamp3'); | |
const analyzeBtn = document.getElementById('analyzeBtn'); | |
const isValid = t1.checkValidity() && t2.checkValidity() && t3.checkValidity() && t1.value !== '' && t2.value !== '' && t3.value !== ''; | |
analyzeBtn.disabled = !isValid; | |
} | |
function validateTimeOrder(t1, t2, t3) { | |
const toSeconds = t => { const [h, m, s] = t.split(':').map(Number); return h*3600 + m*60 + s; }; | |
return toSeconds(t1) < toSeconds(t2) && toSeconds(t2) < toSeconds(t3); | |
} | |
function resetAnalyzeButton() { | |
const btn = document.getElementById('analyzeBtn'); | |
btn.disabled = false; | |
btn.innerText = 'Start Analysis'; | |
btn.onclick = startAnalysis; | |
} | |
function resetApp() { | |
const preview = document.getElementById('videoPreview'); | |
if (preview.src) URL.revokeObjectURL(preview.src); | |
preview.src = ''; | |
preview.classList.add('hidden'); | |
document.getElementById('videoInput').value = ''; | |
const progressBar = document.getElementById('progressBar'); if (progressBar) progressBar.style.width = '0%'; | |
const progressMessage = document.getElementById('progressMessage'); if (progressMessage) progressMessage.textContent = ''; | |
const spForm = document.getElementById('spCredForm'); if (spForm) spForm.reset(); | |
if (progressSource) { progressSource.close(); progressSource = null; } | |
currentFile = null; | |
isSharePointFile = false; | |
spCredentials = {}; | |
} | |
async function cancelAnalysis() { | |
try { | |
if (progressSource) { | |
progressSource.close(); | |
progressSource = null; | |
} | |
if (!analysisAbortController) return; | |
const progressMessage = document.getElementById('progressMessage'); if (progressMessage) progressMessage.textContent = "Cancelling analysis..."; | |
analysisAbortController.abort(); | |
await fetch(`${API_BASE_URL}/api/cancel-analysis`, { method: 'POST' }); | |
} catch (error) { | |
console.error('Cancellation error:', error); | |
throw error; | |
} finally { | |
analysisAbortController = null; | |
} | |
} | |
function setupDownload(blob) { | |
const url = URL.createObjectURL(blob); | |
const downloadBtn = document.getElementById('downloadBtn'); | |
downloadBtn.onclick = null; | |
downloadBtn.onclick = () => { | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = `stranger_danger_analysis_${new Date().toISOString().slice(0,10)}.csv`; | |
document.body.appendChild(a); | |
a.click(); | |
setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); | |
}; | |
} | |
// ==================== Global Exports ==================== | |
window.showUploadScreen = showUploadScreen; | |
window.handleSpCredSubmit = handleSpCredSubmit; | |
window.handleSpFile = handleSpFile; | |
window.startAnalysis = startAnalysis; | |
window.cancelAnalysis = cancelAnalysis; | |
window.resetApp = resetApp; | |