// ==================== 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 = ' 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 => `
${file.name}
`).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 = ' 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 = `${message}`;
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;